diff --git a/res_built/atlas/atlas0_hq.json b/res_built/atlas/atlas0_hq.json index 1c0cb736..9edcc70f 100644 --- a/res_built/atlas/atlas0_hq.json +++ b/res_built/atlas/atlas0_hq.json @@ -18,7 +18,7 @@ }, "sprites/belt/built/forward_2.png": { - "frame": {"x":1802,"y":1366,"w":116,"h":144}, + "frame": {"x":1804,"y":1366,"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":1922,"y":1405,"w":116,"h":144}, + "frame": {"x":1924,"y":1405,"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":1796,"y":1514,"w":116,"h":144}, + "frame": {"x":439,"y":1038,"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":1796,"y":1662,"w":116,"h":144}, + "frame": {"x":1801,"y":1514,"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":1916,"y":1693,"w":116,"h":144}, + "frame": {"x":1921,"y":1553,"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":1795,"y":1810,"w":116,"h":144}, + "frame": {"x":1801,"y":1662,"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":145,"y":1887,"w":116,"h":144}, + "frame": {"x":1921,"y":1701,"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":265,"y":1887,"w":116,"h":144}, + "frame": {"x":137,"y":1849,"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":1537,"y":1139,"w":116,"h":144}, + "frame": {"x":1539,"y":1139,"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":1804,"y":1218,"w":116,"h":144}, + "frame": {"x":1538,"y":1287,"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":1924,"y":1257,"w":116,"h":144}, + "frame": {"x":1806,"y":1218,"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":439,"y":1038,"w":116,"h":144}, + "frame": {"x":1926,"y":1257,"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":961,"y":1033,"w":130,"h":130}, + "frame": {"x":3,"y":1182,"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":1523,"y":1287,"w":130,"h":130}, + "frame": {"x":137,"y":1182,"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":137,"y":1700,"w":130,"h":130}, + "frame": {"x":283,"y":1703,"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":271,"y":1700,"w":130,"h":130}, + "frame": {"x":389,"y":1849,"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":545,"y":1736,"w":130,"h":130}, + "frame": {"x":417,"y":1703,"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":927,"y":1578,"w":130,"h":130}, + "frame": {"x":523,"y":1849,"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":786,"y":1649,"w":130,"h":130}, + "frame": {"x":551,"y":1703,"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":645,"y":1887,"w":130,"h":130}, + "frame": {"x":657,"y":1849,"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":779,"y":1887,"w":130,"h":130}, + "frame": {"x":791,"y":1408,"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":913,"y":1887,"w":130,"h":130}, + "frame": {"x":685,"y":1703,"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":1519,"y":1421,"w":130,"h":130}, + "frame": {"x":425,"y":1186,"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":1519,"y":1555,"w":130,"h":130}, + "frame": {"x":425,"y":1320,"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":1915,"y":1841,"w":130,"h":130}, + "frame": {"x":424,"y":1454,"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":3,"y":1700,"w":130,"h":130}, + "frame": {"x":144,"y":1465,"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":1047,"y":1887,"w":130,"h":130}, + "frame": {"x":925,"y":1476,"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":1181,"y":1887,"w":130,"h":130}, + "frame": {"x":565,"y":1542,"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":920,"y":1712,"w":130,"h":130}, + "frame": {"x":1461,"y":1703,"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":1054,"y":1735,"w":130,"h":130}, + "frame": {"x":791,"y":1849,"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":1188,"y":1735,"w":130,"h":130}, + "frame": {"x":925,"y":1610,"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":1322,"y":1703,"w":130,"h":130}, + "frame": {"x":1059,"y":1740,"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":405,"y":1315,"w":130,"h":130}, + "frame": {"x":1193,"y":1805,"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":405,"y":1449,"w":130,"h":130}, + "frame": {"x":1327,"y":1827,"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":405,"y":1583,"w":130,"h":130}, + "frame": {"x":925,"y":1744,"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":3,"y":1182,"w":130,"h":130}, + "frame": {"x":1059,"y":1874,"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":1315,"y":1887,"w":130,"h":130}, + "frame": {"x":699,"y":1542,"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":1449,"y":1887,"w":130,"h":130}, + "frame": {"x":1059,"y":1606,"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":1583,"y":1887,"w":130,"h":130}, + "frame": {"x":1193,"y":1671,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -330,7 +330,7 @@ }, "sprites/belt/built/right_13.png": { - "frame": {"x":1061,"y":1601,"w":130,"h":130}, + "frame": {"x":1327,"y":1693,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -338,7 +338,7 @@ }, "sprites/blueprints/belt_left.png": { - "frame": {"x":137,"y":1182,"w":130,"h":130}, + "frame": {"x":925,"y":1878,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -346,7 +346,7 @@ }, "sprites/blueprints/belt_right.png": { - "frame": {"x":271,"y":1182,"w":130,"h":130}, + "frame": {"x":1461,"y":1849,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -354,7 +354,7 @@ }, "sprites/blueprints/belt_top.png": { - "frame": {"x":385,"y":1887,"w":116,"h":144}, + "frame": {"x":3,"y":1318,"w":116,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":0,"w":116,"h":144}, @@ -386,7 +386,7 @@ }, "sprites/blueprints/display.png": { - "frame": {"x":1916,"y":1553,"w":128,"h":136}, + "frame": {"x":257,"y":1849,"w":128,"h":136}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":8,"y":8,"w":128,"h":136}, @@ -458,7 +458,7 @@ }, "sprites/blueprints/miner.png": { - "frame": {"x":1513,"y":1689,"w":136,"h":143}, + "frame": {"x":123,"y":1318,"w":136,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":136,"h":143}, @@ -506,7 +506,7 @@ }, "sprites/blueprints/reader.png": { - "frame": {"x":1086,"y":1313,"w":141,"h":144}, + "frame": {"x":938,"y":1181,"w":141,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":141,"h":144}, @@ -514,7 +514,7 @@ }, "sprites/blueprints/rotater-ccw.png": { - "frame": {"x":1243,"y":1123,"w":143,"h":144}, + "frame": {"x":1245,"y":1123,"w":143,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":144}, @@ -522,7 +522,7 @@ }, "sprites/blueprints/rotater-fl.png": { - "frame": {"x":940,"y":1167,"w":142,"h":144}, + "frame": {"x":1658,"y":1325,"w":142,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":142,"h":144}, @@ -530,7 +530,7 @@ }, "sprites/blueprints/rotater.png": { - "frame": {"x":1390,"y":1126,"w":143,"h":144}, + "frame": {"x":1097,"y":1171,"w":143,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":144}, @@ -538,7 +538,7 @@ }, "sprites/blueprints/splitter-compact-inverse.png": { - "frame": {"x":791,"y":1254,"w":142,"h":138}, + "frame": {"x":560,"y":1036,"w":142,"h":138}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":2,"w":142,"h":138}, @@ -546,7 +546,7 @@ }, "sprites/blueprints/splitter-compact-merge-inverse.png": { - "frame": {"x":1086,"y":1171,"w":142,"h":138}, + "frame": {"x":792,"y":1118,"w":142,"h":138}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":2,"w":142,"h":138}, @@ -554,7 +554,7 @@ }, "sprites/blueprints/splitter-compact-merge.png": { - "frame": {"x":1376,"y":1421,"w":139,"h":138}, + "frame": {"x":1373,"y":1409,"w":139,"h":138}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":2,"w":139,"h":138}, @@ -562,7 +562,7 @@ }, "sprites/blueprints/splitter-compact.png": { - "frame": {"x":1227,"y":1558,"w":139,"h":138}, + "frame": {"x":1373,"y":1551,"w":139,"h":138}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":2,"w":139,"h":138}, @@ -602,7 +602,7 @@ }, "sprites/blueprints/underground_belt_entry-tier2.png": { - "frame": {"x":405,"y":1186,"w":138,"h":125}, + "frame": {"x":1516,"y":1574,"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":3,"y":1836,"w":138,"h":112}, + "frame": {"x":283,"y":1182,"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":1653,"y":1632,"w":139,"h":112}, + "frame": {"x":1225,"y":1555,"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":645,"y":1377,"w":138,"h":112}, + "frame": {"x":283,"y":1298,"w":138,"h":112}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":138,"h":112}, @@ -656,6 +656,14 @@ "spriteSourceSize": {"x":0,"y":0,"w":144,"h":133}, "sourceSize": {"w":144,"h":144} }, +"sprites/blueprints/virtual_processor-stacker.png": +{ + "frame": {"x":961,"y":1033,"w":132,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":132,"h":144}, + "sourceSize": {"w":144,"h":144} +}, "sprites/blueprints/virtual_processor-unstacker.png": { "frame": {"x":1101,"y":704,"w":144,"h":144}, @@ -690,7 +698,7 @@ }, "sprites/blueprints/wire-turn.png": { - "frame": {"x":707,"y":1036,"w":82,"h":82}, + "frame": {"x":706,"y":1036,"w":82,"h":82}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":62,"y":62,"w":82,"h":82}, @@ -714,7 +722,7 @@ }, "sprites/blueprints/wire_tunnel.png": { - "frame": {"x":1653,"y":1748,"w":138,"h":135}, + "frame": {"x":1516,"y":1435,"w":138,"h":135}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":138,"h":135}, @@ -722,7 +730,7 @@ }, "sprites/buildings/belt_left.png": { - "frame": {"x":961,"y":1033,"w":130,"h":130}, + "frame": {"x":3,"y":1182,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":14,"w":130,"h":130}, @@ -730,7 +738,7 @@ }, "sprites/buildings/belt_right.png": { - "frame": {"x":1047,"y":1887,"w":130,"h":130}, + "frame": {"x":925,"y":1476,"w":130,"h":130}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":14,"y":14,"w":130,"h":130}, @@ -810,7 +818,7 @@ }, "sprites/buildings/logic_gate-or.png": { - "frame": {"x":559,"y":1173,"w":143,"h":123}, + "frame": {"x":1659,"y":1198,"w":143,"h":123}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":123}, @@ -826,7 +834,7 @@ }, "sprites/buildings/logic_gate-xor.png": { - "frame": {"x":1657,"y":1198,"w":143,"h":143}, + "frame": {"x":1392,"y":1126,"w":143,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":143}, @@ -834,7 +842,7 @@ }, "sprites/buildings/logic_gate.png": { - "frame": {"x":793,"y":1118,"w":143,"h":132}, + "frame": {"x":1391,"y":1273,"w":143,"h":132}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":132}, @@ -842,7 +850,7 @@ }, "sprites/buildings/miner-chainable.png": { - "frame": {"x":505,"y":1887,"w":136,"h":142}, + "frame": {"x":3,"y":1703,"w":136,"h":142}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":136,"h":142}, @@ -850,7 +858,7 @@ }, "sprites/buildings/miner.png": { - "frame": {"x":405,"y":1736,"w":136,"h":142}, + "frame": {"x":143,"y":1703,"w":136,"h":142}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":136,"h":142}, @@ -898,7 +906,7 @@ }, "sprites/buildings/reader.png": { - "frame": {"x":937,"y":1315,"w":141,"h":144}, + "frame": {"x":790,"y":1260,"w":141,"h":144}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":141,"h":144}, @@ -906,7 +914,7 @@ }, "sprites/buildings/rotater-ccw.png": { - "frame": {"x":1378,"y":1274,"w":141,"h":143}, + "frame": {"x":1083,"y":1319,"w":141,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":141,"h":143}, @@ -914,7 +922,7 @@ }, "sprites/buildings/rotater-fl.png": { - "frame": {"x":1231,"y":1411,"w":141,"h":143}, + "frame": {"x":935,"y":1329,"w":141,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":141,"h":143}, @@ -922,7 +930,7 @@ }, "sprites/buildings/rotater.png": { - "frame": {"x":1657,"y":1345,"w":141,"h":143}, + "frame": {"x":1228,"y":1408,"w":141,"h":143}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":141,"h":143}, @@ -930,7 +938,7 @@ }, "sprites/buildings/splitter-compact-inverse.png": { - "frame": {"x":1082,"y":1461,"w":141,"h":136}, + "frame": {"x":1080,"y":1466,"w":141,"h":136}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":3,"w":141,"h":136}, @@ -938,7 +946,7 @@ }, "sprites/buildings/splitter-compact-merge-inverse.png": { - "frame": {"x":1232,"y":1271,"w":142,"h":136}, + "frame": {"x":559,"y":1178,"w":142,"h":136}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":3,"w":142,"h":136}, @@ -946,7 +954,7 @@ }, "sprites/buildings/splitter-compact-merge.png": { - "frame": {"x":1653,"y":1492,"w":139,"h":136}, + "frame": {"x":1658,"y":1473,"w":139,"h":136}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":3,"w":139,"h":136}, @@ -954,7 +962,7 @@ }, "sprites/buildings/splitter-compact.png": { - "frame": {"x":1370,"y":1563,"w":139,"h":136}, + "frame": {"x":1658,"y":1613,"w":139,"h":136}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":3,"w":139,"h":136}, @@ -994,7 +1002,7 @@ }, "sprites/buildings/underground_belt_entry-tier2.png": { - "frame": {"x":645,"y":1493,"w":137,"h":124}, + "frame": {"x":3,"y":1476,"w":137,"h":124}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":20,"w":137,"h":124}, @@ -1002,7 +1010,7 @@ }, "sprites/buildings/underground_belt_entry.png": { - "frame": {"x":928,"y":1463,"w":137,"h":111}, + "frame": {"x":283,"y":1414,"w":137,"h":111}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":33,"w":137,"h":111}, @@ -1010,7 +1018,7 @@ }, "sprites/buildings/underground_belt_exit-tier2.png": { - "frame": {"x":786,"y":1534,"w":137,"h":111}, + "frame": {"x":283,"y":1529,"w":137,"h":111}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":137,"h":111}, @@ -1018,7 +1026,7 @@ }, "sprites/buildings/underground_belt_exit.png": { - "frame": {"x":645,"y":1621,"w":137,"h":111}, + "frame": {"x":424,"y":1588,"w":137,"h":111}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":137,"h":111}, @@ -1042,12 +1050,20 @@ }, "sprites/buildings/virtual_processor-shapecompare.png": { - "frame": {"x":560,"y":1036,"w":143,"h":133}, + "frame": {"x":1244,"y":1271,"w":143,"h":133}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":143,"h":133}, "sourceSize": {"w":144,"h":144} }, +"sprites/buildings/virtual_processor-stacker.png": +{ + "frame": {"x":3,"y":1849,"w":130,"h":144}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":1,"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}, @@ -1106,7 +1122,7 @@ }, "sprites/buildings/wire_tunnel.png": { - "frame": {"x":787,"y":1396,"w":137,"h":134}, + "frame": {"x":559,"y":1318,"w":137,"h":134}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":5,"w":137,"h":134}, @@ -1138,7 +1154,7 @@ }, "sprites/misc/processor_disabled.png": { - "frame": {"x":3,"y":1958,"w":78,"h":81}, + "frame": {"x":3,"y":1606,"w":78,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":10,"y":10,"w":78,"h":81}, @@ -1186,7 +1202,7 @@ }, "sprites/misc/waypoint.png": { - "frame": {"x":292,"y":1834,"w":38,"h":48}, + "frame": {"x":48,"y":1997,"w":38,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":38,"h":48}, @@ -1250,7 +1266,7 @@ }, "sprites/wires/display/white.png": { - "frame": {"x":145,"y":1836,"w":47,"h":47}, + "frame": {"x":90,"y":1997,"w":47,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":47}, @@ -1258,7 +1274,7 @@ }, "sprites/wires/display/yellow.png": { - "frame": {"x":196,"y":1836,"w":47,"h":47}, + "frame": {"x":141,"y":1997,"w":47,"h":47}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":1,"w":47,"h":47}, @@ -1290,7 +1306,7 @@ }, "sprites/wires/network_conflict.png": { - "frame": {"x":85,"y":1952,"w":47,"h":44}, + "frame": {"x":192,"y":1997,"w":47,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":2,"w":47,"h":44}, @@ -1298,7 +1314,7 @@ }, "sprites/wires/network_empty.png": { - "frame": {"x":247,"y":1834,"w":41,"h":48}, + "frame": {"x":3,"y":1997,"w":41,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":0,"w":41,"h":48}, @@ -1338,7 +1354,7 @@ }, "sprites/wires/sets/color_turn.png": { - "frame": {"x":707,"y":1122,"w":81,"h":81}, + "frame": {"x":706,"y":1122,"w":81,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":63,"w":81,"h":81}, @@ -1370,7 +1386,7 @@ }, "sprites/wires/sets/conflict_turn.png": { - "frame": {"x":706,"y":1207,"w":81,"h":81}, + "frame": {"x":705,"y":1207,"w":81,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":63,"w":81,"h":81}, @@ -1426,7 +1442,7 @@ }, "sprites/wires/sets/shape_split.png": { - "frame": {"x":1095,"y":1086,"w":144,"h":81}, + "frame": {"x":1097,"y":1086,"w":144,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":63,"w":144,"h":81}, @@ -1434,7 +1450,7 @@ }, "sprites/wires/sets/shape_turn.png": { - "frame": {"x":706,"y":1292,"w":81,"h":81}, + "frame": {"x":705,"y":1292,"w":81,"h":81}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":63,"y":63,"w":81,"h":81}, @@ -1455,6 +1471,6 @@ "format": "RGBA8888", "size": {"w":2048,"h":2048}, "scale": "0.75", - "smartupdate": "$TexturePacker:SmartUpdate:26af492934beffa75e19b73e6d9eba56:d408ec7454e52a9384f80985bf14db9c:908b89f5ca8ff73e331a35a3b14d0604$" + "smartupdate": "$TexturePacker:SmartUpdate:d21082eda6f288e04b0739186004794d:0912211652d1c400e2846013f9de057b:908b89f5ca8ff73e331a35a3b14d0604$" } } diff --git a/res_built/atlas/atlas0_hq.png b/res_built/atlas/atlas0_hq.png index c7e5c979..f56d35d0 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 c4702db9..c5cdf2ad 100644 --- a/res_built/atlas/atlas0_lq.json +++ b/res_built/atlas/atlas0_lq.json @@ -10,7 +10,7 @@ }, "sprites/belt/built/forward_1.png": { - "frame": {"x":3,"y":939,"w":40,"h":48}, + "frame": {"x":51,"y":961,"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":179,"y":969,"w":40,"h":48}, + "frame": {"x":352,"y":869,"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":407,"y":864,"w":40,"h":48}, + "frame": {"x":300,"y":891,"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":352,"y":869,"w":40,"h":48}, + "frame": {"x":249,"y":893,"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":300,"y":883,"w":40,"h":48}, + "frame": {"x":455,"y":864,"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":249,"y":893,"w":40,"h":48}, + "frame": {"x":396,"y":912,"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":451,"y":864,"w":40,"h":48}, + "frame": {"x":344,"y":921,"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":223,"y":945,"w":40,"h":48}, + "frame": {"x":227,"y":973,"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":267,"y":945,"w":40,"h":48}, + "frame": {"x":271,"y":973,"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":47,"y":961,"w":40,"h":48}, + "frame": {"x":95,"y":961,"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":91,"y":961,"w":40,"h":48}, + "frame": {"x":139,"y":941,"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":135,"y":941,"w":40,"h":48}, + "frame": {"x":183,"y":917,"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":179,"y":917,"w":40,"h":48}, + "frame": {"x":183,"y":969,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -130,7 +130,7 @@ }, "sprites/belt/built/left_2.png": { - "frame": {"x":223,"y":611,"w":44,"h":44}, + "frame": {"x":323,"y":699,"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":659,"w":44,"h":44}, + "frame": {"x":275,"y":707,"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":691,"w":44,"h":44}, + "frame": {"x":323,"y":747,"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":275,"y":707,"w":44,"h":44}, + "frame": {"x":208,"y":759,"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":323,"y":739,"w":44,"h":44}, + "frame": {"x":156,"y":773,"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":208,"y":759,"w":44,"h":44}, + "frame": {"x":105,"y":797,"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":156,"y":773,"w":44,"h":44}, + "frame": {"x":54,"y":817,"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":105,"y":797,"w":44,"h":44}, + "frame": {"x":3,"y":843,"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":325,"y":547,"w":44,"h":44}, + "frame": {"x":465,"y":619,"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":325,"y":595,"w":44,"h":44}, + "frame": {"x":324,"y":651,"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":464,"y":619,"w":44,"h":44}, + "frame": {"x":223,"y":611,"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":324,"y":643,"w":44,"h":44}, + "frame": {"x":275,"y":659,"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":54,"y":817,"w":44,"h":44}, + "frame": {"x":311,"y":795,"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":3,"y":843,"w":44,"h":44}, + "frame": {"x":256,"y":797,"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":102,"y":845,"w":44,"h":44}, + "frame": {"x":3,"y":891,"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":51,"y":865,"w":44,"h":44}, + "frame": {"x":359,"y":821,"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":3,"y":891,"w":44,"h":44}, + "frame": {"x":304,"y":843,"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":359,"y":821,"w":44,"h":44}, + "frame": {"x":252,"y":845,"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":304,"y":835,"w":44,"h":44}, + "frame": {"x":201,"y":855,"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":252,"y":845,"w":44,"h":44}, + "frame": {"x":150,"y":869,"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":201,"y":855,"w":44,"h":44}, + "frame": {"x":99,"y":893,"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":150,"y":869,"w":44,"h":44}, + "frame": {"x":51,"y":913,"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":311,"y":787,"w":44,"h":44}, + "frame": {"x":204,"y":807,"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":256,"y":797,"w":44,"h":44}, + "frame": {"x":153,"y":821,"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":204,"y":807,"w":44,"h":44}, + "frame": {"x":102,"y":845,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -330,7 +330,7 @@ }, "sprites/belt/built/right_13.png": { - "frame": {"x":153,"y":821,"w":44,"h":44}, + "frame": {"x":51,"y":865,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -338,7 +338,7 @@ }, "sprites/blueprints/belt_left.png": { - "frame": {"x":99,"y":893,"w":44,"h":44}, + "frame": {"x":3,"y":939,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":4,"w":44,"h":44}, @@ -346,7 +346,7 @@ }, "sprites/blueprints/belt_right.png": { - "frame": {"x":51,"y":913,"w":44,"h":44}, + "frame": {"x":407,"y":864,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -354,7 +354,7 @@ }, "sprites/blueprints/belt_top.png": { - "frame": {"x":311,"y":935,"w":40,"h":48}, + "frame": {"x":315,"y":973,"w":40,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":0,"w":40,"h":48}, @@ -656,6 +656,14 @@ "spriteSourceSize": {"x":0,"y":0,"w":48,"h":45}, "sourceSize": {"w":48,"h":48} }, +"sprites/blueprints/virtual_processor-stacker.png": +{ + "frame": {"x":325,"y":547,"w":45,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":45,"h":48}, + "sourceSize": {"w":48,"h":48} +}, "sprites/blueprints/virtual_processor-unstacker.png": { "frame": {"x":107,"y":400,"w":48,"h":48}, @@ -706,7 +714,7 @@ }, "sprites/blueprints/wire_tunnel-coating.png": { - "frame": {"x":425,"y":655,"w":12,"h":46}, + "frame": {"x":426,"y":655,"w":12,"h":46}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":18,"y":1,"w":12,"h":46}, @@ -730,7 +738,7 @@ }, "sprites/buildings/belt_right.png": { - "frame": {"x":54,"y":817,"w":44,"h":44}, + "frame": {"x":311,"y":795,"w":44,"h":44}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":4,"y":4,"w":44,"h":44}, @@ -818,7 +826,7 @@ }, "sprites/buildings/logic_gate-transistor.png": { - "frame": {"x":425,"y":586,"w":35,"h":48}, + "frame": {"x":426,"y":586,"w":35,"h":48}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":35,"h":48}, @@ -914,7 +922,7 @@ }, "sprites/buildings/rotater-fl.png": { - "frame": {"x":273,"y":555,"w":48,"h":48}, + "frame": {"x":374,"y":567,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -922,7 +930,7 @@ }, "sprites/buildings/rotater.png": { - "frame": {"x":373,"y":567,"w":48,"h":48}, + "frame": {"x":273,"y":555,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -986,7 +994,7 @@ }, "sprites/buildings/trash.png": { - "frame": {"x":373,"y":619,"w":48,"h":48}, + "frame": {"x":374,"y":619,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1048,6 +1056,14 @@ "spriteSourceSize": {"x":0,"y":0,"w":48,"h":45}, "sourceSize": {"w":48,"h":48} }, +"sprites/buildings/virtual_processor-stacker.png": +{ + "frame": {"x":325,"y":599,"w":45,"h":48}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":45,"h":48}, + "sourceSize": {"w":48,"h":48} +}, "sprites/buildings/virtual_processor-unstacker.png": { "frame": {"x":272,"y":607,"w":48,"h":48}, @@ -1066,7 +1082,7 @@ }, "sprites/buildings/wire-cross.png": { - "frame": {"x":457,"y":667,"w":48,"h":48}, + "frame": {"x":458,"y":667,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1098,7 +1114,7 @@ }, "sprites/buildings/wire_tunnel-coating.png": { - "frame": {"x":441,"y":655,"w":12,"h":46}, + "frame": {"x":442,"y":655,"w":12,"h":46}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":18,"y":1,"w":12,"h":46}, @@ -1162,7 +1178,7 @@ }, "sprites/misc/slot_bad_arrow.png": { - "frame": {"x":425,"y":638,"w":13,"h":13}, + "frame": {"x":426,"y":638,"w":13,"h":13}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, @@ -1170,7 +1186,7 @@ }, "sprites/misc/slot_good_arrow.png": { - "frame": {"x":442,"y":638,"w":13,"h":13}, + "frame": {"x":443,"y":638,"w":13,"h":13}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":13,"h":13}, @@ -1378,7 +1394,7 @@ }, "sprites/wires/sets/regular_cross.png": { - "frame": {"x":457,"y":667,"w":48,"h":48}, + "frame": {"x":458,"y":667,"w":48,"h":48}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":48,"h":48}, @@ -1455,6 +1471,6 @@ "format": "RGBA8888", "size": {"w":512,"h":1024}, "scale": "0.25", - "smartupdate": "$TexturePacker:SmartUpdate:26af492934beffa75e19b73e6d9eba56:d408ec7454e52a9384f80985bf14db9c:908b89f5ca8ff73e331a35a3b14d0604$" + "smartupdate": "$TexturePacker:SmartUpdate:d21082eda6f288e04b0739186004794d:0912211652d1c400e2846013f9de057b:908b89f5ca8ff73e331a35a3b14d0604$" } } diff --git a/res_built/atlas/atlas0_lq.png b/res_built/atlas/atlas0_lq.png index 7a18ee17..1c60156c 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 72e36985..c5d7d311 100644 --- a/res_built/atlas/atlas0_mq.json +++ b/res_built/atlas/atlas0_mq.json @@ -10,7 +10,7 @@ }, "sprites/belt/built/forward_1.png": { - "frame": {"x":891,"y":1749,"w":78,"h":96}, + "frame": {"x":94,"y":1746,"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":331,"y":1550,"w":78,"h":96}, + "frame": {"x":754,"y":1555,"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":413,"y":1639,"w":78,"h":96}, + "frame": {"x":653,"y":1564,"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":495,"y":1695,"w":78,"h":96}, + "frame": {"x":556,"y":1601,"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":577,"y":1756,"w":78,"h":96}, + "frame": {"x":458,"y":1663,"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":659,"y":1834,"w":78,"h":96}, + "frame": {"x":359,"y":1711,"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":3,"y":1934,"w":78,"h":96}, + "frame": {"x":268,"y":1771,"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":85,"y":1538,"w":78,"h":96}, + "frame": {"x":176,"y":1835,"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":3,"y":1538,"w":78,"h":96}, + "frame": {"x":85,"y":1846,"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":1438,"w":78,"h":96}, + "frame": {"x":3,"y":1789,"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":85,"y":1438,"w":78,"h":96}, + "frame": {"x":3,"y":1889,"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":167,"y":1502,"w":78,"h":96}, + "frame": {"x":856,"y":1469,"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":249,"y":1530,"w":78,"h":96}, + "frame": {"x":938,"y":1469,"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":931,"y":903,"w":87,"h":87}, + "frame": {"x":403,"y":911,"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":911,"w":87,"h":87}, + "frame": {"x":403,"y":1002,"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":663,"y":1383,"w":87,"h":87}, + "frame": {"x":3,"y":1516,"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":572,"y":1418,"w":87,"h":87}, + "frame": {"x":785,"y":1191,"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":754,"y":1461,"w":87,"h":87}, + "frame": {"x":876,"y":1196,"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":663,"y":1474,"w":87,"h":87}, + "frame": {"x":785,"y":1282,"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":845,"y":1464,"w":87,"h":87}, + "frame": {"x":685,"y":1287,"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":754,"y":1552,"w":87,"h":87}, + "frame": {"x":583,"y":1325,"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":845,"y":1555,"w":87,"h":87}, + "frame": {"x":482,"y":1381,"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":3,"y":1256,"w":87,"h":87}, + "frame": {"x":383,"y":1429,"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":401,"y":1002,"w":87,"h":87}, + "frame": {"x":391,"y":1338,"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":481,"y":1275,"w":87,"h":87}, + "frame": {"x":292,"y":1398,"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":572,"y":1327,"w":87,"h":87}, + "frame": {"x":196,"y":1462,"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":481,"y":1366,"w":87,"h":87}, + "frame": {"x":99,"y":1473,"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":94,"y":1256,"w":87,"h":87}, + "frame": {"x":287,"y":1489,"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":185,"y":1320,"w":87,"h":87}, + "frame": {"x":190,"y":1553,"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":640,"y":1565,"w":87,"h":87}, + "frame": {"x":674,"y":1378,"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":731,"y":1643,"w":87,"h":87}, + "frame": {"x":573,"y":1416,"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":822,"y":1646,"w":87,"h":87}, + "frame": {"x":474,"y":1472,"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":913,"y":1658,"w":87,"h":87}, + "frame": {"x":378,"y":1520,"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":3,"y":1347,"w":87,"h":87}, + "frame": {"x":281,"y":1580,"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":94,"y":1347,"w":87,"h":87}, + "frame": {"x":185,"y":1644,"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":185,"y":1411,"w":87,"h":87}, + "frame": {"x":94,"y":1655,"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":276,"y":1439,"w":87,"h":87}, + "frame": {"x":3,"y":1698,"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":276,"y":1348,"w":87,"h":87}, + "frame": {"x":94,"y":1564,"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":367,"y":1368,"w":87,"h":87}, + "frame": {"x":3,"y":1607,"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":458,"y":1457,"w":87,"h":87}, + "frame": {"x":876,"y":1287,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -330,7 +330,7 @@ }, "sprites/belt/built/right_13.png": { - "frame": {"x":549,"y":1509,"w":87,"h":87}, + "frame": {"x":776,"y":1373,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -338,7 +338,7 @@ }, "sprites/blueprints/belt_left.png": { - "frame": {"x":367,"y":1459,"w":87,"h":87}, + "frame": {"x":867,"y":1378,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -346,7 +346,7 @@ }, "sprites/blueprints/belt_right.png": { - "frame": {"x":458,"y":1548,"w":87,"h":87}, + "frame": {"x":765,"y":1464,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -354,7 +354,7 @@ }, "sprites/blueprints/belt_top.png": { - "frame": {"x":167,"y":1602,"w":78,"h":96}, + "frame": {"x":85,"y":1946,"w":78,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":78,"h":96}, @@ -386,7 +386,7 @@ }, "sprites/blueprints/display.png": { - "frame": {"x":549,"y":1600,"w":86,"h":91}, + "frame": {"x":664,"y":1469,"w":86,"h":91}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":5,"y":5,"w":86,"h":91}, @@ -402,7 +402,7 @@ }, "sprites/blueprints/lever.png": { - "frame": {"x":85,"y":1638,"w":75,"h":86}, + "frame": {"x":167,"y":1935,"w":75,"h":86}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":11,"y":3,"w":75,"h":86}, @@ -410,7 +410,7 @@ }, "sprites/blueprints/logic_gate-not.png": { - "frame": {"x":936,"y":1558,"w":83,"h":96}, + "frame": {"x":469,"y":1563,"w":83,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":7,"y":0,"w":83,"h":96}, @@ -418,7 +418,7 @@ }, "sprites/blueprints/logic_gate-or.png": { - "frame": {"x":203,"y":861,"w":96,"h":82}, + "frame": {"x":303,"y":903,"w":96,"h":82}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":82}, @@ -442,7 +442,7 @@ }, "sprites/blueprints/logic_gate.png": { - "frame": {"x":910,"y":1094,"w":96,"h":89}, + "frame": {"x":910,"y":1103,"w":96,"h":89}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":89}, @@ -450,7 +450,7 @@ }, "sprites/blueprints/miner-chainable.png": { - "frame": {"x":585,"y":1227,"w":92,"h":96}, + "frame": {"x":100,"y":1373,"w":92,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":92,"h":96}, @@ -458,7 +458,7 @@ }, "sprites/blueprints/miner.png": { - "frame": {"x":681,"y":1283,"w":92,"h":96}, + "frame": {"x":3,"y":1416,"w":92,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":92,"h":96}, @@ -506,7 +506,7 @@ }, "sprites/blueprints/reader.png": { - "frame": {"x":203,"y":947,"w":95,"h":96}, + "frame": {"x":3,"y":926,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, @@ -522,7 +522,7 @@ }, "sprites/blueprints/rotater-fl.png": { - "frame": {"x":302,"y":961,"w":95,"h":96}, + "frame": {"x":102,"y":983,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":95,"h":96}, @@ -538,7 +538,7 @@ }, "sprites/blueprints/splitter-compact-inverse.png": { - "frame": {"x":399,"y":1093,"w":95,"h":93}, + "frame": {"x":300,"y":1047,"w":95,"h":93}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":1,"w":95,"h":93}, @@ -546,7 +546,7 @@ }, "sprites/blueprints/splitter-compact-merge-inverse.png": { - "frame": {"x":498,"y":1099,"w":95,"h":93}, + "frame": {"x":201,"y":1111,"w":95,"h":93}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":1,"w":95,"h":93}, @@ -554,7 +554,7 @@ }, "sprites/blueprints/splitter-compact-merge.png": { - "frame": {"x":794,"y":1270,"w":93,"h":93}, + "frame": {"x":300,"y":1144,"w":93,"h":93}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":1,"w":93,"h":93}, @@ -562,7 +562,7 @@ }, "sprites/blueprints/splitter-compact.png": { - "frame": {"x":891,"y":1280,"w":93,"h":93}, + "frame": {"x":201,"y":1208,"w":93,"h":93}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":1,"w":93,"h":93}, @@ -602,7 +602,7 @@ }, "sprites/blueprints/underground_belt_entry-tier2.png": { - "frame": {"x":294,"y":1161,"w":93,"h":84}, + "frame": {"x":397,"y":1172,"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":391,"y":1190,"w":93,"h":75}, + "frame": {"x":298,"y":1241,"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":794,"y":1191,"w":94,"h":75}, + "frame": {"x":399,"y":1093,"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":488,"y":1196,"w":93,"h":75}, + "frame": {"x":198,"y":1305,"w":93,"h":75}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":0,"w":93,"h":75}, @@ -642,7 +642,7 @@ }, "sprites/blueprints/virtual_processor-rotater.png": { - "frame": {"x":725,"y":1734,"w":79,"h":96}, + "frame": {"x":276,"y":1671,"w":79,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":79,"h":96}, @@ -650,12 +650,20 @@ }, "sprites/blueprints/virtual_processor-shapecompare.png": { - "frame": {"x":910,"y":1187,"w":96,"h":89}, + "frame": {"x":3,"y":774,"w":96,"h":89}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":89}, "sourceSize": {"w":96,"h":96} }, +"sprites/blueprints/virtual_processor-stacker.png": +{ + "frame": {"x":931,"y":903,"w":88,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":88,"h":96}, + "sourceSize": {"w":96,"h":96} +}, "sprites/blueprints/virtual_processor-unstacker.png": { "frame": {"x":349,"y":803,"w":96,"h":96}, @@ -682,7 +690,7 @@ }, "sprites/blueprints/wire-split.png": { - "frame": {"x":103,"y":866,"w":96,"h":55}, + "frame": {"x":3,"y":867,"w":96,"h":55}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":41,"w":96,"h":55}, @@ -714,7 +722,7 @@ }, "sprites/blueprints/wire_tunnel.png": { - "frame": {"x":3,"y":1083,"w":93,"h":91}, + "frame": {"x":101,"y":1278,"w":93,"h":91}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":2,"y":2,"w":93,"h":91}, @@ -722,7 +730,7 @@ }, "sprites/buildings/belt_left.png": { - "frame": {"x":931,"y":903,"w":87,"h":87}, + "frame": {"x":403,"y":911,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":9,"w":87,"h":87}, @@ -730,7 +738,7 @@ }, "sprites/buildings/belt_right.png": { - "frame": {"x":94,"y":1256,"w":87,"h":87}, + "frame": {"x":287,"y":1489,"w":87,"h":87}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":9,"w":87,"h":87}, @@ -770,7 +778,7 @@ }, "sprites/buildings/display.png": { - "frame": {"x":936,"y":1464,"w":84,"h":90}, + "frame": {"x":565,"y":1507,"w":84,"h":90}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":6,"y":6,"w":84,"h":90}, @@ -802,7 +810,7 @@ }, "sprites/buildings/logic_gate-not.png": { - "frame": {"x":639,"y":1656,"w":82,"h":96}, + "frame": {"x":372,"y":1611,"w":82,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":8,"y":0,"w":82,"h":96}, @@ -810,7 +818,7 @@ }, "sprites/buildings/logic_gate-or.png": { - "frame": {"x":203,"y":774,"w":96,"h":83}, + "frame": {"x":203,"y":866,"w":96,"h":83}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":83}, @@ -834,7 +842,7 @@ }, "sprites/buildings/logic_gate.png": { - "frame": {"x":103,"y":774,"w":96,"h":88}, + "frame": {"x":203,"y":774,"w":96,"h":88}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":88}, @@ -842,7 +850,7 @@ }, "sprites/buildings/miner-chainable.png": { - "frame": {"x":291,"y":1249,"w":91,"h":95}, + "frame": {"x":690,"y":1188,"w":91,"h":95}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":91,"h":95}, @@ -850,7 +858,7 @@ }, "sprites/buildings/miner.png": { - "frame": {"x":386,"y":1269,"w":91,"h":95}, + "frame": {"x":590,"y":1226,"w":91,"h":95}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":91,"h":95}, @@ -898,7 +906,7 @@ }, "sprites/buildings/reader.png": { - "frame": {"x":3,"y":983,"w":95,"h":96}, + "frame": {"x":3,"y":1026,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, @@ -906,7 +914,7 @@ }, "sprites/buildings/rotater-ccw.png": { - "frame": {"x":102,"y":983,"w":95,"h":96}, + "frame": {"x":201,"y":1011,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, @@ -914,7 +922,7 @@ }, "sprites/buildings/rotater-fl.png": { - "frame": {"x":201,"y":1047,"w":95,"h":96}, + "frame": {"x":102,"y":1083,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":95,"h":96}, @@ -922,7 +930,7 @@ }, "sprites/buildings/rotater.png": { - "frame": {"x":300,"y":1061,"w":95,"h":96}, + "frame": {"x":3,"y":1126,"w":95,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":1,"y":0,"w":95,"h":96}, @@ -930,7 +938,7 @@ }, "sprites/buildings/splitter-compact-inverse.png": { - "frame": {"x":696,"y":1188,"w":94,"h":91}, + "frame": {"x":3,"y":1226,"w":94,"h":91}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":2,"w":94,"h":91}, @@ -938,7 +946,7 @@ }, "sprites/buildings/splitter-compact-merge-inverse.png": { - "frame": {"x":597,"y":1132,"w":95,"h":91}, + "frame": {"x":102,"y":1183,"w":95,"h":91}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":2,"w":95,"h":91}, @@ -946,7 +954,7 @@ }, "sprites/buildings/splitter-compact-merge.png": { - "frame": {"x":100,"y":1083,"w":93,"h":91}, + "frame": {"x":3,"y":1321,"w":93,"h":91}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":2,"w":93,"h":91}, @@ -954,7 +962,7 @@ }, "sprites/buildings/splitter-compact.png": { - "frame": {"x":197,"y":1147,"w":93,"h":91}, + "frame": {"x":497,"y":1099,"w":93,"h":91}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":2,"w":93,"h":91}, @@ -994,7 +1002,7 @@ }, "sprites/buildings/underground_belt_entry-tier2.png": { - "frame": {"x":873,"y":1377,"w":92,"h":83}, + "frame": {"x":494,"y":1194,"w":92,"h":83}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":13,"w":92,"h":83}, @@ -1002,7 +1010,7 @@ }, "sprites/buildings/underground_belt_entry.png": { - "frame": {"x":3,"y":1178,"w":92,"h":74}, + "frame": {"x":395,"y":1260,"w":92,"h":74}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":22,"w":92,"h":74}, @@ -1010,7 +1018,7 @@ }, "sprites/buildings/underground_belt_exit-tier2.png": { - "frame": {"x":99,"y":1178,"w":92,"h":74}, + "frame": {"x":295,"y":1320,"w":92,"h":74}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":92,"h":74}, @@ -1018,7 +1026,7 @@ }, "sprites/buildings/underground_belt_exit.png": { - "frame": {"x":195,"y":1242,"w":92,"h":74}, + "frame": {"x":196,"y":1384,"w":92,"h":74}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":0,"w":92,"h":74}, @@ -1034,7 +1042,7 @@ }, "sprites/buildings/virtual_processor-rotater.png": { - "frame": {"x":808,"y":1737,"w":79,"h":96}, + "frame": {"x":185,"y":1735,"w":79,"h":96}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":9,"y":0,"w":79,"h":96}, @@ -1042,12 +1050,20 @@ }, "sprites/buildings/virtual_processor-shapecompare.png": { - "frame": {"x":3,"y":774,"w":96,"h":89}, + "frame": {"x":103,"y":774,"w":96,"h":89}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":89}, "sourceSize": {"w":96,"h":96} }, +"sprites/buildings/virtual_processor-stacker.png": +{ + "frame": {"x":491,"y":1281,"w":88,"h":96}, + "rotated": false, + "trimmed": true, + "spriteSourceSize": {"x":0,"y":0,"w":88,"h":96}, + "sourceSize": {"w":96,"h":96} +}, "sprites/buildings/virtual_processor-unstacker.png": { "frame": {"x":510,"y":999,"w":96,"h":96}, @@ -1074,7 +1090,7 @@ }, "sprites/buildings/wire-split.png": { - "frame": {"x":3,"y":867,"w":96,"h":54}, + "frame": {"x":103,"y":867,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1106,7 +1122,7 @@ }, "sprites/buildings/wire_tunnel.png": { - "frame": {"x":777,"y":1367,"w":92,"h":90}, + "frame": {"x":594,"y":1132,"w":92,"h":90}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":3,"y":3,"w":92,"h":90}, @@ -1330,7 +1346,7 @@ }, "sprites/wires/sets/color_split.png": { - "frame": {"x":303,"y":903,"w":96,"h":54}, + "frame": {"x":103,"y":925,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1362,7 +1378,7 @@ }, "sprites/wires/sets/conflict_split.png": { - "frame": {"x":3,"y":925,"w":96,"h":54}, + "frame": {"x":203,"y":953,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1394,7 +1410,7 @@ }, "sprites/wires/sets/regular_split.png": { - "frame": {"x":3,"y":867,"w":96,"h":54}, + "frame": {"x":103,"y":867,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1410,7 +1426,7 @@ }, "sprites/wires/sets/shape_cross.png": { - "frame": {"x":922,"y":994,"w":96,"h":96}, + "frame": {"x":922,"y":1003,"w":96,"h":96}, "rotated": false, "trimmed": false, "spriteSourceSize": {"x":0,"y":0,"w":96,"h":96}, @@ -1426,7 +1442,7 @@ }, "sprites/wires/sets/shape_split.png": { - "frame": {"x":103,"y":925,"w":96,"h":54}, + "frame": {"x":303,"y":989,"w":96,"h":54}, "rotated": false, "trimmed": true, "spriteSourceSize": {"x":0,"y":42,"w":96,"h":54}, @@ -1455,6 +1471,6 @@ "format": "RGBA8888", "size": {"w":1024,"h":2048}, "scale": "0.5", - "smartupdate": "$TexturePacker:SmartUpdate:26af492934beffa75e19b73e6d9eba56:d408ec7454e52a9384f80985bf14db9c:908b89f5ca8ff73e331a35a3b14d0604$" + "smartupdate": "$TexturePacker:SmartUpdate:d21082eda6f288e04b0739186004794d:0912211652d1c400e2846013f9de057b:908b89f5ca8ff73e331a35a3b14d0604$" } } diff --git a/res_built/atlas/atlas0_mq.png b/res_built/atlas/atlas0_mq.png index 909d3b8b..9e9b97c6 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 730b130c..947bdb04 100644 --- a/res_raw/atlas.tps +++ b/res_raw/atlas.tps @@ -267,6 +267,7 @@ sprites/blueprints/logic_gate.png sprites/blueprints/miner-chainable.png sprites/blueprints/miner.png + sprites/blueprints/reader.png sprites/blueprints/rotater-ccw.png sprites/blueprints/rotater-fl.png sprites/blueprints/rotater.png @@ -295,6 +296,7 @@ sprites/buildings/logic_gate-xor.png sprites/buildings/logic_gate.png sprites/buildings/miner-chainable.png + sprites/buildings/reader.png sprites/buildings/rotater-ccw.png sprites/buildings/rotater-fl.png sprites/buildings/splitter-compact-inverse.png @@ -312,6 +314,7 @@ sprites/buildings/virtual_processor.png sprites/buildings/wire_tunnel-coating.png sprites/buildings/wire_tunnel.png + sprites/misc/reader_overlay.png sprites/wires/lever_on.png sprites/wires/sets/color_cross.png sprites/wires/sets/color_forward.png @@ -466,6 +469,8 @@ sprites/buildings/miner.png sprites/buildings/rotater.png sprites/buildings/trash.png + sprites/misc/processor_disabled.png + sprites/misc/processor_disconnected.png sprites/wires/logical_acceptor.png sprites/wires/logical_ejector.png sprites/wires/overlay_tile.png diff --git a/res_raw/sprites/blueprints/virtual_processor-stacker.png b/res_raw/sprites/blueprints/virtual_processor-stacker.png new file mode 100644 index 00000000..1d691491 Binary files /dev/null and b/res_raw/sprites/blueprints/virtual_processor-stacker.png differ diff --git a/res_raw/sprites/buildings/virtual_processor-stacker.png b/res_raw/sprites/buildings/virtual_processor-stacker.png new file mode 100644 index 00000000..15882e63 Binary files /dev/null and b/res_raw/sprites/buildings/virtual_processor-stacker.png differ diff --git a/src/js/core/config.local.js b/src/js/core/config.local.js index 92d0c577..b75c5650 100644 --- a/src/js/core/config.local.js +++ b/src/js/core/config.local.js @@ -1,111 +1,114 @@ -export default { - // You can set any debug options here! - /* dev:start */ - // ----------------------------------------------------------------------------------- - // Quickly enters the game and skips the main menu - good for fast iterating - // fastGameEnter: true, - // ----------------------------------------------------------------------------------- - // Skips any delays like transitions between states and such - // noArtificialDelays: true, - // ----------------------------------------------------------------------------------- - // Disables writing of savegames, useful for testing the same savegame over and over - // disableSavegameWrite: true, - // ----------------------------------------------------------------------------------- - // Shows bounds of all entities - // showEntityBounds: true, - // ----------------------------------------------------------------------------------- - // Shows arrows for every ejector / acceptor - // showAcceptorEjectors: true, - // ----------------------------------------------------------------------------------- - // Disables the music (Overrides any setting, can cause weird behaviour) - // disableMusic: true, - // ----------------------------------------------------------------------------------- - // Do not render static map entities (=most buildings) - // doNotRenderStatics: true, - // ----------------------------------------------------------------------------------- - // Allow to zoom freely without limits - // disableZoomLimits: true, - // ----------------------------------------------------------------------------------- - // Shows a border arround every chunk - // showChunkBorders: true, - // ----------------------------------------------------------------------------------- - // All rewards can be unlocked by passing just 1 of any shape - // rewardsInstant: true, - // ----------------------------------------------------------------------------------- - // Unlocks all buildings - // allBuildingsUnlocked: true, - // ----------------------------------------------------------------------------------- - // Disables cost of blueprints - // blueprintsNoCost: true, - // ----------------------------------------------------------------------------------- - // Disables cost of upgrades - // upgradesNoCost: true, - // ----------------------------------------------------------------------------------- - // Disables the dialog when completing a level - // disableUnlockDialog: true, - // ----------------------------------------------------------------------------------- - // Disables the simulation - This effectively pauses the game. - // disableLogicTicks: true, - // ----------------------------------------------------------------------------------- - // Test the rendering if everything is clipped out properly - // testClipping: true, - // ----------------------------------------------------------------------------------- - // Allows to render slower, useful for recording at half speed to avoid stuttering - // framePausesBetweenTicks: 1, - // ----------------------------------------------------------------------------------- - // Replace all translations with emojis to see which texts are translateable - // testTranslations: true, - // ----------------------------------------------------------------------------------- - // Enables an inspector which shows information about the entity below the curosr - // enableEntityInspector: true, - // ----------------------------------------------------------------------------------- - // Enables ads in the local build (normally they are deactivated there) - // testAds: true, - // ----------------------------------------------------------------------------------- - // Disables the automatic switch to an overview when zooming out - // disableMapOverview: true, - // ----------------------------------------------------------------------------------- - // Disables the notification when there are new entries in the changelog since last played - // disableUpgradeNotification: true, - // ----------------------------------------------------------------------------------- - // Makes belts almost infinitely fast - // instantBelts: true, - // ----------------------------------------------------------------------------------- - // Makes item processors almost infinitely fast - // instantProcessors: true, - // ----------------------------------------------------------------------------------- - // Makes miners almost infinitely fast - // instantMiners: true, - // ----------------------------------------------------------------------------------- - // When using fastGameEnter, controls whether a new game is started or the last one is resumed - // resumeGameOnFastEnter: true, - // ----------------------------------------------------------------------------------- - // Special option used to render the trailer - // renderForTrailer: true, - // ----------------------------------------------------------------------------------- - // Whether to render changes - // renderChanges: true, - // ----------------------------------------------------------------------------------- - // Whether to render belt paths - // renderBeltPaths: true, - // ----------------------------------------------------------------------------------- - // Whether to check belt paths - // checkBeltPaths: true, - // ----------------------------------------------------------------------------------- - // Whether to items / s instead of items / m in stats - // detailedStatistics: true, - // ----------------------------------------------------------------------------------- - // Shows detailed information about which atlas is used - // showAtlasInfo: true, - // ----------------------------------------------------------------------------------- - // Renders the rotation of all wires - // renderWireRotations: true, - // ----------------------------------------------------------------------------------- - // Renders information about wire networks - // renderWireNetworkInfos: true, - // ----------------------------------------------------------------------------------- +export default { + // You can set any debug options here! + /* dev:start */ + // ----------------------------------------------------------------------------------- + // Quickly enters the game and skips the main menu - good for fast iterating + // fastGameEnter: true, + // ----------------------------------------------------------------------------------- + // Skips any delays like transitions between states and such + // noArtificialDelays: true, + // ----------------------------------------------------------------------------------- + // Disables writing of savegames, useful for testing the same savegame over and over + // disableSavegameWrite: true, + // ----------------------------------------------------------------------------------- + // Shows bounds of all entities + // showEntityBounds: true, + // ----------------------------------------------------------------------------------- + // Shows arrows for every ejector / acceptor + // showAcceptorEjectors: true, + // ----------------------------------------------------------------------------------- + // Disables the music (Overrides any setting, can cause weird behaviour) + // disableMusic: true, + // ----------------------------------------------------------------------------------- + // Do not render static map entities (=most buildings) + // doNotRenderStatics: true, + // ----------------------------------------------------------------------------------- + // Allow to zoom freely without limits + // disableZoomLimits: true, + // ----------------------------------------------------------------------------------- + // Shows a border arround every chunk + // showChunkBorders: true, + // ----------------------------------------------------------------------------------- + // All rewards can be unlocked by passing just 1 of any shape + // rewardsInstant: true, + // ----------------------------------------------------------------------------------- + // Unlocks all buildings + // allBuildingsUnlocked: true, + // ----------------------------------------------------------------------------------- + // Disables cost of blueprints + // blueprintsNoCost: true, + // ----------------------------------------------------------------------------------- + // Disables cost of upgrades + // upgradesNoCost: true, + // ----------------------------------------------------------------------------------- + // Disables the dialog when completing a level + // disableUnlockDialog: true, + // ----------------------------------------------------------------------------------- + // Disables the simulation - This effectively pauses the game. + // disableLogicTicks: true, + // ----------------------------------------------------------------------------------- + // Test the rendering if everything is clipped out properly + // testClipping: true, + // ----------------------------------------------------------------------------------- + // Allows to render slower, useful for recording at half speed to avoid stuttering + // framePausesBetweenTicks: 250, + // ----------------------------------------------------------------------------------- + // Replace all translations with emojis to see which texts are translateable + // testTranslations: true, + // ----------------------------------------------------------------------------------- + // Enables an inspector which shows information about the entity below the curosr + // enableEntityInspector: true, + // ----------------------------------------------------------------------------------- + // Enables ads in the local build (normally they are deactivated there) + // testAds: true, + // ----------------------------------------------------------------------------------- + // Disables the automatic switch to an overview when zooming out + // disableMapOverview: true, + // ----------------------------------------------------------------------------------- + // Disables the notification when there are new entries in the changelog since last played + // disableUpgradeNotification: true, + // ----------------------------------------------------------------------------------- + // Makes belts almost infinitely fast + // instantBelts: true, + // ----------------------------------------------------------------------------------- + // Makes item processors almost infinitely fast + // instantProcessors: true, + // ----------------------------------------------------------------------------------- + // Makes miners almost infinitely fast + // instantMiners: true, + // ----------------------------------------------------------------------------------- + // When using fastGameEnter, controls whether a new game is started or the last one is resumed + // resumeGameOnFastEnter: true, + // ----------------------------------------------------------------------------------- + // Special option used to render the trailer + // renderForTrailer: true, + // ----------------------------------------------------------------------------------- + // Whether to render changes + // renderChanges: true, + // ----------------------------------------------------------------------------------- + // Whether to render belt paths + // renderBeltPaths: true, + // ----------------------------------------------------------------------------------- + // Whether to check belt paths + // checkBeltPaths: true, + // ----------------------------------------------------------------------------------- + // Whether to items / s instead of items / m in stats + // detailedStatistics: true, + // ----------------------------------------------------------------------------------- + // Shows detailed information about which atlas is used + // showAtlasInfo: true, + // ----------------------------------------------------------------------------------- + // Renders the rotation of all wires + // renderWireRotations: true, + // ----------------------------------------------------------------------------------- + // Renders information about wire networks + // renderWireNetworkInfos: true, + // ----------------------------------------------------------------------------------- // Disables ejector animations and processing // disableEjectorProcessing: true, // ----------------------------------------------------------------------------------- - /* dev:end */ -}; + // Allows manual ticking + // manualTickOnly: true, + // ----------------------------------------------------------------------------------- + /* dev:end */ +}; diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 0e4b7b79..b999e0ee 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -1,1218 +1,1241 @@ -import { globalConfig } from "../core/config"; -import { DrawParameters } from "../core/draw_parameters"; -import { createLogger } from "../core/logging"; -import { Rectangle } from "../core/rectangle"; -import { epsilonCompare, round4Digits } from "../core/utils"; -import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector"; -import { BasicSerializableObject, types } from "../savegame/serialization"; -import { BaseItem } from "./base_item"; -import { Entity } from "./entity"; -import { typeItemSingleton } from "./item_resolver"; -import { GameRoot } from "./root"; - -const logger = createLogger("belt_path"); - -// Helpers for more semantic access into interleaved arrays -const _nextDistance = 0; -const _item = 1; - -const DEBUG = G_IS_DEV && false; - -/** - * Stores a path of belts, used for optimizing performance - */ -export class BeltPath extends BasicSerializableObject { - static getId() { - return "BeltPath"; - } - - static getSchema() { - return { - entityPath: types.array(types.entity), - items: types.array(types.pair(types.ufloat, typeItemSingleton)), - spacingToFirstItem: types.ufloat, - }; - } - - /** - * Creates a path from a serialized object - * @param {GameRoot} root - * @param {Object} data - * @returns {BeltPath|string} - */ - static fromSerialized(root, data) { - // Create fake object which looks like a belt path but skips the constructor - const fakeObject = /** @type {BeltPath} */ (Object.create(BeltPath.prototype)); - fakeObject.root = root; - - // Deserialize the data - const errorCodeDeserialize = fakeObject.deserialize(data); - if (errorCodeDeserialize) { - return errorCodeDeserialize; - } - - // Compute other properties - fakeObject.init(false); - - return fakeObject; - } - - /** - * @param {GameRoot} root - * @param {Array} entityPath - */ - constructor(root, entityPath) { - super(); - this.root = root; - - assert(entityPath.length > 0, "invalid entity path"); - this.entityPath = entityPath; - - /** - * Stores the items sorted, and their distance to the previous item (or start) - * Layout: [distanceToNext, item] - * @type {Array<[number, BaseItem]>} - */ - this.items = []; - - /** - * Stores the spacing to the first item - */ - - this.init(); - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("constructor"); - } - } - /** - * Initializes the path by computing the properties which are not saved - * @param {boolean} computeSpacing Whether to also compute the spacing - */ - init(computeSpacing = true) { - this.onPathChanged(); - - this.totalLength = this.computeTotalLength(); - - if (computeSpacing) { - this.spacingToFirstItem = this.totalLength; - } - - /** - * Current bounds of this path - * @type {Rectangle} - */ - this.worldBounds = this.computeBounds(); - - // Connect the belts - for (let i = 0; i < this.entityPath.length; ++i) { - this.entityPath[i].components.Belt.assignedPath = this; - } - } - - /** - * Returns whether this path can accept a new item - * @returns {boolean} - */ - canAcceptItem() { - return this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts; - } - - /** - * Tries to accept the item - * @param {BaseItem} item - */ - tryAcceptItem(item) { - if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { - // So, since we already need one tick to accept this item we will add this directly. - const beltProgressPerTick = - this.root.hubGoals.getBeltBaseSpeed() * - this.root.dynamicTickrate.deltaSeconds * - globalConfig.itemSpacingOnBelts; - - // First, compute how much progress we can make *at max* - const maxProgress = Math.max(0, this.spacingToFirstItem - globalConfig.itemSpacingOnBelts); - const initialProgress = Math.min(maxProgress, beltProgressPerTick); - - this.items.unshift([this.spacingToFirstItem - initialProgress, item]); - this.spacingToFirstItem = initialProgress; - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("accept-item"); - } - - return true; - } - return false; - } - - /** - * SLOW / Tries to find the item closest to the given tile - * @param {Vector} tile - * @returns {BaseItem|null} - */ - findItemAtTile(tile) { - // @TODO: This breaks color blind mode otherwise - return null; - } - - /** - * Computes the tile bounds of the path - * @returns {Rectangle} - */ - computeBounds() { - let bounds = this.entityPath[0].components.StaticMapEntity.getTileSpaceBounds(); - for (let i = 1; i < this.entityPath.length; ++i) { - const staticComp = this.entityPath[i].components.StaticMapEntity; - const otherBounds = staticComp.getTileSpaceBounds(); - bounds = bounds.getUnion(otherBounds); - } - return bounds.allScaled(globalConfig.tileSize); - } - - /** - * Recomputes cache variables once the path was changed - */ - onPathChanged() { - this.acceptorTarget = this.computeAcceptingEntityAndSlot(); - } - - /** - * Called by the belt system when the surroundings changed - */ - onSurroundingsChanged() { - this.onPathChanged(); - } - - /** - * Finds the entity which accepts our items - * @return {{ entity: Entity, slot: number, direction?: enumDirection }} - */ - computeAcceptingEntityAndSlot() { - const lastEntity = this.entityPath[this.entityPath.length - 1]; - const lastStatic = lastEntity.components.StaticMapEntity; - const lastBeltComp = lastEntity.components.Belt; - - // Figure out where and into which direction we eject items - const ejectSlotWsTile = lastStatic.localTileToWorld(new Vector(0, 0)); - const ejectSlotWsDirection = lastStatic.localDirectionToWorld(lastBeltComp.direction); - const ejectSlotWsDirectionVector = enumDirectionToVector[ejectSlotWsDirection]; - const ejectSlotTargetWsTile = ejectSlotWsTile.add(ejectSlotWsDirectionVector); - - // Try to find the given acceptor component to take the item - const targetEntity = this.root.map.getLayerContentXY( - ejectSlotTargetWsTile.x, - ejectSlotTargetWsTile.y, - "regular" - ); - - if (targetEntity) { - const targetStaticComp = targetEntity.components.StaticMapEntity; - const targetBeltComp = targetEntity.components.Belt; - - // Check for belts (special case) - if (targetBeltComp) { - const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top); - if (ejectSlotWsDirection === beltAcceptingDirection) { - return { - entity: targetEntity, - direction: null, - slot: 0, - }; - } - } - - // Check for item acceptors - const targetAcceptorComp = targetEntity.components.ItemAcceptor; - if (!targetAcceptorComp) { - // Entity doesn't accept items - return; - } - - const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection); - const matchingSlot = targetAcceptorComp.findMatchingSlot( - targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile), - ejectingDirection - ); - - if (!matchingSlot) { - // No matching slot found - return; - } - - return { - entity: targetEntity, - slot: matchingSlot.index, - direction: enumInvertedDirections[ejectingDirection], - }; - } - } - - // Following code will be compiled out outside of dev versions - /* dev:start */ - - /** - * Helper to throw an error on mismatch - * @param {string} change - * @param {Array} reason - */ - debug_failIntegrity(change, ...reason) { - throw new Error("belt path invalid (" + change + "): " + reason.map(i => "" + i).join(" ")); - } - - /** - * Checks if this path is valid - */ - debug_checkIntegrity(currentChange = "change") { - const fail = (...args) => this.debug_failIntegrity(currentChange, ...args); - - // Check for empty path - if (this.entityPath.length === 0) { - return fail("Belt path is empty"); - } - - // Check for mismatching length - const totalLength = this.computeTotalLength(); - if (!epsilonCompare(this.totalLength, totalLength, 0.01)) { - return this.debug_failIntegrity( - currentChange, - "Total length mismatch, stored =", - this.totalLength, - "but correct is", - totalLength - ); - } - - // Check for misconnected entities - for (let i = 0; i < this.entityPath.length - 1; ++i) { - const entity = this.entityPath[i]; - if (entity.destroyed) { - return fail("Reference to destroyed entity " + entity.uid); - } - - const followUp = this.root.systemMgr.systems.belt.findFollowUpEntity(entity); - if (!followUp) { - return fail( - "Follow up entity for the", - i, - "-th entity (total length", - this.entityPath.length, - ") was null!" - ); - } - if (followUp !== this.entityPath[i + 1]) { - return fail( - "Follow up entity mismatch, stored is", - this.entityPath[i + 1].uid, - "but real one is", - followUp.uid - ); - } - if (entity.components.Belt.assignedPath !== this) { - return fail( - "Entity with uid", - entity.uid, - "doesn't have this path assigned, but this path contains the entity." - ); - } - } - - // Check spacing - if (this.spacingToFirstItem > this.totalLength + 0.005) { - return fail( - currentChange, - "spacing to first item (", - this.spacingToFirstItem, - ") is greater than total length (", - this.totalLength, - ")" - ); - } - - // Check distance if empty - if (this.items.length === 0 && !epsilonCompare(this.spacingToFirstItem, this.totalLength, 0.01)) { - return fail( - currentChange, - "Path is empty but spacing to first item (", - this.spacingToFirstItem, - ") does not equal total length (", - this.totalLength, - ")" - ); - } - - // Check items etc - let currentPos = this.spacingToFirstItem; - for (let i = 0; i < this.items.length; ++i) { - const item = this.items[i]; - - if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength + 0.02) { - return fail( - "Item has invalid offset to next item: ", - item[_nextDistance], - "(total length:", - this.totalLength, - ")" - ); - } - - currentPos += item[_nextDistance]; - } - - // Check the total sum matches - if (!epsilonCompare(currentPos, this.totalLength, 0.01)) { - return fail( - "total sum (", - currentPos, - ") of first item spacing (", - this.spacingToFirstItem, - ") and items does not match total length (", - this.totalLength, - ") -> items: " + this.items.map(i => i[_nextDistance]).join("|") - ); - } - - // Check bounds - const actualBounds = this.computeBounds(); - if (!actualBounds.equalsEpsilon(this.worldBounds, 0.01)) { - return fail("Bounds are stale"); - } - } - - /* dev:end */ - - /** - * Extends the belt path by the given belt - * @param {Entity} entity - */ - extendOnEnd(entity) { - DEBUG && logger.log("Extending belt path by entity at", entity.components.StaticMapEntity.origin); - - const beltComp = entity.components.Belt; - - // Append the entity - this.entityPath.push(entity); - this.onPathChanged(); - - // Extend the path length - const additionalLength = beltComp.getEffectiveLengthTiles(); - this.totalLength += additionalLength; - DEBUG && logger.log(" Extended total length by", additionalLength, "to", this.totalLength); - - // If we have no item, just update the distance to the first item - if (this.items.length === 0) { - this.spacingToFirstItem = this.totalLength; - DEBUG && logger.log(" Extended spacing to first to", this.totalLength, "(= total length)"); - } else { - // Otherwise, update the next-distance of the last item - const lastItem = this.items[this.items.length - 1]; - DEBUG && - logger.log( - " Extended spacing of last item from", - lastItem[_nextDistance], - "to", - lastItem[_nextDistance] + additionalLength - ); - lastItem[_nextDistance] += additionalLength; - } - - // Assign reference - beltComp.assignedPath = this; - - // Update bounds - this.worldBounds = this.computeBounds(); - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("extend-on-end"); - } - } - - /** - * Extends the path with the given entity on the beginning - * @param {Entity} entity - */ - extendOnBeginning(entity) { - const beltComp = entity.components.Belt; - - DEBUG && logger.log("Extending the path on the beginning"); - - // All items on that belt are simply lost (for now) - - const length = beltComp.getEffectiveLengthTiles(); - - // Extend the length of this path - this.totalLength += length; - - // Simply adjust the first item spacing cuz we have no items contained - this.spacingToFirstItem += length; - - // Set handles and append entity - beltComp.assignedPath = this; - this.entityPath.unshift(entity); - this.onPathChanged(); - - // Update bounds - this.worldBounds = this.computeBounds(); - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("extend-on-begin"); - } - } - - /** - * Returns if the given entity is the end entity of the path - * @param {Entity} entity - * @returns {boolean} - */ - isEndEntity(entity) { - return this.entityPath[this.entityPath.length - 1] === entity; - } - - /** - * Returns if the given entity is the start entity of the path - * @param {Entity} entity - * @returns {boolean} - */ - isStartEntity(entity) { - return this.entityPath[0] === entity; - } - - /** - * Splits this path at the given entity by removing it, and - * returning the new secondary paht - * @param {Entity} entity - * @returns {BeltPath} - */ - deleteEntityOnPathSplitIntoTwo(entity) { - DEBUG && logger.log("Splitting path at entity", entity.components.StaticMapEntity.origin); - - // First, find where the current path ends - const beltComp = entity.components.Belt; - beltComp.assignedPath = null; - - const entityLength = beltComp.getEffectiveLengthTiles(); - assert(this.entityPath.indexOf(entity) >= 0, "Entity not contained for split"); - assert(this.entityPath.indexOf(entity) !== 0, "Entity is first"); - assert(this.entityPath.indexOf(entity) !== this.entityPath.length - 1, "Entity is last"); - - let firstPathEntityCount = 0; - let firstPathLength = 0; - let firstPathEndEntity = null; - - for (let i = 0; i < this.entityPath.length; ++i) { - const otherEntity = this.entityPath[i]; - if (otherEntity === entity) { - DEBUG && logger.log("Found entity at", i, "of length", firstPathLength); - break; - } - - ++firstPathEntityCount; - firstPathEndEntity = otherEntity; - firstPathLength += otherEntity.components.Belt.getEffectiveLengthTiles(); - } - - DEBUG && - logger.log( - "First path ends at", - firstPathLength, - "and entity", - firstPathEndEntity.components.StaticMapEntity.origin, - "and has", - firstPathEntityCount, - "entities" - ); - - // Compute length of second path - const secondPathLength = this.totalLength - firstPathLength - entityLength; - const secondPathStart = firstPathLength + entityLength; - const secondEntities = this.entityPath.splice(firstPathEntityCount + 1); - DEBUG && - logger.log( - "Second path starts at", - secondPathStart, - "and has a length of ", - secondPathLength, - "with", - secondEntities.length, - "entities" - ); - - // Remove the last item - this.entityPath.pop(); - - DEBUG && logger.log("Splitting", this.items.length, "items"); - DEBUG && - logger.log( - "Old items are", - this.items.map(i => i[_nextDistance]) - ); - - // Create second path - const secondPath = new BeltPath(this.root, secondEntities); - - // Remove all items which are no longer relevant and transfer them to the second path - let itemPos = this.spacingToFirstItem; - for (let i = 0; i < this.items.length; ++i) { - const item = this.items[i]; - const distanceToNext = item[_nextDistance]; - - DEBUG && logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next"); - - // Check if this item is past the first path - if (itemPos >= firstPathLength) { - // Remove it from the first path - this.items.splice(i, 1); - i -= 1; - DEBUG && - logger.log(" Removed item from first path since its no longer contained @", itemPos); - - // Check if its on the second path (otherwise its on the removed belt and simply lost) - if (itemPos >= secondPathStart) { - // Put item on second path - secondPath.items.push([distanceToNext, item[_item]]); - DEBUG && - logger.log( - " Put item to second path @", - itemPos, - "with distance to next =", - distanceToNext - ); - - // If it was the first item, adjust the distance to the first item - if (secondPath.items.length === 1) { - DEBUG && logger.log(" Sinc it was the first, set sapcing of first to", itemPos); - secondPath.spacingToFirstItem = itemPos - secondPathStart; - } - } else { - DEBUG && logger.log(" Item was on the removed belt, so its gone - forever!"); - } - } else { - // Seems this item is on the first path (so all good), so just make sure it doesn't - // have a nextDistance which is bigger than the total path length - const clampedDistanceToNext = Math.min(itemPos + distanceToNext, firstPathLength) - itemPos; - if (clampedDistanceToNext < distanceToNext) { - DEBUG && - logger.log( - "Correcting next distance (first path) from", - distanceToNext, - "to", - clampedDistanceToNext - ); - item[_nextDistance] = clampedDistanceToNext; - } - } - - // Advance items - itemPos += distanceToNext; - } - - DEBUG && - logger.log( - "New items are", - this.items.map(i => i[_nextDistance]) - ); - - DEBUG && - logger.log( - "And second path items are", - secondPath.items.map(i => i[_nextDistance]) - ); - - // Adjust our total length - this.totalLength = firstPathLength; - - // Make sure that if we are empty, we set our first distance properly - if (this.items.length === 0) { - this.spacingToFirstItem = this.totalLength; - } - - this.onPathChanged(); - secondPath.onPathChanged(); - - // Update bounds - this.worldBounds = this.computeBounds(); - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("split-two-first"); - secondPath.debug_checkIntegrity("split-two-second"); - } - - return secondPath; - } - - /** - * Deletes the last entity - * @param {Entity} entity - */ - deleteEntityOnEnd(entity) { - assert( - this.entityPath[this.entityPath.length - 1] === entity, - "Not actually the last entity (instead " + this.entityPath.indexOf(entity) + ")" - ); - - // Ok, first remove the entity - const beltComp = entity.components.Belt; - const beltLength = beltComp.getEffectiveLengthTiles(); - - DEBUG && - logger.log( - "Deleting last entity on path with length", - this.entityPath.length, - "(reducing", - this.totalLength, - " by", - beltLength, - ")" - ); - this.totalLength -= beltLength; - this.entityPath.pop(); - this.onPathChanged(); - - DEBUG && - logger.log( - " New path has length of", - this.totalLength, - "with", - this.entityPath.length, - "entities" - ); - - // This is just for sanity - beltComp.assignedPath = null; - - // Clean up items - if (this.items.length === 0) { - // Simple case with no items, just update the first item spacing - this.spacingToFirstItem = this.totalLength; - } else { - // Ok, make sure we simply drop all items which are no longer contained - let itemOffset = this.spacingToFirstItem; - let lastItemOffset = itemOffset; - - DEBUG && logger.log(" Adjusting", this.items.length, "items"); - - for (let i = 0; i < this.items.length; ++i) { - const item = this.items[i]; - - // Get rid of items past this path - if (itemOffset >= this.totalLength) { - DEBUG && logger.log("Dropping item (current index=", i, ")"); - this.items.splice(i, 1); - i -= 1; - continue; - } - - DEBUG && logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]); - lastItemOffset = itemOffset; - itemOffset += item[_nextDistance]; - } - - // If we still have an item, make sure the last item matches - if (this.items.length > 0) { - // We can easily compute the next distance since we know where the last item is now - const lastDistance = this.totalLength - lastItemOffset; - assert( - lastDistance >= 0.0, - "Last item distance mismatch: " + - lastDistance + - " -> Total length was " + - this.totalLength + - " and lastItemOffset was " + - lastItemOffset - ); - - DEBUG && - logger.log( - "Adjusted distance of last item: it is at", - lastItemOffset, - "so it has a distance of", - lastDistance, - "to the end (", - this.totalLength, - ")" - ); - this.items[this.items.length - 1][_nextDistance] = lastDistance; - } else { - DEBUG && logger.log(" Removed all items so we'll update spacing to total length"); - - // We removed all items so update our spacing - this.spacingToFirstItem = this.totalLength; - } - } - - // Update bounds - this.worldBounds = this.computeBounds(); - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("delete-on-end"); - } - } - - /** - * Deletes the entity of the start of the path - * @see deleteEntityOnEnd - * @param {Entity} entity - */ - deleteEntityOnStart(entity) { - assert( - entity === this.entityPath[0], - "Not actually the start entity (instead " + this.entityPath.indexOf(entity) + ")" - ); - - // Ok, first remove the entity - const beltComp = entity.components.Belt; - const beltLength = beltComp.getEffectiveLengthTiles(); - - DEBUG && - logger.log( - "Deleting first entity on path with length", - this.entityPath.length, - "(reducing", - this.totalLength, - " by", - beltLength, - ")" - ); - this.totalLength -= beltLength; - this.entityPath.shift(); - this.onPathChanged(); - - DEBUG && - logger.log( - " New path has length of", - this.totalLength, - "with", - this.entityPath.length, - "entities" - ); - - // This is just for sanity - beltComp.assignedPath = null; - - // Clean up items - if (this.items.length === 0) { - // Simple case with no items, just update the first item spacing - this.spacingToFirstItem = this.totalLength; - } else { - // Simple case, we had no item on the beginning -> all good - if (this.spacingToFirstItem >= beltLength) { - DEBUG && - logger.log( - " No item on the first place, so we can just adjust the spacing (spacing=", - this.spacingToFirstItem, - ") removed =", - beltLength - ); - this.spacingToFirstItem -= beltLength; - } else { - // Welp, okay we need to drop all items which are < beltLength and adjust - // the other item offsets as well - - DEBUG && - logger.log( - " We have at least one item in the beginning, drop those and adjust spacing (first item @", - this.spacingToFirstItem, - ") since we removed", - beltLength, - "length from path" - ); - DEBUG && - logger.log( - " Items:", - this.items.map(i => i[_nextDistance]) - ); - - // Find offset to first item - let itemOffset = this.spacingToFirstItem; - for (let i = 0; i < this.items.length; ++i) { - const item = this.items[i]; - if (itemOffset <= beltLength) { - DEBUG && - logger.log( - " -> Dropping item with index", - i, - "at", - itemOffset, - "since it was on the removed belt" - ); - // This item must be dropped - this.items.splice(i, 1); - i -= 1; - itemOffset += item[_nextDistance]; - continue; - } else { - // This item can be kept, thus its the first we know - break; - } - } - - if (this.items.length > 0) { - DEBUG && - logger.log( - " Offset of first non-dropped item was at:", - itemOffset, - "-> setting spacing to it (total length=", - this.totalLength, - ")" - ); - - this.spacingToFirstItem = itemOffset - beltLength; - assert( - this.spacingToFirstItem >= 0.0, - "Invalid spacing after delete on start: " + this.spacingToFirstItem - ); - } else { - DEBUG && logger.log(" We dropped all items, simply set spacing to total length"); - // We dropped all items, simple one - this.spacingToFirstItem = this.totalLength; - } - } - } - - // Update bounds - this.worldBounds = this.computeBounds(); - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("delete-on-start"); - } - } - - /** - * Extends the path by the given other path - * @param {BeltPath} otherPath - */ - extendByPath(otherPath) { - assert(otherPath !== this, "Circular path dependency"); - - const entities = otherPath.entityPath; - DEBUG && logger.log("Extending path by other path, starting to add entities"); - - const oldLength = this.totalLength; - - DEBUG && logger.log(" Adding", entities.length, "new entities, current length =", this.totalLength); - - // First, append entities - for (let i = 0; i < entities.length; ++i) { - const entity = entities[i]; - const beltComp = entity.components.Belt; - - // Add to path and update references - this.entityPath.push(entity); - beltComp.assignedPath = this; - - // Update our length - const additionalLength = beltComp.getEffectiveLengthTiles(); - this.totalLength += additionalLength; - } - - DEBUG && - logger.log( - " Path is now", - this.entityPath.length, - "entities and has a length of", - this.totalLength - ); - - // Now, update the distance of our last item - if (this.items.length !== 0) { - const lastItem = this.items[this.items.length - 1]; - lastItem[_nextDistance] += otherPath.spacingToFirstItem; - DEBUG && - logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now"); - } else { - // Seems we have no items, update our first item distance - this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem; - DEBUG && - logger.log( - " We had no items, so our new spacing to first is old length (", - oldLength, - ") plus others spacing to first (", - otherPath.spacingToFirstItem, - ") =", - this.spacingToFirstItem - ); - } - - DEBUG && logger.log(" Pushing", otherPath.items.length, "items from other path"); - - // Aaand push the other paths items - for (let i = 0; i < otherPath.items.length; ++i) { - const item = otherPath.items[i]; - this.items.push([item[_nextDistance], item[_item]]); - } - - // Update bounds - this.worldBounds = this.computeBounds(); - - this.onPathChanged(); - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("extend-by-path"); - } - } - - /** - * Computes the total length of the path - * @returns {number} - */ - computeTotalLength() { - let length = 0; - for (let i = 0; i < this.entityPath.length; ++i) { - const entity = this.entityPath[i]; - length += entity.components.Belt.getEffectiveLengthTiles(); - } - return length; - } - - /** - * Performs one tick - */ - update() { - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("pre-update"); - } - - // Divide by item spacing on belts since we use throughput and not speed - let beltSpeed = - this.root.hubGoals.getBeltBaseSpeed() * - this.root.dynamicTickrate.deltaSeconds * - globalConfig.itemSpacingOnBelts; - - if (G_IS_DEV && globalConfig.debug.instantBelts) { - beltSpeed *= 100; - } - - let minimumDistance = 0; - - // Try to reduce spacing - let remainingAmount = beltSpeed; - for (let i = this.items.length - 1; i >= 0; --i) { - const nextDistanceAndItem = this.items[i]; - const minimumSpacing = minimumDistance; - - const takeAway = Math.max( - 0, - Math.min(remainingAmount, nextDistanceAndItem[_nextDistance] - minimumSpacing) - ); - - remainingAmount -= takeAway; - nextDistanceAndItem[_nextDistance] -= takeAway; - - this.spacingToFirstItem += takeAway; - if (remainingAmount < 0.01) { - break; - } - - minimumDistance = globalConfig.itemSpacingOnBelts; - } - - // 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(); - } - } - - if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { - this.debug_checkIntegrity("post-update"); - } - } - - /** - * Tries to hand over the item to the end entity - * @param {BaseItem} item - */ - tryHandOverItem(item) { - if (!this.acceptorTarget) { - return; - } - - const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor; - - // Check if the acceptor has a filter for example - if (targetAcceptorComp && !targetAcceptorComp.canAcceptItem(this.acceptorTarget.slot, item)) { - // Well, this item is not accepted - return false; - } - - // Try to pass over - if ( - this.root.systemMgr.systems.itemEjector.tryPassOverItem( - item, - this.acceptorTarget.entity, - this.acceptorTarget.slot - ) - ) { - // Trigger animation on the acceptor comp - const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor; - if (targetAcceptorComp) { - targetAcceptorComp.onItemAccepted( - this.acceptorTarget.slot, - this.acceptorTarget.direction, - item - ); - } - - return true; - } - - return false; - } - - /** - * Computes a world space position from the given progress - * @param {number} progress - * @returns {Vector} - */ - computePositionFromProgress(progress) { - let currentLength = 0; - - // floating point issuses .. - assert(progress <= this.totalLength + 0.02, "Progress too big: " + progress); - - for (let i = 0; i < this.entityPath.length; ++i) { - const beltComp = this.entityPath[i].components.Belt; - const localLength = beltComp.getEffectiveLengthTiles(); - - if (currentLength + localLength >= progress || i === this.entityPath.length - 1) { - // Min required here due to floating point issues - const localProgress = Math.min(1.0, progress - currentLength); - - assert(localProgress >= 0.0, "Invalid local progress: " + localProgress); - const localSpace = beltComp.transformBeltToLocalSpace(localProgress); - return this.entityPath[i].components.StaticMapEntity.localTileToWorld(localSpace); - } - currentLength += localLength; - } - - assert(false, "invalid progress: " + progress + " (max: " + this.totalLength + ")"); - } - - /** - * - * @param {DrawParameters} parameters - */ - drawDebug(parameters) { - if (!parameters.visibleRect.containsRect(this.worldBounds)) { - return; - } - - parameters.context.fillStyle = "#d79a25"; - parameters.context.strokeStyle = "#d79a25"; - parameters.context.beginPath(); - - for (let i = 0; i < this.entityPath.length; ++i) { - const entity = this.entityPath[i]; - const pos = entity.components.StaticMapEntity; - const worldPos = pos.origin.toWorldSpaceCenterOfTile(); - - if (i === 0) { - parameters.context.moveTo(worldPos.x, worldPos.y); - } else { - parameters.context.lineTo(worldPos.x, worldPos.y); - } - } - parameters.context.stroke(); - - // Items - let progress = this.spacingToFirstItem; - for (let i = 0; i < this.items.length; ++i) { - const nextDistanceAndItem = this.items[i]; - const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile(); - parameters.context.fillStyle = "#268e4d"; - parameters.context.beginRoundedRect(worldPos.x - 5, worldPos.y - 5, 10, 10, 3); - parameters.context.fill(); - parameters.context.font = "6px GameFont"; - parameters.context.fillStyle = "#111"; - parameters.context.fillText( - "" + round4Digits(nextDistanceAndItem[_nextDistance]), - worldPos.x + 5, - worldPos.y + 2 - ); - progress += nextDistanceAndItem[_nextDistance]; - } - - for (let i = 0; i < this.entityPath.length; ++i) { - const entity = this.entityPath[i]; - parameters.context.fillStyle = "#d79a25"; - const pos = entity.components.StaticMapEntity; - const worldPos = pos.origin.toWorldSpaceCenterOfTile(); - parameters.context.beginCircle(worldPos.x, worldPos.y, i === 0 ? 5 : 3); - parameters.context.fill(); - } - - for (let progress = 0; progress <= this.totalLength + 0.01; progress += 0.2) { - const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile(); - parameters.context.fillStyle = "red"; - parameters.context.beginCircle(worldPos.x, worldPos.y, 1); - parameters.context.fill(); - } - - const firstItemIndicator = this.computePositionFromProgress( - this.spacingToFirstItem - ).toWorldSpaceCenterOfTile(); - parameters.context.fillStyle = "purple"; - parameters.context.fillRect(firstItemIndicator.x - 3, firstItemIndicator.y - 1, 6, 2); - } - - /** - * Draws the path - * @param {DrawParameters} parameters - */ - draw(parameters) { - if (!parameters.visibleRect.containsRect(this.worldBounds)) { - return; - } - - if (this.items.length === 0) { - // Early out - return; - } - - let currentItemPos = this.spacingToFirstItem; - let currentItemIndex = 0; - - let trackPos = 0.0; - - // Iterate whole track and check items - for (let i = 0; i < this.entityPath.length; ++i) { - const entity = this.entityPath[i]; - const beltComp = entity.components.Belt; - const beltLength = beltComp.getEffectiveLengthTiles(); - - // Check if the current items are on the belt - while (trackPos + beltLength >= currentItemPos - 1e-5) { - // Its on the belt, render it now - const staticComp = entity.components.StaticMapEntity; - assert( - currentItemPos - trackPos >= 0, - "invalid track pos: " + currentItemPos + " vs " + trackPos + " (l =" + beltLength + ")" - ); - - const localPos = beltComp.transformBeltToLocalSpace(currentItemPos - trackPos); - const worldPos = staticComp.localTileToWorld(localPos).toWorldSpaceCenterOfTile(); - - const distanceAndItem = this.items[currentItemIndex]; - - distanceAndItem[_item].drawItemCenteredClipped( - worldPos.x, - worldPos.y, - parameters, - globalConfig.defaultItemDiameter - ); - - // Check for the next item - currentItemPos += distanceAndItem[_nextDistance]; - ++currentItemIndex; - - if (currentItemIndex >= this.items.length) { - // We rendered all items - return; - } - } - - trackPos += beltLength; - } - } -} +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 { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector"; +import { BasicSerializableObject, types } from "../savegame/serialization"; +import { BaseItem } from "./base_item"; +import { Entity } from "./entity"; +import { typeItemSingleton } from "./item_resolver"; +import { GameRoot } from "./root"; + +const logger = createLogger("belt_path"); + +// Helpers for more semantic access into interleaved arrays +const _nextDistance = 0; +const _item = 1; + +const DEBUG = G_IS_DEV && false; + +/** + * Stores a path of belts, used for optimizing performance + */ +export class BeltPath extends BasicSerializableObject { + static getId() { + return "BeltPath"; + } + + static getSchema() { + return { + entityPath: types.array(types.entity), + items: types.array(types.pair(types.ufloat, typeItemSingleton)), + spacingToFirstItem: types.ufloat, + }; + } + + /** + * Creates a path from a serialized object + * @param {GameRoot} root + * @param {Object} data + * @returns {BeltPath|string} + */ + static fromSerialized(root, data) { + // Create fake object which looks like a belt path but skips the constructor + const fakeObject = /** @type {BeltPath} */ (Object.create(BeltPath.prototype)); + fakeObject.root = root; + + // Deserialize the data + const errorCodeDeserialize = fakeObject.deserialize(data); + if (errorCodeDeserialize) { + return errorCodeDeserialize; + } + + // Compute other properties + fakeObject.init(false); + + return fakeObject; + } + + /** + * @param {GameRoot} root + * @param {Array} entityPath + */ + constructor(root, entityPath) { + super(); + this.root = root; + + assert(entityPath.length > 0, "invalid entity path"); + this.entityPath = entityPath; + + /** + * Stores the items sorted, and their distance to the previous item (or start) + * Layout: [distanceToNext, item] + * @type {Array<[number, BaseItem]>} + */ + this.items = []; + + /** + * Stores the spacing to the first item + */ + + this.init(); + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("constructor"); + } + } + /** + * Initializes the path by computing the properties which are not saved + * @param {boolean} computeSpacing Whether to also compute the spacing + */ + init(computeSpacing = true) { + this.onPathChanged(); + + this.totalLength = this.computeTotalLength(); + + if (computeSpacing) { + this.spacingToFirstItem = this.totalLength; + } + + /** + * Current bounds of this path + * @type {Rectangle} + */ + this.worldBounds = this.computeBounds(); + + // Connect the belts + for (let i = 0; i < this.entityPath.length; ++i) { + this.entityPath[i].components.Belt.assignedPath = this; + } + } + + /** + * Returns whether this path can accept a new item + * @returns {boolean} + */ + canAcceptItem() { + return this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts; + } + + /** + * Tries to accept the item + * @param {BaseItem} item + */ + tryAcceptItem(item) { + if (this.spacingToFirstItem >= globalConfig.itemSpacingOnBelts) { + // So, since we already need one tick to accept this item we will add this directly. + const beltProgressPerTick = + this.root.hubGoals.getBeltBaseSpeed() * + this.root.dynamicTickrate.deltaSeconds * + globalConfig.itemSpacingOnBelts; + + // First, compute how much progress we can make *at max* + const maxProgress = Math.max(0, this.spacingToFirstItem - globalConfig.itemSpacingOnBelts); + const initialProgress = Math.min(maxProgress, beltProgressPerTick); + + this.items.unshift([this.spacingToFirstItem - initialProgress, item]); + this.spacingToFirstItem = initialProgress; + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("accept-item"); + } + + return true; + } + return false; + } + + /** + * SLOW / Tries to find the item closest to the given tile + * @param {Vector} tile + * @returns {BaseItem|null} + */ + findItemAtTile(tile) { + // @TODO: This breaks color blind mode otherwise + return null; + } + + /** + * Computes the tile bounds of the path + * @returns {Rectangle} + */ + computeBounds() { + let bounds = this.entityPath[0].components.StaticMapEntity.getTileSpaceBounds(); + for (let i = 1; i < this.entityPath.length; ++i) { + const staticComp = this.entityPath[i].components.StaticMapEntity; + const otherBounds = staticComp.getTileSpaceBounds(); + bounds = bounds.getUnion(otherBounds); + } + return bounds.allScaled(globalConfig.tileSize); + } + + /** + * Recomputes cache variables once the path was changed + */ + onPathChanged() { + this.acceptorTarget = this.computeAcceptingEntityAndSlot(); + } + + /** + * Called by the belt system when the surroundings changed + */ + onSurroundingsChanged() { + this.onPathChanged(); + } + + /** + * Finds the entity which accepts our items + * @return {{ entity: Entity, slot: number, direction?: enumDirection }} + */ + computeAcceptingEntityAndSlot() { + const lastEntity = this.entityPath[this.entityPath.length - 1]; + const lastStatic = lastEntity.components.StaticMapEntity; + const lastBeltComp = lastEntity.components.Belt; + + // Figure out where and into which direction we eject items + const ejectSlotWsTile = lastStatic.localTileToWorld(new Vector(0, 0)); + const ejectSlotWsDirection = lastStatic.localDirectionToWorld(lastBeltComp.direction); + const ejectSlotWsDirectionVector = enumDirectionToVector[ejectSlotWsDirection]; + const ejectSlotTargetWsTile = ejectSlotWsTile.add(ejectSlotWsDirectionVector); + + // Try to find the given acceptor component to take the item + const targetEntity = this.root.map.getLayerContentXY( + ejectSlotTargetWsTile.x, + ejectSlotTargetWsTile.y, + "regular" + ); + + if (targetEntity) { + const targetStaticComp = targetEntity.components.StaticMapEntity; + const targetBeltComp = targetEntity.components.Belt; + + // Check for belts (special case) + if (targetBeltComp) { + const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top); + if (ejectSlotWsDirection === beltAcceptingDirection) { + return { + entity: targetEntity, + direction: null, + slot: 0, + }; + } + } + + // Check for item acceptors + const targetAcceptorComp = targetEntity.components.ItemAcceptor; + if (!targetAcceptorComp) { + // Entity doesn't accept items + return; + } + + const ejectingDirection = targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection); + const matchingSlot = targetAcceptorComp.findMatchingSlot( + targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile), + ejectingDirection + ); + + if (!matchingSlot) { + // No matching slot found + return; + } + + return { + entity: targetEntity, + slot: matchingSlot.index, + direction: enumInvertedDirections[ejectingDirection], + }; + } + } + + // Following code will be compiled out outside of dev versions + /* dev:start */ + + /** + * Helper to throw an error on mismatch + * @param {string} change + * @param {Array} reason + */ + debug_failIntegrity(change, ...reason) { + throw new Error("belt path invalid (" + change + "): " + reason.map(i => "" + i).join(" ")); + } + + /** + * Checks if this path is valid + */ + debug_checkIntegrity(currentChange = "change") { + const fail = (...args) => this.debug_failIntegrity(currentChange, ...args); + + // Check for empty path + if (this.entityPath.length === 0) { + return fail("Belt path is empty"); + } + + // Check for mismatching length + const totalLength = this.computeTotalLength(); + if (!epsilonCompare(this.totalLength, totalLength, 0.01)) { + return this.debug_failIntegrity( + currentChange, + "Total length mismatch, stored =", + this.totalLength, + "but correct is", + totalLength + ); + } + + // Check for misconnected entities + for (let i = 0; i < this.entityPath.length - 1; ++i) { + const entity = this.entityPath[i]; + if (entity.destroyed) { + return fail("Reference to destroyed entity " + entity.uid); + } + + const followUp = this.root.systemMgr.systems.belt.findFollowUpEntity(entity); + if (!followUp) { + return fail( + "Follow up entity for the", + i, + "-th entity (total length", + this.entityPath.length, + ") was null!" + ); + } + if (followUp !== this.entityPath[i + 1]) { + return fail( + "Follow up entity mismatch, stored is", + this.entityPath[i + 1].uid, + "but real one is", + followUp.uid + ); + } + if (entity.components.Belt.assignedPath !== this) { + return fail( + "Entity with uid", + entity.uid, + "doesn't have this path assigned, but this path contains the entity." + ); + } + } + + // Check spacing + if (this.spacingToFirstItem > this.totalLength + 0.005) { + return fail( + currentChange, + "spacing to first item (", + this.spacingToFirstItem, + ") is greater than total length (", + this.totalLength, + ")" + ); + } + + // Check distance if empty + if (this.items.length === 0 && !epsilonCompare(this.spacingToFirstItem, this.totalLength, 0.01)) { + return fail( + currentChange, + "Path is empty but spacing to first item (", + this.spacingToFirstItem, + ") does not equal total length (", + this.totalLength, + ")" + ); + } + + // Check items etc + let currentPos = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + + if (item[_nextDistance] < 0 || item[_nextDistance] > this.totalLength + 0.02) { + return fail( + "Item has invalid offset to next item: ", + item[_nextDistance], + "(total length:", + this.totalLength, + ")" + ); + } + + currentPos += item[_nextDistance]; + } + + // Check the total sum matches + if (!epsilonCompare(currentPos, this.totalLength, 0.01)) { + return fail( + "total sum (", + currentPos, + ") of first item spacing (", + this.spacingToFirstItem, + ") and items does not match total length (", + this.totalLength, + ") -> items: " + this.items.map(i => i[_nextDistance]).join("|") + ); + } + + // Check bounds + const actualBounds = this.computeBounds(); + if (!actualBounds.equalsEpsilon(this.worldBounds, 0.01)) { + return fail("Bounds are stale"); + } + } + + /* dev:end */ + + /** + * Extends the belt path by the given belt + * @param {Entity} entity + */ + extendOnEnd(entity) { + DEBUG && logger.log("Extending belt path by entity at", entity.components.StaticMapEntity.origin); + + const beltComp = entity.components.Belt; + + // Append the entity + this.entityPath.push(entity); + this.onPathChanged(); + + // Extend the path length + const additionalLength = beltComp.getEffectiveLengthTiles(); + this.totalLength += additionalLength; + DEBUG && logger.log(" Extended total length by", additionalLength, "to", this.totalLength); + + // If we have no item, just update the distance to the first item + if (this.items.length === 0) { + this.spacingToFirstItem = this.totalLength; + DEBUG && logger.log(" Extended spacing to first to", this.totalLength, "(= total length)"); + } else { + // Otherwise, update the next-distance of the last item + const lastItem = this.items[this.items.length - 1]; + DEBUG && + logger.log( + " Extended spacing of last item from", + lastItem[_nextDistance], + "to", + lastItem[_nextDistance] + additionalLength + ); + lastItem[_nextDistance] += additionalLength; + } + + // Assign reference + beltComp.assignedPath = this; + + // Update bounds + this.worldBounds = this.computeBounds(); + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("extend-on-end"); + } + } + + /** + * Extends the path with the given entity on the beginning + * @param {Entity} entity + */ + extendOnBeginning(entity) { + const beltComp = entity.components.Belt; + + DEBUG && logger.log("Extending the path on the beginning"); + + // All items on that belt are simply lost (for now) + + const length = beltComp.getEffectiveLengthTiles(); + + // Extend the length of this path + this.totalLength += length; + + // Simply adjust the first item spacing cuz we have no items contained + this.spacingToFirstItem += length; + + // Set handles and append entity + beltComp.assignedPath = this; + this.entityPath.unshift(entity); + this.onPathChanged(); + + // Update bounds + this.worldBounds = this.computeBounds(); + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("extend-on-begin"); + } + } + + /** + * Returns if the given entity is the end entity of the path + * @param {Entity} entity + * @returns {boolean} + */ + isEndEntity(entity) { + return this.entityPath[this.entityPath.length - 1] === entity; + } + + /** + * Returns if the given entity is the start entity of the path + * @param {Entity} entity + * @returns {boolean} + */ + isStartEntity(entity) { + return this.entityPath[0] === entity; + } + + /** + * Splits this path at the given entity by removing it, and + * returning the new secondary paht + * @param {Entity} entity + * @returns {BeltPath} + */ + deleteEntityOnPathSplitIntoTwo(entity) { + DEBUG && logger.log("Splitting path at entity", entity.components.StaticMapEntity.origin); + + // First, find where the current path ends + const beltComp = entity.components.Belt; + beltComp.assignedPath = null; + + const entityLength = beltComp.getEffectiveLengthTiles(); + assert(this.entityPath.indexOf(entity) >= 0, "Entity not contained for split"); + assert(this.entityPath.indexOf(entity) !== 0, "Entity is first"); + assert(this.entityPath.indexOf(entity) !== this.entityPath.length - 1, "Entity is last"); + + let firstPathEntityCount = 0; + let firstPathLength = 0; + let firstPathEndEntity = null; + + for (let i = 0; i < this.entityPath.length; ++i) { + const otherEntity = this.entityPath[i]; + if (otherEntity === entity) { + DEBUG && logger.log("Found entity at", i, "of length", firstPathLength); + break; + } + + ++firstPathEntityCount; + firstPathEndEntity = otherEntity; + firstPathLength += otherEntity.components.Belt.getEffectiveLengthTiles(); + } + + DEBUG && + logger.log( + "First path ends at", + firstPathLength, + "and entity", + firstPathEndEntity.components.StaticMapEntity.origin, + "and has", + firstPathEntityCount, + "entities" + ); + + // Compute length of second path + const secondPathLength = this.totalLength - firstPathLength - entityLength; + const secondPathStart = firstPathLength + entityLength; + const secondEntities = this.entityPath.splice(firstPathEntityCount + 1); + DEBUG && + logger.log( + "Second path starts at", + secondPathStart, + "and has a length of ", + secondPathLength, + "with", + secondEntities.length, + "entities" + ); + + // Remove the last item + this.entityPath.pop(); + + DEBUG && logger.log("Splitting", this.items.length, "items"); + DEBUG && + logger.log( + "Old items are", + this.items.map(i => i[_nextDistance]) + ); + + // Create second path + const secondPath = new BeltPath(this.root, secondEntities); + + // Remove all items which are no longer relevant and transfer them to the second path + let itemPos = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + const distanceToNext = item[_nextDistance]; + + DEBUG && logger.log(" Checking item at", itemPos, "with distance of", distanceToNext, "to next"); + + // Check if this item is past the first path + if (itemPos >= firstPathLength) { + // Remove it from the first path + this.items.splice(i, 1); + i -= 1; + DEBUG && + logger.log(" Removed item from first path since its no longer contained @", itemPos); + + // Check if its on the second path (otherwise its on the removed belt and simply lost) + if (itemPos >= secondPathStart) { + // Put item on second path + secondPath.items.push([distanceToNext, item[_item]]); + DEBUG && + logger.log( + " Put item to second path @", + itemPos, + "with distance to next =", + distanceToNext + ); + + // If it was the first item, adjust the distance to the first item + if (secondPath.items.length === 1) { + DEBUG && logger.log(" Sinc it was the first, set sapcing of first to", itemPos); + secondPath.spacingToFirstItem = itemPos - secondPathStart; + } + } else { + DEBUG && logger.log(" Item was on the removed belt, so its gone - forever!"); + } + } else { + // Seems this item is on the first path (so all good), so just make sure it doesn't + // have a nextDistance which is bigger than the total path length + const clampedDistanceToNext = Math.min(itemPos + distanceToNext, firstPathLength) - itemPos; + if (clampedDistanceToNext < distanceToNext) { + DEBUG && + logger.log( + "Correcting next distance (first path) from", + distanceToNext, + "to", + clampedDistanceToNext + ); + item[_nextDistance] = clampedDistanceToNext; + } + } + + // Advance items + itemPos += distanceToNext; + } + + DEBUG && + logger.log( + "New items are", + this.items.map(i => i[_nextDistance]) + ); + + DEBUG && + logger.log( + "And second path items are", + secondPath.items.map(i => i[_nextDistance]) + ); + + // Adjust our total length + this.totalLength = firstPathLength; + + // Make sure that if we are empty, we set our first distance properly + if (this.items.length === 0) { + this.spacingToFirstItem = this.totalLength; + } + + this.onPathChanged(); + secondPath.onPathChanged(); + + // Update bounds + this.worldBounds = this.computeBounds(); + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("split-two-first"); + secondPath.debug_checkIntegrity("split-two-second"); + } + + return secondPath; + } + + /** + * Deletes the last entity + * @param {Entity} entity + */ + deleteEntityOnEnd(entity) { + assert( + this.entityPath[this.entityPath.length - 1] === entity, + "Not actually the last entity (instead " + this.entityPath.indexOf(entity) + ")" + ); + + // Ok, first remove the entity + const beltComp = entity.components.Belt; + const beltLength = beltComp.getEffectiveLengthTiles(); + + DEBUG && + logger.log( + "Deleting last entity on path with length", + this.entityPath.length, + "(reducing", + this.totalLength, + " by", + beltLength, + ")" + ); + this.totalLength -= beltLength; + this.entityPath.pop(); + this.onPathChanged(); + + DEBUG && + logger.log( + " New path has length of", + this.totalLength, + "with", + this.entityPath.length, + "entities" + ); + + // This is just for sanity + beltComp.assignedPath = null; + + // Clean up items + if (this.items.length === 0) { + // Simple case with no items, just update the first item spacing + this.spacingToFirstItem = this.totalLength; + } else { + // Ok, make sure we simply drop all items which are no longer contained + let itemOffset = this.spacingToFirstItem; + let lastItemOffset = itemOffset; + + DEBUG && logger.log(" Adjusting", this.items.length, "items"); + + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + + // Get rid of items past this path + if (itemOffset >= this.totalLength) { + DEBUG && logger.log("Dropping item (current index=", i, ")"); + this.items.splice(i, 1); + i -= 1; + continue; + } + + DEBUG && logger.log("Item", i, "is at", itemOffset, "with next offset", item[_nextDistance]); + lastItemOffset = itemOffset; + itemOffset += item[_nextDistance]; + } + + // If we still have an item, make sure the last item matches + if (this.items.length > 0) { + // We can easily compute the next distance since we know where the last item is now + const lastDistance = this.totalLength - lastItemOffset; + assert( + lastDistance >= 0.0, + "Last item distance mismatch: " + + lastDistance + + " -> Total length was " + + this.totalLength + + " and lastItemOffset was " + + lastItemOffset + ); + + DEBUG && + logger.log( + "Adjusted distance of last item: it is at", + lastItemOffset, + "so it has a distance of", + lastDistance, + "to the end (", + this.totalLength, + ")" + ); + this.items[this.items.length - 1][_nextDistance] = lastDistance; + } else { + DEBUG && logger.log(" Removed all items so we'll update spacing to total length"); + + // We removed all items so update our spacing + this.spacingToFirstItem = this.totalLength; + } + } + + // Update bounds + this.worldBounds = this.computeBounds(); + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("delete-on-end"); + } + } + + /** + * Deletes the entity of the start of the path + * @see deleteEntityOnEnd + * @param {Entity} entity + */ + deleteEntityOnStart(entity) { + assert( + entity === this.entityPath[0], + "Not actually the start entity (instead " + this.entityPath.indexOf(entity) + ")" + ); + + // Ok, first remove the entity + const beltComp = entity.components.Belt; + const beltLength = beltComp.getEffectiveLengthTiles(); + + DEBUG && + logger.log( + "Deleting first entity on path with length", + this.entityPath.length, + "(reducing", + this.totalLength, + " by", + beltLength, + ")" + ); + this.totalLength -= beltLength; + this.entityPath.shift(); + this.onPathChanged(); + + DEBUG && + logger.log( + " New path has length of", + this.totalLength, + "with", + this.entityPath.length, + "entities" + ); + + // This is just for sanity + beltComp.assignedPath = null; + + // Clean up items + if (this.items.length === 0) { + // Simple case with no items, just update the first item spacing + this.spacingToFirstItem = this.totalLength; + } else { + // Simple case, we had no item on the beginning -> all good + if (this.spacingToFirstItem >= beltLength) { + DEBUG && + logger.log( + " No item on the first place, so we can just adjust the spacing (spacing=", + this.spacingToFirstItem, + ") removed =", + beltLength + ); + this.spacingToFirstItem -= beltLength; + } else { + // Welp, okay we need to drop all items which are < beltLength and adjust + // the other item offsets as well + + DEBUG && + logger.log( + " We have at least one item in the beginning, drop those and adjust spacing (first item @", + this.spacingToFirstItem, + ") since we removed", + beltLength, + "length from path" + ); + DEBUG && + logger.log( + " Items:", + this.items.map(i => i[_nextDistance]) + ); + + // Find offset to first item + let itemOffset = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const item = this.items[i]; + if (itemOffset <= beltLength) { + DEBUG && + logger.log( + " -> Dropping item with index", + i, + "at", + itemOffset, + "since it was on the removed belt" + ); + // This item must be dropped + this.items.splice(i, 1); + i -= 1; + itemOffset += item[_nextDistance]; + continue; + } else { + // This item can be kept, thus its the first we know + break; + } + } + + if (this.items.length > 0) { + DEBUG && + logger.log( + " Offset of first non-dropped item was at:", + itemOffset, + "-> setting spacing to it (total length=", + this.totalLength, + ")" + ); + + this.spacingToFirstItem = itemOffset - beltLength; + assert( + this.spacingToFirstItem >= 0.0, + "Invalid spacing after delete on start: " + this.spacingToFirstItem + ); + } else { + DEBUG && logger.log(" We dropped all items, simply set spacing to total length"); + // We dropped all items, simple one + this.spacingToFirstItem = this.totalLength; + } + } + } + + // Update bounds + this.worldBounds = this.computeBounds(); + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("delete-on-start"); + } + } + + /** + * Extends the path by the given other path + * @param {BeltPath} otherPath + */ + extendByPath(otherPath) { + assert(otherPath !== this, "Circular path dependency"); + + const entities = otherPath.entityPath; + DEBUG && logger.log("Extending path by other path, starting to add entities"); + + const oldLength = this.totalLength; + + DEBUG && logger.log(" Adding", entities.length, "new entities, current length =", this.totalLength); + + // First, append entities + for (let i = 0; i < entities.length; ++i) { + const entity = entities[i]; + const beltComp = entity.components.Belt; + + // Add to path and update references + this.entityPath.push(entity); + beltComp.assignedPath = this; + + // Update our length + const additionalLength = beltComp.getEffectiveLengthTiles(); + this.totalLength += additionalLength; + } + + DEBUG && + logger.log( + " Path is now", + this.entityPath.length, + "entities and has a length of", + this.totalLength + ); + + // Now, update the distance of our last item + if (this.items.length !== 0) { + const lastItem = this.items[this.items.length - 1]; + lastItem[_nextDistance] += otherPath.spacingToFirstItem; + DEBUG && + logger.log(" Add distance to last item, effectively being", lastItem[_nextDistance], "now"); + } else { + // Seems we have no items, update our first item distance + this.spacingToFirstItem = oldLength + otherPath.spacingToFirstItem; + DEBUG && + logger.log( + " We had no items, so our new spacing to first is old length (", + oldLength, + ") plus others spacing to first (", + otherPath.spacingToFirstItem, + ") =", + this.spacingToFirstItem + ); + } + + DEBUG && logger.log(" Pushing", otherPath.items.length, "items from other path"); + + // Aaand push the other paths items + for (let i = 0; i < otherPath.items.length; ++i) { + const item = otherPath.items[i]; + this.items.push([item[_nextDistance], item[_item]]); + } + + // Update bounds + this.worldBounds = this.computeBounds(); + + this.onPathChanged(); + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("extend-by-path"); + } + } + + /** + * Computes the total length of the path + * @returns {number} + */ + computeTotalLength() { + let length = 0; + for (let i = 0; i < this.entityPath.length; ++i) { + const entity = this.entityPath[i]; + length += entity.components.Belt.getEffectiveLengthTiles(); + } + return length; + } + + /** + * Performs one tick + */ + update() { + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("pre-update"); + } + + // Divide by item spacing on belts since we use throughput and not speed + let beltSpeed = + this.root.hubGoals.getBeltBaseSpeed() * + this.root.dynamicTickrate.deltaSeconds * + globalConfig.itemSpacingOnBelts; + + if (G_IS_DEV && globalConfig.debug.instantBelts) { + beltSpeed *= 100; + } + + // Store whether this is the first item we processed, so premature + // item ejection is available + let isFirstItemProcessed = true; + + // 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]; + + // Compute how much spacing we need at least + const minimumSpacing = i === this.items.length - 1 ? 0 : globalConfig.itemSpacingOnBelts; + + // Compute how much we can advance + const clampedProgress = Math.max( + 0, + Math.min(remainingVelocity, nextDistanceAndItem[_nextDistance] - minimumSpacing) + ); + + // Reduce our velocity by the amount we consumed + remainingVelocity -= clampedProgress; + + // Reduce the spacing + nextDistanceAndItem[_nextDistance] -= clampedProgress; + + // If the last item can be ejected, eject it and reduce the spacing, because otherwise + // we lose velocity + if (isFirstItemProcessed && nextDistanceAndItem[_nextDistance] < 1e-7) { + // Store how much velocity we "lost" because we bumped the item to the end of the + // belt but couldn't move it any farther. We need this to tell the item acceptor + // animation to start a tad later, so everything matches up. Yes I'm a perfectionist. + const excessVelocity = beltSpeed - clampedProgress; + + // Try to directly get rid of the item + if (this.tryHandOverItem(nextDistanceAndItem[_item], excessVelocity)) { + this.items.pop(); + } + } + + isFirstItemProcessed = false; + this.spacingToFirstItem += clampedProgress; + if (remainingVelocity < 0.01) { + break; + } + } + + // 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(); + } + } + + if (G_IS_DEV && globalConfig.debug.checkBeltPaths) { + this.debug_checkIntegrity("post-update"); + } + } + + /** + * Tries to hand over the item to the end entity + * @param {BaseItem} item + */ + tryHandOverItem(item, remainingProgress = 0.0) { + if (!this.acceptorTarget) { + return; + } + + const targetAcceptorComp = this.acceptorTarget.entity.components.ItemAcceptor; + + // Check if the acceptor has a filter for example + if (targetAcceptorComp && !targetAcceptorComp.canAcceptItem(this.acceptorTarget.slot, item)) { + // Well, this item is not accepted + return false; + } + + // Try to pass over + if ( + this.root.systemMgr.systems.itemEjector.tryPassOverItem( + item, + this.acceptorTarget.entity, + this.acceptorTarget.slot + ) + ) { + // 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 + ); + } + + return true; + } + + return false; + } + + /** + * Computes a world space position from the given progress + * @param {number} progress + * @returns {Vector} + */ + computePositionFromProgress(progress) { + let currentLength = 0; + + // floating point issuses .. + assert(progress <= this.totalLength + 0.02, "Progress too big: " + progress); + + for (let i = 0; i < this.entityPath.length; ++i) { + const beltComp = this.entityPath[i].components.Belt; + const localLength = beltComp.getEffectiveLengthTiles(); + + if (currentLength + localLength >= progress || i === this.entityPath.length - 1) { + // Min required here due to floating point issues + const localProgress = Math.min(1.0, progress - currentLength); + + assert(localProgress >= 0.0, "Invalid local progress: " + localProgress); + const localSpace = beltComp.transformBeltToLocalSpace(localProgress); + return this.entityPath[i].components.StaticMapEntity.localTileToWorld(localSpace); + } + currentLength += localLength; + } + + assert(false, "invalid progress: " + progress + " (max: " + this.totalLength + ")"); + } + + /** + * + * @param {DrawParameters} parameters + */ + drawDebug(parameters) { + if (!parameters.visibleRect.containsRect(this.worldBounds)) { + return; + } + + parameters.context.fillStyle = "#d79a25"; + parameters.context.strokeStyle = "#d79a25"; + parameters.context.beginPath(); + + for (let i = 0; i < this.entityPath.length; ++i) { + const entity = this.entityPath[i]; + const pos = entity.components.StaticMapEntity; + const worldPos = pos.origin.toWorldSpaceCenterOfTile(); + + if (i === 0) { + parameters.context.moveTo(worldPos.x, worldPos.y); + } else { + parameters.context.lineTo(worldPos.x, worldPos.y); + } + } + parameters.context.stroke(); + + // Items + let progress = this.spacingToFirstItem; + for (let i = 0; i < this.items.length; ++i) { + const nextDistanceAndItem = this.items[i]; + const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile(); + parameters.context.fillStyle = "#268e4d"; + parameters.context.beginRoundedRect(worldPos.x - 5, worldPos.y - 5, 10, 10, 3); + parameters.context.fill(); + parameters.context.font = "6px GameFont"; + parameters.context.fillStyle = "#111"; + parameters.context.fillText( + "" + round4Digits(nextDistanceAndItem[_nextDistance]), + worldPos.x + 5, + worldPos.y + 2 + ); + progress += nextDistanceAndItem[_nextDistance]; + } + + for (let i = 0; i < this.entityPath.length; ++i) { + const entity = this.entityPath[i]; + parameters.context.fillStyle = "#d79a25"; + const pos = entity.components.StaticMapEntity; + const worldPos = pos.origin.toWorldSpaceCenterOfTile(); + parameters.context.beginCircle(worldPos.x, worldPos.y, i === 0 ? 5 : 3); + parameters.context.fill(); + } + + for (let progress = 0; progress <= this.totalLength + 0.01; progress += 0.2) { + const worldPos = this.computePositionFromProgress(progress).toWorldSpaceCenterOfTile(); + parameters.context.fillStyle = "red"; + parameters.context.beginCircle(worldPos.x, worldPos.y, 1); + parameters.context.fill(); + } + + const firstItemIndicator = this.computePositionFromProgress( + this.spacingToFirstItem + ).toWorldSpaceCenterOfTile(); + parameters.context.fillStyle = "purple"; + parameters.context.fillRect(firstItemIndicator.x - 3, firstItemIndicator.y - 1, 6, 2); + } + + /** + * Draws the path + * @param {DrawParameters} parameters + */ + draw(parameters) { + if (!parameters.visibleRect.containsRect(this.worldBounds)) { + return; + } + + if (this.items.length === 0) { + // Early out + return; + } + + let currentItemPos = this.spacingToFirstItem; + let currentItemIndex = 0; + + let trackPos = 0.0; + + // Iterate whole track and check items + for (let i = 0; i < this.entityPath.length; ++i) { + const entity = this.entityPath[i]; + const beltComp = entity.components.Belt; + const beltLength = beltComp.getEffectiveLengthTiles(); + + // Check if the current items are on the belt + while (trackPos + beltLength >= currentItemPos - 1e-5) { + // Its on the belt, render it now + const staticComp = entity.components.StaticMapEntity; + assert( + currentItemPos - trackPos >= 0, + "invalid track pos: " + currentItemPos + " vs " + trackPos + " (l =" + beltLength + ")" + ); + + const localPos = beltComp.transformBeltToLocalSpace(currentItemPos - trackPos); + const worldPos = staticComp.localTileToWorld(localPos).toWorldSpaceCenterOfTile(); + + const distanceAndItem = this.items[currentItemIndex]; + + distanceAndItem[_item].drawItemCenteredClipped( + worldPos.x, + worldPos.y, + parameters, + globalConfig.defaultItemDiameter + ); + + // Check for the next item + currentItemPos += distanceAndItem[_nextDistance]; + ++currentItemIndex; + + if (currentItemIndex >= this.items.length) { + // We rendered all items + return; + } + } + + trackPos += beltLength; + } + } +} diff --git a/src/js/game/components/belt_reader.js b/src/js/game/components/belt_reader.js index 125572d6..d59feb9c 100644 --- a/src/js/game/components/belt_reader.js +++ b/src/js/game/components/belt_reader.js @@ -1,5 +1,7 @@ import { Component } from "../component"; import { BaseItem } from "../base_item"; +import { typeItemSingleton } from "../item_resolver"; +import { types } from "../../savegame/serialization"; export class BeltReaderComponent extends Component { static getId() { @@ -10,6 +12,12 @@ export class BeltReaderComponent extends Component { return new BeltReaderComponent(); } + static getSchema() { + return { + lastItem: types.nullable(typeItemSingleton), + }; + } + constructor() { super(); diff --git a/src/js/game/components/item_acceptor.js b/src/js/game/components/item_acceptor.js index ebd7ae5f..3885eb1f 100644 --- a/src/js/game/components/item_acceptor.js +++ b/src/js/game/components/item_acceptor.js @@ -1,144 +1,150 @@ -import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector"; -import { types } from "../../savegame/serialization"; -import { BaseItem } from "../base_item"; -import { Component } from "../component"; - -/** @typedef {{ - * pos: Vector, - * directions: enumDirection[], - * filter?: ItemType - * }} ItemAcceptorSlot */ - -/** - * Contains information about a slot plus its location - * @typedef {{ - * slot: ItemAcceptorSlot, - * index: number, - * acceptedDirection: enumDirection - * }} ItemAcceptorLocatedSlot */ - -/** @typedef {{ - * pos: Vector, - * directions: enumDirection[], - * filter?: ItemType - * }} ItemAcceptorSlotConfig */ - -export class ItemAcceptorComponent extends Component { - static getId() { - 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 - * @param {Array} param0.slots The slots from which we accept items - */ - constructor({ slots = [] }) { - super(); - - /** - * Fixes belt animations - * @type {Array<{ item: BaseItem, slotIndex: number, animProgress: number, direction: enumDirection }>} - */ - this.itemConsumptionAnimations = []; - - this.setSlots(slots); - } - - /** - * - * @param {Array} slots - */ - setSlots(slots) { - /** @type {Array} */ - this.slots = []; - for (let i = 0; i < slots.length; ++i) { - const slot = slots[i]; - this.slots.push({ - pos: slot.pos, - directions: slot.directions, - - // Which type of item to accept (shape | color | all) @see ItemType - filter: slot.filter, - }); - } - } - - /** - * Returns if this acceptor can accept a new item at slot N - * @param {number} slotIndex - * @param {BaseItem=} item - */ - canAcceptItem(slotIndex, item) { - const slot = this.slots[slotIndex]; - return !slot.filter || slot.filter === item.getItemType(); - } - - /** - * Called when an item has been accepted so that - * @param {number} slotIndex - * @param {enumDirection} direction - * @param {BaseItem} item - */ - onItemAccepted(slotIndex, direction, item) { - this.itemConsumptionAnimations.push({ - item, - slotIndex, - direction, - animProgress: 0.0, - }); - } - - /** - * Tries to find a slot which accepts the current item - * @param {Vector} targetLocalTile - * @param {enumDirection} fromLocalDirection - * @returns {ItemAcceptorLocatedSlot|null} - */ - findMatchingSlot(targetLocalTile, fromLocalDirection) { - // We need to invert our direction since the acceptor specifies *from* which direction - // it accepts items, but the ejector specifies *into* which direction it ejects items. - // E.g.: Ejector ejects into "right" direction but acceptor accepts from "left" direction. - const desiredDirection = enumInvertedDirections[fromLocalDirection]; - - // Go over all slots and try to find a target slot - for (let slotIndex = 0; slotIndex < this.slots.length; ++slotIndex) { - const slot = this.slots[slotIndex]; - - // Make sure the acceptor slot is on the right position - if (!slot.pos.equals(targetLocalTile)) { - continue; - } - - // Check if the acceptor slot accepts items from our direction - for (let i = 0; i < slot.directions.length; ++i) { - // const localDirection = targetStaticComp.localDirectionToWorld(slot.directions[l]); - if (desiredDirection === slot.directions[i]) { - return { - slot, - index: slotIndex, - acceptedDirection: desiredDirection, - }; - } - } - } - - return null; - } -} +import { enumDirection, enumInvertedDirections, Vector } from "../../core/vector"; +import { types } from "../../savegame/serialization"; +import { BaseItem } from "../base_item"; +import { Component } from "../component"; + +/** @typedef {{ + * pos: Vector, + * directions: enumDirection[], + * filter?: ItemType + * }} ItemAcceptorSlot */ + +/** + * Contains information about a slot plus its location + * @typedef {{ + * slot: ItemAcceptorSlot, + * index: number, + * acceptedDirection: enumDirection + * }} ItemAcceptorLocatedSlot */ + +/** @typedef {{ + * pos: Vector, + * directions: enumDirection[], + * filter?: ItemType + * }} ItemAcceptorSlotConfig */ + +export class ItemAcceptorComponent extends Component { + static getId() { + 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 + * @param {Array} param0.slots The slots from which we accept items + */ + constructor({ slots = [] }) { + super(); + + /** + * Fixes belt animations + * @type {Array<{ + * item: BaseItem, + * slotIndex: number, + * animProgress: number, + * direction: enumDirection + * }>} + */ + this.itemConsumptionAnimations = []; + + this.setSlots(slots); + } + + /** + * + * @param {Array} slots + */ + setSlots(slots) { + /** @type {Array} */ + this.slots = []; + for (let i = 0; i < slots.length; ++i) { + const slot = slots[i]; + this.slots.push({ + pos: slot.pos, + directions: slot.directions, + + // Which type of item to accept (shape | color | all) @see ItemType + filter: slot.filter, + }); + } + } + + /** + * Returns if this acceptor can accept a new item at slot N + * @param {number} slotIndex + * @param {BaseItem=} item + */ + canAcceptItem(slotIndex, item) { + const slot = this.slots[slotIndex]; + return !slot.filter || slot.filter === item.getItemType(); + } + + /** + * Called when an item has been accepted so that + * @param {number} slotIndex + * @param {enumDirection} direction + * @param {BaseItem} item + * @param {number} remainingProgress World space remaining progress, can be set to set the start position of the item + */ + onItemAccepted(slotIndex, direction, item, remainingProgress = 0.0) { + this.itemConsumptionAnimations.push({ + item, + slotIndex, + direction, + animProgress: Math.min(1, remainingProgress * 2), + }); + } + + /** + * Tries to find a slot which accepts the current item + * @param {Vector} targetLocalTile + * @param {enumDirection} fromLocalDirection + * @returns {ItemAcceptorLocatedSlot|null} + */ + findMatchingSlot(targetLocalTile, fromLocalDirection) { + // We need to invert our direction since the acceptor specifies *from* which direction + // it accepts items, but the ejector specifies *into* which direction it ejects items. + // E.g.: Ejector ejects into "right" direction but acceptor accepts from "left" direction. + const desiredDirection = enumInvertedDirections[fromLocalDirection]; + + // Go over all slots and try to find a target slot + for (let slotIndex = 0; slotIndex < this.slots.length; ++slotIndex) { + const slot = this.slots[slotIndex]; + + // Make sure the acceptor slot is on the right position + if (!slot.pos.equals(targetLocalTile)) { + continue; + } + + // Check if the acceptor slot accepts items from our direction + for (let i = 0; i < slot.directions.length; ++i) { + // const localDirection = targetStaticComp.localDirectionToWorld(slot.directions[l]); + if (desiredDirection === slot.directions[i]) { + return { + slot, + index: slotIndex, + acceptedDirection: desiredDirection, + }; + } + } + } + + return null; + } +} diff --git a/src/js/game/components/item_processor.js b/src/js/game/components/item_processor.js index c413fd8e..5d51b4a3 100644 --- a/src/js/game/components/item_processor.js +++ b/src/js/game/components/item_processor.js @@ -29,6 +29,17 @@ export const enumItemProcessorRequirements = { filter: "filter", }; +/** @typedef {{ + * item: BaseItem, + * requiredSlot?: number, + * preferredSlot?: number + * }} EjectorItemToEject */ + +/** @typedef {{ + * remainingTime: number, + * items: Array, + * }} EjectorCharge */ + export class ItemProcessorComponent extends Component { static getId() { return "ItemProcessor"; @@ -37,20 +48,6 @@ export class ItemProcessorComponent extends Component { static getSchema() { return { nextOutputSlot: types.uint, - inputSlots: types.array( - types.structured({ - item: typeItemSingleton, - sourceSlot: types.uint, - }) - ), - itemsToEject: types.array( - types.structured({ - item: typeItemSingleton, - requiredSlot: types.nullable(types.uint), - preferredSlot: types.nullable(types.uint), - }) - ), - secondsUntilEject: types.float, }; } @@ -101,21 +98,15 @@ export class ItemProcessorComponent extends Component { * What we are currently processing, empty if we don't produce anything rn * requiredSlot: Item *must* be ejected on this slot * preferredSlot: Item *can* be ejected on this slot, but others are fine too if the one is not usable - * @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>} + * @type {Array} */ - this.itemsToEject = []; - - /** - * How long it takes until we are done with the current items - * @type {number} - */ - this.secondsUntilEject = 0; + this.ongoingCharges = []; /** * How much processing time we have left from the last tick * @type {number} */ - this.bonusFromLastTick = 0; + this.bonusTime = 0; } /** diff --git a/src/js/game/core.js b/src/js/game/core.js index 54c0f3e9..642d8d9d 100644 --- a/src/js/game/core.js +++ b/src/js/game/core.js @@ -125,6 +125,24 @@ export class GameCore { // @ts-ignore window.globalRoot = root; } + + // @todo Find better place + if (G_IS_DEV && globalConfig.debug.manualTickOnly) { + this.root.gameState.inputReciever.keydown.add(key => { + if (key.keyCode === 84) { + // 'T' + + // Extract current real time + this.root.time.updateRealtimeNow(); + + // Perform logic ticks + this.root.time.performTicks(this.root.dynamicTickrate.deltaMs, this.boundInternalTick); + + // Update analytics + root.productionAnalytics.update(); + } + }); + } } /** @@ -244,11 +262,13 @@ export class GameCore { // Camera is always updated, no matter what root.camera.update(deltaMs); - // Perform logic ticks - this.root.time.performTicks(deltaMs, this.boundInternalTick); + if (!(G_IS_DEV && globalConfig.debug.manualTickOnly)) { + // Perform logic ticks + this.root.time.performTicks(deltaMs, this.boundInternalTick); - // Update analytics - root.productionAnalytics.update(); + // Update analytics + root.productionAnalytics.update(); + } // Update automatic save after everything finished root.automaticSave.update(); diff --git a/src/js/game/dynamic_tickrate.js b/src/js/game/dynamic_tickrate.js index a5033acf..3e29aba3 100644 --- a/src/js/game/dynamic_tickrate.js +++ b/src/js/game/dynamic_tickrate.js @@ -1,124 +1,125 @@ -import { GameRoot } from "./root"; -import { createLogger } from "../core/logging"; -import { globalConfig } from "../core/config"; - -const logger = createLogger("dynamic_tickrate"); - -const fpsAccumulationTime = 1000; - -export class DynamicTickrate { - /** - * - * @param {GameRoot} root - */ - constructor(root) { - this.root = root; - - this.currentTickStart = null; - this.capturedTicks = []; - this.averageTickDuration = 0; - - this.accumulatedFps = 0; - this.accumulatedFpsLastUpdate = 0; - - this.averageFps = 60; - - this.setTickRate(60); - - if (G_IS_DEV && globalConfig.debug.renderForTrailer) { - this.setTickRate(300); - } - } - - onFrameRendered() { - ++this.accumulatedFps; - - const now = performance.now(); - const timeDuration = now - this.accumulatedFpsLastUpdate; - if (timeDuration > fpsAccumulationTime) { - const avgFps = (this.accumulatedFps / fpsAccumulationTime) * 1000; - this.averageFps = avgFps; - this.accumulatedFps = 0; - this.accumulatedFpsLastUpdate = now; - } - } - - /** - * Sets the tick rate to N updates per second - * @param {number} rate - */ - setTickRate(rate) { - logger.log("Applying tick-rate of", rate); - this.currentTickRate = rate; - this.deltaMs = 1000.0 / this.currentTickRate; - this.deltaSeconds = 1.0 / this.currentTickRate; - } - - /** - * Increases the tick rate marginally - */ - increaseTickRate() { - if (G_IS_DEV && globalConfig.debug.renderForTrailer) { - return; - } - - const desiredFps = this.root.app.settings.getDesiredFps(); - this.setTickRate(Math.round(Math.min(desiredFps, this.currentTickRate * 1.2))); - } - - /** - * Decreases the tick rate marginally - */ - decreaseTickRate() { - if (G_IS_DEV && globalConfig.debug.renderForTrailer) { - return; - } - - const desiredFps = this.root.app.settings.getDesiredFps(); - this.setTickRate(Math.round(Math.max(desiredFps / 2, this.currentTickRate * 0.8))); - } - - /** - * Call whenever a tick began - */ - beginTick() { - assert(this.currentTickStart === null, "BeginTick called twice"); - this.currentTickStart = performance.now(); - - if (this.capturedTicks.length > this.currentTickRate * 2) { - // Take only a portion of the ticks - this.capturedTicks.sort(); - this.capturedTicks.splice(0, 10); - this.capturedTicks.splice(this.capturedTicks.length - 11, 10); - - let average = 0; - for (let i = 0; i < this.capturedTicks.length; ++i) { - average += this.capturedTicks[i]; - } - average /= this.capturedTicks.length; - - this.averageTickDuration = average; - - const desiredFps = this.root.app.settings.getDesiredFps(); - - if (this.averageFps > desiredFps * 0.9) { - // if (average < maxTickDuration) { - this.increaseTickRate(); - } else if (this.averageFps < desiredFps * 0.7) { - this.decreaseTickRate(); - } - - this.capturedTicks = []; - } - } - - /** - * Call whenever a tick ended - */ - endTick() { - assert(this.currentTickStart !== null, "EndTick called without BeginTick"); - const duration = performance.now() - this.currentTickStart; - this.capturedTicks.push(duration); - this.currentTickStart = null; - } -} +import { GameRoot } from "./root"; +import { createLogger } from "../core/logging"; +import { globalConfig } from "../core/config"; + +const logger = createLogger("dynamic_tickrate"); + +const fpsAccumulationTime = 1000; + +export class DynamicTickrate { + /** + * + * @param {GameRoot} root + */ + constructor(root) { + this.root = root; + + this.currentTickStart = null; + this.capturedTicks = []; + this.averageTickDuration = 0; + + this.accumulatedFps = 0; + this.accumulatedFpsLastUpdate = 0; + + this.averageFps = 60; + + this.setTickRate(this.root.app.settings.getDesiredFps()); + + if (G_IS_DEV && globalConfig.debug.renderForTrailer) { + this.setTickRate(300); + } + } + + onFrameRendered() { + ++this.accumulatedFps; + + const now = performance.now(); + const timeDuration = now - this.accumulatedFpsLastUpdate; + if (timeDuration > fpsAccumulationTime) { + const avgFps = (this.accumulatedFps / fpsAccumulationTime) * 1000; + this.averageFps = avgFps; + this.accumulatedFps = 0; + this.accumulatedFpsLastUpdate = now; + } + } + + /** + * Sets the tick rate to N updates per second + * @param {number} rate + */ + setTickRate(rate) { + logger.log("Applying tick-rate of", rate); + this.currentTickRate = rate; + this.deltaMs = 1000.0 / this.currentTickRate; + this.deltaSeconds = 1.0 / this.currentTickRate; + } + + /** + * Increases the tick rate marginally + */ + increaseTickRate() { + if (G_IS_DEV && globalConfig.debug.renderForTrailer) { + return; + } + + const desiredFps = this.root.app.settings.getDesiredFps(); + this.setTickRate(Math.round(Math.min(desiredFps, this.currentTickRate * 1.2))); + } + + /** + * Decreases the tick rate marginally + */ + decreaseTickRate() { + if (G_IS_DEV && globalConfig.debug.renderForTrailer) { + return; + } + + const desiredFps = this.root.app.settings.getDesiredFps(); + this.setTickRate(Math.round(Math.max(desiredFps / 2, this.currentTickRate * 0.8))); + } + + /** + * Call whenever a tick began + */ + beginTick() { + assert(this.currentTickStart === null, "BeginTick called twice"); + this.currentTickStart = performance.now(); + + if (this.capturedTicks.length > this.currentTickRate * 2) { + // Take only a portion of the ticks + this.capturedTicks.sort(); + this.capturedTicks.splice(0, 10); + this.capturedTicks.splice(this.capturedTicks.length - 11, 10); + + let average = 0; + for (let i = 0; i < this.capturedTicks.length; ++i) { + average += this.capturedTicks[i]; + } + average /= this.capturedTicks.length; + + this.averageTickDuration = average; + + const desiredFps = this.root.app.settings.getDesiredFps(); + + // Disabled for now: Dynamicall adjusting tick rate + // if (this.averageFps > desiredFps * 0.9) { + // // if (average < maxTickDuration) { + // this.increaseTickRate(); + // } else if (this.averageFps < desiredFps * 0.7) { + // this.decreaseTickRate(); + // } + + this.capturedTicks = []; + } + } + + /** + * Call whenever a tick ended + */ + endTick() { + assert(this.currentTickStart !== null, "EndTick called without BeginTick"); + const duration = performance.now() - this.currentTickStart; + this.capturedTicks.push(duration); + this.currentTickStart = null; + } +} diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index bcbb7ccf..b0ae46f2 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -110,6 +110,10 @@ export class GameSystemManager { // Order is important! + // IMPORTANT: Item acceptor must be before the belt, because it may not tick after the belt + // has put in the item into the acceptor animation, otherwise its off + add("itemAcceptor", ItemAcceptorSystem); + add("belt", BeltSystem); add("undergroundBelt", UndergroundBeltSystem); @@ -134,11 +138,6 @@ export class GameSystemManager { add("constantSignal", ConstantSignalSystem); - // IMPORTANT: Must be after belt system since belt system can change the - // orientation of an entity after it is placed -> the item acceptor cache - // then would be invalid - add("itemAcceptor", ItemAcceptorSystem); - // WIRES section add("lever", LeverSystem); diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index b86a9552..ffe9b414 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -454,6 +454,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { const ejectorComp = this.fakeEntity.components.ItemEjector; const staticComp = this.fakeEntity.components.StaticMapEntity; const beltComp = this.fakeEntity.components.Belt; + const minerComp = this.fakeEntity.components.Miner; const goodArrowSprite = Loader.getSprite("sprites/misc/slot_good_arrow.png"); const badArrowSprite = Loader.getSprite("sprites/misc/slot_bad_arrow.png"); @@ -571,6 +572,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { const destEntity = destEntities[i]; const destAcceptor = destEntity.components.ItemAcceptor; const destStaticComp = destEntity.components.StaticMapEntity; + const destMiner = destEntity.components.Miner; const destLocalTile = destStaticComp.worldToLocalTile(ejectorSlotWsTile); const destLocalDir = destStaticComp.worldDirectionToLocal(ejectorSlotWsDirection); @@ -580,6 +582,9 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { } else if (destEntity.components.Belt && destLocalDir === enumDirection.top) { // Connected to a belt isConnected = true; + } else if (minerComp && minerComp.chainable && destMiner && destMiner.chainable) { + // Chainable miners connected to eachother + isConnected = true; } else { // This one is blocked isBlocked = true; diff --git a/src/js/game/systems/item_acceptor.js b/src/js/game/systems/item_acceptor.js index 745da4bf..6d6fec77 100644 --- a/src/js/game/systems/item_acceptor.js +++ b/src/js/game/systems/item_acceptor.js @@ -1,82 +1,80 @@ -import { globalConfig } from "../../core/config"; -import { DrawParameters } from "../../core/draw_parameters"; -import { fastArrayDelete } from "../../core/utils"; -import { enumDirectionToVector } from "../../core/vector"; -import { ItemAcceptorComponent } from "../components/item_acceptor"; -import { GameSystemWithFilter } from "../game_system_with_filter"; -import { MapChunkView } from "../map_chunk_view"; - -export class ItemAcceptorSystem extends GameSystemWithFilter { - constructor(root) { - super(root, [ItemAcceptorComponent]); - } - - update() { - const progress = this.root.dynamicTickrate.deltaSeconds * 2; // * 2 because its only a half tile - - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; - const aceptorComp = entity.components.ItemAcceptor; - const animations = aceptorComp.itemConsumptionAnimations; - - // Process item consumption animations to avoid items popping from the belts - for (let animIndex = 0; animIndex < animations.length; ++animIndex) { - const anim = animations[animIndex]; - anim.animProgress += - progress * this.root.hubGoals.getBeltBaseSpeed() * globalConfig.itemSpacingOnBelts; - if (anim.animProgress > 1) { - // Original - // animations.splice(animIndex, 1); - - // Faster variant - fastArrayDelete(animations, animIndex); - - animIndex -= 1; - } - } - } - } - - /** - * @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 acceptorComp = entity.components.ItemAcceptor; - if (!acceptorComp) { - continue; - } - - const staticComp = entity.components.StaticMapEntity; - for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { - const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ - animIndex - ]; - - const slotData = acceptorComp.slots[slotIndex]; - const realSlotPos = staticComp.localTileToWorld(slotData.pos); - - if (!chunk.tileSpaceRectangle.containsPoint(realSlotPos.x, realSlotPos.y)) { - // Not within this chunk - continue; - } - - const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)]; - const finalTile = realSlotPos.subScalars( - fadeOutDirection.x * (animProgress / 2 - 0.5), - fadeOutDirection.y * (animProgress / 2 - 0.5) - ); - - item.drawItemCenteredClipped( - (finalTile.x + 0.5) * globalConfig.tileSize, - (finalTile.y + 0.5) * globalConfig.tileSize, - parameters, - globalConfig.defaultItemDiameter - ); - } - } - } -} +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { fastArrayDelete } from "../../core/utils"; +import { enumDirectionToVector } from "../../core/vector"; +import { ItemAcceptorComponent } from "../components/item_acceptor"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { MapChunkView } from "../map_chunk_view"; + +export class ItemAcceptorSystem extends GameSystemWithFilter { + constructor(root) { + super(root, [ItemAcceptorComponent]); + } + + update() { + const progress = + this.root.dynamicTickrate.deltaSeconds * + 2 * + this.root.hubGoals.getBeltBaseSpeed() * + globalConfig.itemSpacingOnBelts; // * 2 because its only a half tile + + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + const aceptorComp = entity.components.ItemAcceptor; + const animations = aceptorComp.itemConsumptionAnimations; + + // Process item consumption animations to avoid items popping from the belts + for (let animIndex = 0; animIndex < animations.length; ++animIndex) { + const anim = animations[animIndex]; + anim.animProgress += progress; + if (anim.animProgress > 1) { + fastArrayDelete(animations, animIndex); + animIndex -= 1; + } + } + } + } + + /** + * @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 acceptorComp = entity.components.ItemAcceptor; + if (!acceptorComp) { + continue; + } + + const staticComp = entity.components.StaticMapEntity; + for (let animIndex = 0; animIndex < acceptorComp.itemConsumptionAnimations.length; ++animIndex) { + const { item, slotIndex, animProgress, direction } = acceptorComp.itemConsumptionAnimations[ + animIndex + ]; + + const slotData = acceptorComp.slots[slotIndex]; + const realSlotPos = staticComp.localTileToWorld(slotData.pos); + + if (!chunk.tileSpaceRectangle.containsPoint(realSlotPos.x, realSlotPos.y)) { + // Not within this chunk + continue; + } + + const fadeOutDirection = enumDirectionToVector[staticComp.localDirectionToWorld(direction)]; + const finalTile = realSlotPos.subScalars( + fadeOutDirection.x * (animProgress / 2 - 0.5), + fadeOutDirection.y * (animProgress / 2 - 0.5) + ); + + item.drawItemCenteredClipped( + (finalTile.x + 0.5) * globalConfig.tileSize, + (finalTile.y + 0.5) * globalConfig.tileSize, + parameters, + globalConfig.defaultItemDiameter + ); + } + } + } +} diff --git a/src/js/game/systems/item_processor.js b/src/js/game/systems/item_processor.js index 1640f6ed..d58aa697 100644 --- a/src/js/game/systems/item_processor.js +++ b/src/js/game/systems/item_processor.js @@ -1,4 +1,3 @@ -import { globalConfig } from "../../core/config"; import { BaseItem } from "../base_item"; import { enumColorMixingResults, enumColors } from "../colors"; import { @@ -12,6 +11,11 @@ import { BOOL_TRUE_SINGLETON, isTruthyItem } from "../items/boolean_item"; import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item"; import { ShapeItem } from "../items/shape_item"; +/** + * We need to allow queuing charges, otherwise the throughput will stall + */ +const MAX_QUEUED_CHARGES = 2; + export class ItemProcessorSystem extends GameSystemWithFilter { constructor(root) { super(root, [ItemProcessorComponent]); @@ -24,60 +28,64 @@ export class ItemProcessorSystem extends GameSystemWithFilter { const processorComp = entity.components.ItemProcessor; const ejectorComp = entity.components.ItemEjector; - // First of all, process the current recipe - const newSecondsUntilEject = - processorComp.secondsUntilEject - this.root.dynamicTickrate.deltaSeconds; + const currentCharge = processorComp.ongoingCharges[0]; - processorComp.secondsUntilEject = Math.max(0, newSecondsUntilEject); + if (currentCharge) { + // Process next charge + if (currentCharge.remainingTime > 0.0) { + currentCharge.remainingTime -= this.root.dynamicTickrate.deltaSeconds; + if (currentCharge.remainingTime < 0.0) { + // Add bonus time, this is the time we spent too much + processorComp.bonusTime += -currentCharge.remainingTime; + } + } - if (newSecondsUntilEject < 0) { - processorComp.bonusFromLastTick -= newSecondsUntilEject; - } + // Check if it finished + if (currentCharge.remainingTime <= 0.0) { + const itemsToEject = currentCharge.items; - if (G_IS_DEV && globalConfig.debug.instantProcessors) { - processorComp.secondsUntilEject = 0; - } + // Go over all items and try to eject them + for (let j = 0; j < itemsToEject.length; ++j) { + const { item, requiredSlot, preferredSlot } = itemsToEject[j]; - // Check if we have any finished items we can eject - if ( - processorComp.secondsUntilEject === 0 && // it was processed in time - processorComp.itemsToEject.length > 0 // we have some items left to eject - ) { - for (let itemIndex = 0; itemIndex < processorComp.itemsToEject.length; ++itemIndex) { - const { item, requiredSlot, preferredSlot } = processorComp.itemsToEject[itemIndex]; - - let slot = null; - if (requiredSlot !== null && requiredSlot !== undefined) { - // We have a slot override, check if that is free - if (ejectorComp.canEjectOnSlot(requiredSlot)) { - slot = requiredSlot; - } - } else if (preferredSlot !== null && preferredSlot !== undefined) { - // We have a slot preference, try using it but otherwise use a free slot - if (ejectorComp.canEjectOnSlot(preferredSlot)) { - slot = preferredSlot; + let slot = null; + if (requiredSlot !== null && requiredSlot !== undefined) { + // We have a slot override, check if that is free + if (ejectorComp.canEjectOnSlot(requiredSlot)) { + slot = requiredSlot; + } + } else if (preferredSlot !== null && preferredSlot !== undefined) { + // We have a slot preference, try using it but otherwise use a free slot + if (ejectorComp.canEjectOnSlot(preferredSlot)) { + slot = preferredSlot; + } else { + slot = ejectorComp.getFirstFreeSlot(); + } } else { + // We can eject on any slot slot = ejectorComp.getFirstFreeSlot(); } - } else { - // We can eject on any slot - slot = ejectorComp.getFirstFreeSlot(); + + if (slot !== null) { + // Alright, we can actually eject + if (!ejectorComp.tryEject(slot, item)) { + assert(false, "Failed to eject"); + } else { + itemsToEject.splice(j, 1); + j -= 1; + } + } } - if (slot !== null) { - // Alright, we can actually eject - if (!ejectorComp.tryEject(slot, item)) { - assert(false, "Failed to eject"); - } else { - processorComp.itemsToEject.splice(itemIndex, 1); - itemIndex -= 1; - } + // If the charge was entirely emptied to the outputs, start the next charge + if (itemsToEject.length === 0) { + processorComp.ongoingCharges.shift(); } } } // Check if we have an empty queue and can start a new charge - if (processorComp.itemsToEject.length === 0) { + if (processorComp.ongoingCharges.length < MAX_QUEUED_CHARGES) { if (this.canProcess(entity)) { this.startNewCharge(entity); } @@ -236,12 +244,6 @@ export class ItemProcessorSystem extends GameSystemWithFilter { itemsBySlot[items[i].sourceSlot] = items[i]; } - const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type); - - // Substract one tick because we already process it this frame - processorComp.secondsUntilEject = Math.max(0, 1 / baseSpeed - processorComp.bonusFromLastTick); - processorComp.bonusFromLastTick = 0; - /** @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>} */ const outItems = []; @@ -544,6 +546,35 @@ export class ItemProcessorSystem extends GameSystemWithFilter { } } - processorComp.itemsToEject = outItems; + // Queue Charge + const baseSpeed = this.root.hubGoals.getProcessorBaseSpeed(processorComp.type); + const originalTime = 1 / baseSpeed; + + 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, + }); } } diff --git a/src/js/game/systems/item_processor_overlays.js b/src/js/game/systems/item_processor_overlays.js index 8a3be8ff..2ec91b88 100644 --- a/src/js/game/systems/item_processor_overlays.js +++ b/src/js/game/systems/item_processor_overlays.js @@ -88,10 +88,7 @@ export class ItemProcessorOverlaysSystem extends GameSystem { parameters.context.textAlign = "center"; parameters.context.font = "bold 10px GameFont"; parameters.context.fillText( - "" + - (G_IS_DEV - ? round4Digits(readerComp.lastThroughput) - : Math.round(readerComp.lastThroughput * 10) / 10), + "" + Math.round(readerComp.lastThroughput * 10) / 10, (staticComp.origin.x + 0.5) * globalConfig.tileSize, (staticComp.origin.y + 0.62) * globalConfig.tileSize ); diff --git a/src/js/game/systems/logic_gate.js b/src/js/game/systems/logic_gate.js index a56ffa18..1b04bf9a 100644 --- a/src/js/game/systems/logic_gate.js +++ b/src/js/game/systems/logic_gate.js @@ -35,18 +35,40 @@ export class LogicGateSystem extends GameSystemWithFilter { const slotValues = []; + // Store if any conflict was found + let anyConflict = false; + + // Gather inputs from all connected networks for (let i = 0; i < slotComp.slots.length; ++i) { const slot = slotComp.slots[i]; if (slot.type !== enumPinSlotType.logicalAcceptor) { continue; } if (slot.linkedNetwork) { + if (slot.linkedNetwork.valueConflict) { + anyConflict = true; + break; + } + slotValues.push(slot.linkedNetwork.currentValue); } else { slotValues.push(null); } } + // Handle conflicts + if (anyConflict) { + for (let i = 0; i < slotComp.slots.length; ++i) { + const slot = slotComp.slots[i]; + if (slot.type !== enumPinSlotType.logicalEjector) { + continue; + } + slot.value = null; + } + continue; + } + + // Compute actual result const result = this.boundOperations[logicComp.type](slotValues); if (Array.isArray(result)) { diff --git a/src/js/game/systems/static_map_entity.js b/src/js/game/systems/static_map_entity.js index da6575a5..3e891f7b 100644 --- a/src/js/game/systems/static_map_entity.js +++ b/src/js/game/systems/static_map_entity.js @@ -1,82 +1,82 @@ -import { globalConfig } from "../../core/config"; -import { DrawParameters } from "../../core/draw_parameters"; -import { GameSystem } from "../game_system"; -import { MapChunkView } from "../map_chunk_view"; - -export class StaticMapEntitySystem extends GameSystem { - constructor(root) { - super(root); - - /** @type {Set} */ - this.drawnUids = new Set(); - - this.root.signals.gameFrameStarted.add(this.clearUidList, this); - } - - /** - * Clears the uid list when a new frame started - */ - clearUidList() { - this.drawnUids.clear(); - } - - /** - * Draws the static entities - * @param {DrawParameters} parameters - * @param {MapChunkView} chunk - */ - drawChunk(parameters, chunk) { - if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) { - return; - } - - const contents = chunk.containedEntitiesByLayer.regular; - for (let i = 0; i < contents.length; ++i) { - const entity = contents[i]; - - const staticComp = entity.components.StaticMapEntity; - const sprite = staticComp.getSprite(); - if (sprite) { - // Avoid drawing an entity twice which has been drawn for - // another chunk already - if (this.drawnUids.has(entity.uid)) { - continue; - } - - this.drawnUids.add(entity.uid); - staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2); - } - } - } - - /** - * Draws the static wire entities - * @param {DrawParameters} parameters - * @param {MapChunkView} chunk - */ - drawWiresChunk(parameters, chunk) { - if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) { - return; - } - - const drawnUids = new Set(); - const contents = chunk.wireContents; - for (let y = 0; y < globalConfig.mapChunkSize; ++y) { - for (let x = 0; x < globalConfig.mapChunkSize; ++x) { - const entity = contents[x][y]; - if (entity) { - if (drawnUids.has(entity.uid)) { - continue; - } - drawnUids.add(entity.uid); - const staticComp = entity.components.StaticMapEntity; - - const sprite = staticComp.getSprite(); - if (sprite) { - staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2); - } - } - } - } - } -} +import { globalConfig } from "../../core/config"; +import { DrawParameters } from "../../core/draw_parameters"; +import { GameSystem } from "../game_system"; +import { MapChunkView } from "../map_chunk_view"; + +export class StaticMapEntitySystem extends GameSystem { + constructor(root) { + super(root); + + /** @type {Set} */ + this.drawnUids = new Set(); + + this.root.signals.gameFrameStarted.add(this.clearUidList, this); + } + + /** + * Clears the uid list when a new frame started + */ + clearUidList() { + this.drawnUids.clear(); + } + + /** + * Draws the static entities + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + */ + drawChunk(parameters, chunk) { + if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) { + return; + } + + const contents = chunk.containedEntitiesByLayer.regular; + for (let i = 0; i < contents.length; ++i) { + const entity = contents[i]; + + const staticComp = entity.components.StaticMapEntity; + const sprite = staticComp.getSprite(); + if (sprite) { + // Avoid drawing an entity twice which has been drawn for + // another chunk already + if (this.drawnUids.has(entity.uid)) { + continue; + } + + this.drawnUids.add(entity.uid); + staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2); + } + } + } + + /** + * Draws the static wire entities + * @param {DrawParameters} parameters + * @param {MapChunkView} chunk + */ + drawWiresChunk(parameters, chunk) { + if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) { + return; + } + + const drawnUids = new Set(); + const contents = chunk.wireContents; + for (let y = 0; y < globalConfig.mapChunkSize; ++y) { + for (let x = 0; x < globalConfig.mapChunkSize; ++x) { + const entity = contents[x][y]; + if (entity) { + if (drawnUids.has(entity.uid)) { + continue; + } + drawnUids.add(entity.uid); + const staticComp = entity.components.StaticMapEntity; + + const sprite = staticComp.getSprite(); + if (sprite) { + staticComp.drawSpriteOnBoundsClipped(parameters, sprite, 2); + } + } + } + } + } +} diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index 719ad062..ae9ffbf0 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -1,644 +1,644 @@ -/* typehints:start */ -import { Application } from "../application"; -/* typehints:end */ - -import { ReadWriteProxy } from "../core/read_write_proxy"; -import { BoolSetting, EnumSetting, RangeSetting, BaseSetting } from "./setting_types"; -import { createLogger } from "../core/logging"; -import { ExplainedResult } from "../core/explained_result"; -import { THEMES, THEME, applyGameTheme } from "../game/theme"; -import { IS_DEMO } from "../core/config"; -import { T } from "../translations"; -import { LANGUAGES } from "../languages"; - -const logger = createLogger("application_settings"); - -/** - * @enum {string} - */ -export const enumCategories = { - general: "general", - userInterface: "userInterface", - performance: "performance", - advanced: "advanced", -}; - -export const uiScales = [ - { - id: "super_small", - size: 0.6, - }, - { - id: "small", - size: 0.8, - }, - { - id: "regular", - size: 1, - }, - { - id: "large", - size: 1.05, - }, - { - id: "huge", - size: 1.1, - }, -]; - -export const scrollWheelSensitivities = [ - { - id: "super_slow", - scale: 0.25, - }, - { - id: "slow", - scale: 0.5, - }, - { - id: "regular", - scale: 1, - }, - { - id: "fast", - scale: 2, - }, - { - id: "super_fast", - scale: 4, - }, -]; - -export const movementSpeeds = [ - { - id: "super_slow", - multiplier: 0.25, - }, - { - id: "slow", - multiplier: 0.5, - }, - { - id: "regular", - multiplier: 1, - }, - { - id: "fast", - multiplier: 2, - }, - { - id: "super_fast", - multiplier: 4, - }, - { - id: "extremely_fast", - multiplier: 8, - }, -]; - -export const autosaveIntervals = [ - { - id: "one_minute", - seconds: 60, - }, - { - id: "two_minutes", - seconds: 120, - }, - { - id: "five_minutes", - seconds: 5 * 60, - }, - { - id: "ten_minutes", - seconds: 10 * 60, - }, - { - id: "twenty_minutes", - seconds: 20 * 60, - }, - { - id: "disabled", - seconds: null, - }, -]; - -const refreshRateOptions = ["60", "75", "100", "120", "144", "165", "250", "500"]; - -if (G_IS_DEV) { - refreshRateOptions.unshift("30"); - refreshRateOptions.unshift("10"); - refreshRateOptions.unshift("5"); - refreshRateOptions.push("1000"); - refreshRateOptions.push("2000"); - refreshRateOptions.push("5000"); - refreshRateOptions.push("10000"); -} - -/** @type {Array} */ -export const allApplicationSettings = [ - new EnumSetting("language", { - options: Object.keys(LANGUAGES), - valueGetter: key => key, - textGetter: key => LANGUAGES[key].name, - category: enumCategories.general, - restartRequired: true, - changeCb: (app, id) => null, - magicValue: "auto-detect", - }), - - new EnumSetting("uiScale", { - options: uiScales.sort((a, b) => a.size - b.size), - valueGetter: scale => scale.id, - textGetter: scale => T.settings.labels.uiScale.scales[scale.id], - category: enumCategories.userInterface, - restartRequired: false, - changeCb: - /** - * @param {Application} app - */ - (app, id) => app.updateAfterUiScaleChanged(), - }), - - new BoolSetting( - "soundsMuted", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => app.sound.setSoundsMuted(value) - ), - new RangeSetting( - "soundVolume", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => app.sound.setSoundVolume(value / 100.0) - ), - new BoolSetting( - "musicMuted", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => app.sound.setMusicMuted(value) - ), - new RangeSetting( - "musicVolume", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => app.sound.setMusicVolume(value / 100.0) - ), - - new BoolSetting( - "fullscreen", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => { - if (app.platformWrapper.getSupportsFullscreen()) { - app.platformWrapper.setFullscreen(value); - } - }, - !IS_DEMO - ), - - new BoolSetting( - "enableColorBlindHelper", - enumCategories.general, - /** - * @param {Application} app - */ - (app, value) => null - ), - - new BoolSetting("offerHints", enumCategories.userInterface, (app, value) => {}), - - new EnumSetting("theme", { - options: Object.keys(THEMES), - valueGetter: theme => theme, - textGetter: theme => T.settings.labels.theme.themes[theme], - category: enumCategories.userInterface, - restartRequired: false, - changeCb: - /** - * @param {Application} app - */ - (app, id) => { - applyGameTheme(id); - document.documentElement.setAttribute("data-theme", id); - }, - enabled: !IS_DEMO, - }), - - new EnumSetting("autosaveInterval", { - options: autosaveIntervals, - valueGetter: interval => interval.id, - textGetter: interval => T.settings.labels.autosaveInterval.intervals[interval.id], - category: enumCategories.advanced, - restartRequired: false, - changeCb: - /** - * @param {Application} app - */ - (app, id) => null, - }), - - new EnumSetting("scrollWheelSensitivity", { - options: scrollWheelSensitivities.sort((a, b) => a.scale - b.scale), - valueGetter: scale => scale.id, - textGetter: scale => T.settings.labels.scrollWheelSensitivity.sensitivity[scale.id], - category: enumCategories.advanced, - restartRequired: false, - changeCb: - /** - * @param {Application} app - */ - (app, id) => app.updateAfterUiScaleChanged(), - }), - - new EnumSetting("movementSpeed", { - options: movementSpeeds.sort((a, b) => a.multiplier - b.multiplier), - valueGetter: multiplier => multiplier.id, - textGetter: multiplier => T.settings.labels.movementSpeed.speeds[multiplier.id], - category: enumCategories.advanced, - restartRequired: false, - changeCb: (app, id) => {}, - }), - - new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}), - new BoolSetting("clearCursorOnDeleteWhilePlacing", enumCategories.advanced, (app, value) => {}), - new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}), - new BoolSetting("vignette", enumCategories.userInterface, (app, value) => {}), - new BoolSetting("compactBuildingInfo", enumCategories.userInterface, (app, value) => {}), - new BoolSetting("disableCutDeleteWarnings", enumCategories.advanced, (app, value) => {}), - new BoolSetting("rotationByBuilding", enumCategories.advanced, (app, value) => {}), - new BoolSetting("displayChunkBorders", enumCategories.advanced, (app, value) => {}), - - new EnumSetting("refreshRate", { - options: refreshRateOptions, - valueGetter: rate => rate, - textGetter: rate => rate + " Hz", - category: enumCategories.performance, - restartRequired: false, - changeCb: (app, id) => {}, - enabled: !IS_DEMO, - }), - - new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}), - new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}), - new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}), -]; - -export function getApplicationSettingById(id) { - return allApplicationSettings.find(setting => setting.id === id); -} - -class SettingsStorage { - constructor() { - this.uiScale = "regular"; - this.fullscreen = G_IS_STANDALONE; - - this.soundsMuted = false; - this.musicMuted = false; - this.soundVolume = 1.0; - this.musicVolume = 1.0; - - this.theme = "light"; - this.refreshRate = "60"; - this.scrollWheelSensitivity = "regular"; - this.movementSpeed = "regular"; - this.language = "auto-detect"; - this.autosaveInterval = "two_minutes"; - - this.alwaysMultiplace = false; - this.offerHints = true; - this.enableTunnelSmartplace = true; - this.vignette = true; - this.compactBuildingInfo = false; - this.disableCutDeleteWarnings = false; - this.rotationByBuilding = true; - this.clearCursorOnDeleteWhilePlacing = true; - this.displayChunkBorders = false; - - this.enableColorBlindHelper = false; - - this.lowQualityMapResources = false; - this.disableTileGrid = false; - this.lowQualityTextures = false; - - /** - * @type {Object.} - */ - this.keybindingOverrides = {}; - } -} - -export class ApplicationSettings extends ReadWriteProxy { - constructor(app) { - super(app, "app_settings.bin"); - } - - initialize() { - // Read and directly write latest data back - return this.readAsync() - .then(() => { - // Apply default setting callbacks - const settings = this.getAllSettings(); - for (let i = 0; i < allApplicationSettings.length; ++i) { - const handle = allApplicationSettings[i]; - handle.apply(this.app, settings[handle.id]); - } - }) - - .then(() => this.writeAsync()); - } - - save() { - return this.writeAsync(); - } - - // Getters - - /** - * @returns {SettingsStorage} - */ - getAllSettings() { - return this.getCurrentData().settings; - } - - /** - * @param {string} key - */ - getSetting(key) { - assert(this.getAllSettings().hasOwnProperty(key), "Setting not known: " + key); - return this.getAllSettings()[key]; - } - - getInterfaceScaleId() { - if (!this.currentData) { - // Not initialized yet - return "regular"; - } - return this.getAllSettings().uiScale; - } - - getDesiredFps() { - return parseInt(this.getAllSettings().refreshRate); - } - - getInterfaceScaleValue() { - const id = this.getInterfaceScaleId(); - for (let i = 0; i < uiScales.length; ++i) { - if (uiScales[i].id === id) { - return uiScales[i].size; - } - } - logger.error("Unknown ui scale id:", id); - return 1; - } - - getScrollWheelSensitivity() { - const id = this.getAllSettings().scrollWheelSensitivity; - for (let i = 0; i < scrollWheelSensitivities.length; ++i) { - if (scrollWheelSensitivities[i].id === id) { - return scrollWheelSensitivities[i].scale; - } - } - logger.error("Unknown scroll wheel sensitivity id:", id); - return 1; - } - - getMovementSpeed() { - const id = this.getAllSettings().movementSpeed; - for (let i = 0; i < movementSpeeds.length; ++i) { - if (movementSpeeds[i].id === id) { - return movementSpeeds[i].multiplier; - } - } - logger.error("Unknown movement speed id:", id); - return 1; - } - - getAutosaveIntervalSeconds() { - const id = this.getAllSettings().autosaveInterval; - for (let i = 0; i < autosaveIntervals.length; ++i) { - if (autosaveIntervals[i].id === id) { - return autosaveIntervals[i].seconds; - } - } - logger.error("Unknown autosave interval id:", id); - return 120; - } - - getIsFullScreen() { - return this.getAllSettings().fullscreen; - } - - getKeybindingOverrides() { - return this.getAllSettings().keybindingOverrides; - } - - getLanguage() { - return this.getAllSettings().language; - } - - // Setters - - updateLanguage(id) { - assert(LANGUAGES[id], "Language not known: " + id); - return this.updateSetting("language", id); - } - - /** - * @param {string} key - * @param {string|boolean|number} value - */ - updateSetting(key, value) { - for (let i = 0; i < allApplicationSettings.length; ++i) { - const setting = allApplicationSettings[i]; - if (setting.id === key) { - if (!setting.validate(value)) { - assertAlways(false, "Bad setting value: " + key); - } - this.getAllSettings()[key] = value; - if (setting.changeCb) { - setting.changeCb(this.app, value); - } - return this.writeAsync(); - } - } - assertAlways(false, "Unknown setting: " + key); - } - - /** - * Sets a new keybinding override - * @param {string} keybindingId - * @param {number} keyCode - */ - updateKeybindingOverride(keybindingId, keyCode) { - assert(Number.isInteger(keyCode), "Not a valid key code: " + keyCode); - this.getAllSettings().keybindingOverrides[keybindingId] = keyCode; - return this.writeAsync(); - } - - /** - * Resets a given keybinding override - * @param {string} id - */ - resetKeybindingOverride(id) { - delete this.getAllSettings().keybindingOverrides[id]; - return this.writeAsync(); - } - /** - * Resets all keybinding overrides - */ - resetKeybindingOverrides() { - this.getAllSettings().keybindingOverrides = {}; - return this.writeAsync(); - } - - // RW Proxy impl - verify(data) { - if (!data.settings) { - return ExplainedResult.bad("missing key 'settings'"); - } - if (typeof data.settings !== "object") { - return ExplainedResult.bad("Bad settings object"); - } - - const settings = data.settings; - for (let i = 0; i < allApplicationSettings.length; ++i) { - const setting = allApplicationSettings[i]; - const storedValue = settings[setting.id]; - if (!setting.validate(storedValue)) { - return ExplainedResult.bad("Bad setting value for " + setting.id + ": " + storedValue); - } - } - return ExplainedResult.good(); - } - - getDefaultData() { - return { - version: this.getCurrentVersion(), - settings: new SettingsStorage(), - }; - } - - getCurrentVersion() { - return 24; - } - - /** @param {{settings: SettingsStorage, version: number}} data */ - migrate(data) { - // Simply reset before - if (data.version < 5) { - data.settings = new SettingsStorage(); - data.version = this.getCurrentVersion(); - return ExplainedResult.good(); - } - - if (data.version < 6) { - data.settings.alwaysMultiplace = false; - data.version = 6; - } - - if (data.version < 7) { - data.settings.offerHints = true; - data.version = 7; - } - - if (data.version < 8) { - data.settings.scrollWheelSensitivity = "regular"; - data.version = 8; - } - - if (data.version < 9) { - data.settings.language = "auto-detect"; - data.version = 9; - } - - if (data.version < 10) { - data.settings.movementSpeed = "regular"; - data.version = 10; - } - - if (data.version < 11) { - data.settings.enableTunnelSmartplace = true; - data.version = 11; - } - - if (data.version < 12) { - data.settings.vignette = true; - data.version = 12; - } - - if (data.version < 13) { - data.settings.compactBuildingInfo = false; - data.version = 13; - } - - if (data.version < 14) { - data.settings.disableCutDeleteWarnings = false; - data.version = 14; - } - - if (data.version < 15) { - data.settings.autosaveInterval = "two_minutes"; - data.version = 15; - } - - if (data.version < 16) { - // RE-ENABLE this setting, it already existed - data.settings.enableTunnelSmartplace = true; - data.version = 16; - } - - if (data.version < 17) { - data.settings.enableColorBlindHelper = false; - data.version = 17; - } - - if (data.version < 18) { - data.settings.rotationByBuilding = true; - data.version = 18; - } - - if (data.version < 19) { - data.settings.lowQualityMapResources = false; - data.version = 19; - } - - if (data.version < 20) { - data.settings.disableTileGrid = false; - data.version = 20; - } - - if (data.version < 21) { - data.settings.lowQualityTextures = false; - data.version = 21; - } - - if (data.version < 22) { - data.settings.clearCursorOnDeleteWhilePlacing = true; - data.version = 22; - } - - if (data.version < 23) { - data.settings.displayChunkBorders = false; - data.version = 23; - } - - if (data.version < 24) { - data.settings.musicVolume = 1.0; - data.settings.soundVolume = 1.0; - data.version = 24; - } - - return ExplainedResult.good(); - } -} +/* typehints:start */ +import { Application } from "../application"; +/* typehints:end */ + +import { ReadWriteProxy } from "../core/read_write_proxy"; +import { BoolSetting, EnumSetting, RangeSetting, BaseSetting } from "./setting_types"; +import { createLogger } from "../core/logging"; +import { ExplainedResult } from "../core/explained_result"; +import { THEMES, THEME, applyGameTheme } from "../game/theme"; +import { IS_DEMO } from "../core/config"; +import { T } from "../translations"; +import { LANGUAGES } from "../languages"; + +const logger = createLogger("application_settings"); + +/** + * @enum {string} + */ +export const enumCategories = { + general: "general", + userInterface: "userInterface", + performance: "performance", + advanced: "advanced", +}; + +export const uiScales = [ + { + id: "super_small", + size: 0.6, + }, + { + id: "small", + size: 0.8, + }, + { + id: "regular", + size: 1, + }, + { + id: "large", + size: 1.05, + }, + { + id: "huge", + size: 1.1, + }, +]; + +export const scrollWheelSensitivities = [ + { + id: "super_slow", + scale: 0.25, + }, + { + id: "slow", + scale: 0.5, + }, + { + id: "regular", + scale: 1, + }, + { + id: "fast", + scale: 2, + }, + { + id: "super_fast", + scale: 4, + }, +]; + +export const movementSpeeds = [ + { + id: "super_slow", + multiplier: 0.25, + }, + { + id: "slow", + multiplier: 0.5, + }, + { + id: "regular", + multiplier: 1, + }, + { + id: "fast", + multiplier: 2, + }, + { + id: "super_fast", + multiplier: 4, + }, + { + id: "extremely_fast", + multiplier: 8, + }, +]; + +export const autosaveIntervals = [ + { + id: "one_minute", + seconds: 60, + }, + { + id: "two_minutes", + seconds: 120, + }, + { + id: "five_minutes", + seconds: 5 * 60, + }, + { + id: "ten_minutes", + seconds: 10 * 60, + }, + { + id: "twenty_minutes", + seconds: 20 * 60, + }, + { + id: "disabled", + seconds: null, + }, +]; + +const refreshRateOptions = ["30", "60", "120", "180", "240"]; + +if (G_IS_DEV) { + refreshRateOptions.unshift("10"); + refreshRateOptions.unshift("5"); + refreshRateOptions.push("1000"); + refreshRateOptions.push("2000"); + refreshRateOptions.push("5000"); + refreshRateOptions.push("10000"); +} + +/** @type {Array} */ +export const allApplicationSettings = [ + new EnumSetting("language", { + options: Object.keys(LANGUAGES), + valueGetter: key => key, + textGetter: key => LANGUAGES[key].name, + category: enumCategories.general, + restartRequired: true, + changeCb: (app, id) => null, + magicValue: "auto-detect", + }), + + new EnumSetting("uiScale", { + options: uiScales.sort((a, b) => a.size - b.size), + valueGetter: scale => scale.id, + textGetter: scale => T.settings.labels.uiScale.scales[scale.id], + category: enumCategories.userInterface, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => app.updateAfterUiScaleChanged(), + }), + + new BoolSetting( + "soundsMuted", + enumCategories.general,# + /** + * @param {Application} app + */ + (app, value) => app.sound.setSoundsMuted(value) + ), + new RangeSetting( + "soundVolume", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => app.sound.setSoundVolume(value / 100.0) + ), + new BoolSetting( + "musicMuted", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => app.sound.setMusicMuted(value) + ), + new RangeSetting( + "musicVolume", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => app.sound.setMusicVolume(value / 100.0) + ), + + new BoolSetting( + "fullscreen", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => { + if (app.platformWrapper.getSupportsFullscreen()) { + app.platformWrapper.setFullscreen(value); + } + }, + !IS_DEMO + ), + + new BoolSetting( + "enableColorBlindHelper", + enumCategories.general, + /** + * @param {Application} app + */ + (app, value) => null + ), + + new BoolSetting("offerHints", enumCategories.userInterface, (app, value) => {}), + + new EnumSetting("theme", { + options: Object.keys(THEMES), + valueGetter: theme => theme, + textGetter: theme => T.settings.labels.theme.themes[theme], + category: enumCategories.userInterface, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => { + applyGameTheme(id); + document.documentElement.setAttribute("data-theme", id); + }, + enabled: !IS_DEMO, + }), + + new EnumSetting("autosaveInterval", { + options: autosaveIntervals, + valueGetter: interval => interval.id, + textGetter: interval => T.settings.labels.autosaveInterval.intervals[interval.id], + category: enumCategories.advanced, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => null, + }), + + new EnumSetting("scrollWheelSensitivity", { + options: scrollWheelSensitivities.sort((a, b) => a.scale - b.scale), + valueGetter: scale => scale.id, + textGetter: scale => T.settings.labels.scrollWheelSensitivity.sensitivity[scale.id], + category: enumCategories.advanced, + restartRequired: false, + changeCb: + /** + * @param {Application} app + */ + (app, id) => app.updateAfterUiScaleChanged(), + }), + + new EnumSetting("movementSpeed", { + options: movementSpeeds.sort((a, b) => a.multiplier - b.multiplier), + valueGetter: multiplier => multiplier.id, + textGetter: multiplier => T.settings.labels.movementSpeed.speeds[multiplier.id], + category: enumCategories.advanced, + restartRequired: false, + changeCb: (app, id) => {}, + }), + + new BoolSetting("alwaysMultiplace", enumCategories.advanced, (app, value) => {}), + new BoolSetting("clearCursorOnDeleteWhilePlacing", enumCategories.advanced, (app, value) => {}), + new BoolSetting("enableTunnelSmartplace", enumCategories.advanced, (app, value) => {}), + new BoolSetting("vignette", enumCategories.userInterface, (app, value) => {}), + new BoolSetting("compactBuildingInfo", enumCategories.userInterface, (app, value) => {}), + new BoolSetting("disableCutDeleteWarnings", enumCategories.advanced, (app, value) => {}), + new BoolSetting("rotationByBuilding", enumCategories.advanced, (app, value) => {}), + new BoolSetting("displayChunkBorders", enumCategories.advanced, (app, value) => {}), + + new EnumSetting("refreshRate", { + options: refreshRateOptions, + valueGetter: rate => rate, + textGetter: rate => rate + " Hz", + category: enumCategories.performance, + restartRequired: false, + changeCb: (app, id) => {}, + enabled: !IS_DEMO, + }), + + new BoolSetting("lowQualityMapResources", enumCategories.performance, (app, value) => {}), + new BoolSetting("disableTileGrid", enumCategories.performance, (app, value) => {}), + new BoolSetting("lowQualityTextures", enumCategories.performance, (app, value) => {}), +]; + +export function getApplicationSettingById(id) { + return allApplicationSettings.find(setting => setting.id === id); +} + +class SettingsStorage { + constructor() { + this.uiScale = "regular"; + this.fullscreen = G_IS_STANDALONE; + + this.soundsMuted = false; + this.musicMuted = false; + this.soundVolume = 1.0; + this.musicVolume = 1.0; + + this.theme = "light"; + this.refreshRate = "60"; + this.scrollWheelSensitivity = "regular"; + this.movementSpeed = "regular"; + this.language = "auto-detect"; + this.autosaveInterval = "two_minutes"; + + this.alwaysMultiplace = false; + this.offerHints = true; + this.enableTunnelSmartplace = true; + this.vignette = true; + this.compactBuildingInfo = false; + this.disableCutDeleteWarnings = false; + this.rotationByBuilding = true; + this.clearCursorOnDeleteWhilePlacing = true; + this.displayChunkBorders = false; + + this.enableColorBlindHelper = false; + + this.lowQualityMapResources = false; + this.disableTileGrid = false; + this.lowQualityTextures = false; + + /** + * @type {Object.} + */ + this.keybindingOverrides = {}; + } +} + +export class ApplicationSettings extends ReadWriteProxy { + constructor(app) { + super(app, "app_settings.bin"); + } + + initialize() { + // Read and directly write latest data back + return this.readAsync() + .then(() => { + // Apply default setting callbacks + const settings = this.getAllSettings(); + for (let i = 0; i < allApplicationSettings.length; ++i) { + const handle = allApplicationSettings[i]; + handle.apply(this.app, settings[handle.id]); + } + }) + + .then(() => this.writeAsync()); + } + + save() { + return this.writeAsync(); + } + + // Getters + + /** + * @returns {SettingsStorage} + */ + getAllSettings() { + return this.getCurrentData().settings; + } + + /** + * @param {string} key + */ + getSetting(key) { + assert(this.getAllSettings().hasOwnProperty(key), "Setting not known: " + key); + return this.getAllSettings()[key]; + } + + getInterfaceScaleId() { + if (!this.currentData) { + // Not initialized yet + return "regular"; + } + return this.getAllSettings().uiScale; + } + + getDesiredFps() { + return parseInt(this.getAllSettings().refreshRate); + } + + getInterfaceScaleValue() { + const id = this.getInterfaceScaleId(); + for (let i = 0; i < uiScales.length; ++i) { + if (uiScales[i].id === id) { + return uiScales[i].size; + } + } + logger.error("Unknown ui scale id:", id); + return 1; + } + + getScrollWheelSensitivity() { + const id = this.getAllSettings().scrollWheelSensitivity; + for (let i = 0; i < scrollWheelSensitivities.length; ++i) { + if (scrollWheelSensitivities[i].id === id) { + return scrollWheelSensitivities[i].scale; + } + } + logger.error("Unknown scroll wheel sensitivity id:", id); + return 1; + } + + getMovementSpeed() { + const id = this.getAllSettings().movementSpeed; + for (let i = 0; i < movementSpeeds.length; ++i) { + if (movementSpeeds[i].id === id) { + return movementSpeeds[i].multiplier; + } + } + logger.error("Unknown movement speed id:", id); + return 1; + } + + getAutosaveIntervalSeconds() { + const id = this.getAllSettings().autosaveInterval; + for (let i = 0; i < autosaveIntervals.length; ++i) { + if (autosaveIntervals[i].id === id) { + return autosaveIntervals[i].seconds; + } + } + logger.error("Unknown autosave interval id:", id); + return 120; + } + + getIsFullScreen() { + return this.getAllSettings().fullscreen; + } + + getKeybindingOverrides() { + return this.getAllSettings().keybindingOverrides; + } + + getLanguage() { + return this.getAllSettings().language; + } + + // Setters + + updateLanguage(id) { + assert(LANGUAGES[id], "Language not known: " + id); + return this.updateSetting("language", id); + } + + /** + * @param {string} key + * @param {string|boolean|number} value + */ + updateSetting(key, value) { + for (let i = 0; i < allApplicationSettings.length; ++i) { + const setting = allApplicationSettings[i]; + if (setting.id === key) { + if (!setting.validate(value)) { + assertAlways(false, "Bad setting value: " + key); + } + this.getAllSettings()[key] = value; + if (setting.changeCb) { + setting.changeCb(this.app, value); + } + return this.writeAsync(); + } + } + assertAlways(false, "Unknown setting: " + key); + } + + /** + * Sets a new keybinding override + * @param {string} keybindingId + * @param {number} keyCode + */ + updateKeybindingOverride(keybindingId, keyCode) { + assert(Number.isInteger(keyCode), "Not a valid key code: " + keyCode); + this.getAllSettings().keybindingOverrides[keybindingId] = keyCode; + return this.writeAsync(); + } + + /** + * Resets a given keybinding override + * @param {string} id + */ + resetKeybindingOverride(id) { + delete this.getAllSettings().keybindingOverrides[id]; + return this.writeAsync(); + } + /** + * Resets all keybinding overrides + */ + resetKeybindingOverrides() { + this.getAllSettings().keybindingOverrides = {}; + return this.writeAsync(); + } + + // RW Proxy impl + verify(data) { + if (!data.settings) { + return ExplainedResult.bad("missing key 'settings'"); + } + if (typeof data.settings !== "object") { + return ExplainedResult.bad("Bad settings object"); + } + + const settings = data.settings; + for (let i = 0; i < allApplicationSettings.length; ++i) { + const setting = allApplicationSettings[i]; + const storedValue = settings[setting.id]; + if (!setting.validate(storedValue)) { + return ExplainedResult.bad("Bad setting value for " + setting.id + ": " + storedValue); + } + } + return ExplainedResult.good(); + } + + getDefaultData() { + return { + version: this.getCurrentVersion(), + settings: new SettingsStorage(), + }; + } + + getCurrentVersion() { + return 24; + } + + /** @param {{settings: SettingsStorage, version: number}} data */ + migrate(data) { + // Simply reset before + if (data.version < 5) { + data.settings = new SettingsStorage(); + data.version = this.getCurrentVersion(); + return ExplainedResult.good(); + } + + if (data.version < 6) { + data.settings.alwaysMultiplace = false; + data.version = 6; + } + + if (data.version < 7) { + data.settings.offerHints = true; + data.version = 7; + } + + if (data.version < 8) { + data.settings.scrollWheelSensitivity = "regular"; + data.version = 8; + } + + if (data.version < 9) { + data.settings.language = "auto-detect"; + data.version = 9; + } + + if (data.version < 10) { + data.settings.movementSpeed = "regular"; + data.version = 10; + } + + if (data.version < 11) { + data.settings.enableTunnelSmartplace = true; + data.version = 11; + } + + if (data.version < 12) { + data.settings.vignette = true; + data.version = 12; + } + + if (data.version < 13) { + data.settings.compactBuildingInfo = false; + data.version = 13; + } + + if (data.version < 14) { + data.settings.disableCutDeleteWarnings = false; + data.version = 14; + } + + if (data.version < 15) { + data.settings.autosaveInterval = "two_minutes"; + data.version = 15; + } + + if (data.version < 16) { + // RE-ENABLE this setting, it already existed + data.settings.enableTunnelSmartplace = true; + data.version = 16; + } + + if (data.version < 17) { + data.settings.enableColorBlindHelper = false; + data.version = 17; + } + + if (data.version < 18) { + data.settings.rotationByBuilding = true; + data.version = 18; + } + + if (data.version < 19) { + data.settings.lowQualityMapResources = false; + data.version = 19; + } + + if (data.version < 20) { + data.settings.disableTileGrid = false; + data.version = 20; + } + + if (data.version < 21) { + data.settings.lowQualityTextures = false; + data.version = 21; + } + + if (data.version < 22) { + data.settings.clearCursorOnDeleteWhilePlacing = true; + data.version = 22; + } + + if (data.version < 23) { + data.settings.displayChunkBorders = false; + data.version = 23; + } + + if (data.version < 24) { + data.settings.musicVolume = 1.0; + data.settings.soundVolume = 1.0; + data.settings.refreshRate = "60"; + data.version = 24; + } + + return ExplainedResult.good(); + } +} diff --git a/translations/base-de.yaml b/translations/base-de.yaml index cdc23a2e..da8e1008 100644 --- a/translations/base-de.yaml +++ b/translations/base-de.yaml @@ -44,48 +44,45 @@ steamPage: Nutze dein gesammeltes Wissen über die Maschinen und lasse deine Fabriken die gewünschten Formen der 18 verschiedenen Level abliefern. Schalte mit jedem Level neue Arbeitsschritte oder Gebäude frei. Das sollte dich schon für Stunden beschäftigt halten! Danach werden im Freispielmodus zufällige Formen generiert, die du ebenfalls abliefern kannst. Ich füge regelmäßig neue Funktionen hinzu und davon sind eine ganze Menge geplant! - Wenn du das Spiel erwirbst, erhälst du Zugriff auf die zusätzlichen Features der Standalone-Version. Das bedeutet, du kannst unter anderem die neuesten Updates zuerst spielen! [b]Vorteile der Standalone[/b] [list] - [*] Dark Mode - [*] unbegrenzte Anzahl an Wegpunkten - [*] unbegrenzte Anzahl an Speicherständen - [*] zusätzliche Einstellungen - [*] Bald: Strom & Kabel! (Ungefähr) geplant für ende Juli 2020. - [*] Bald: mehr Level - [*] Erlaubt es mir shapez.io weiter zu entwickeln ❤️ + [*] Dark-Mode + [*] Unbegrenzte Anzahl an Wegpunkten + [*] Unbegrenzte Anzahl an Speicherständen + [*] Zusätzliche Einstellungen + [*] Es kommen: Kabel & Energie! Voraussichtlich gegen Ende Juli 2020 + [*] Es kommen: Mehr Levels + [*] Unterstütze die Entwicklung von shapez.io ❤️ [/list] - [b]Zukünftige Updates:[/b] + [b]Geplante Funktionen[/b] - Ich update das Spiel sehr oft, und versuche wöchentlich ein Update zu veröffentlichen! + Ich bin aktiv mit der Entwicklung beschäftigt und versuche jede Woche ein Update oder den aktuellen Stand der Entwicklung zu veröffentlichen. [list] [*] Verschiedene Karten und Herausforderungen (z.B. Karten mit Hindernissen) - [*] Herausforderungen (liefere die geforderte Form mit einer beschränkten Karte / anzahl an Gebäuden ab) - [*] Eine Kampange, in der die Gebäude einen Preis haben. - [*] einen konfigurierbaren Kartengenerator (bestimme Ressourcen / Größe / Dichte, den Seed und viel mehr) - [*] zusätzliche Formen + [*] 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) + [*] Mehr Formentypen [*] Performanceverbesserungen (Das Spiel läuft bereits sehr gut!) [*] Und vieles mehr! [/list] - [b]Das Spiel ist open source![/b] + [b]Das Spiel ist Open Source![/b] - Jeder kann dazu beitragen. Ich bin aktiv in der Communtiy involviert und versuche alle Vorschläge zu lesen und beziehe so viel Feedback wie möglich mit in die Entwicklung ein. - Die komplette Roadmap gibt es auf dem Trello-Board zum Nachlesen! + Jeder kann dazu beitragen! Ich bin aktiv in die Community involviert, versuche alle Vorschläge zu lesen und beziehe so viel Feedback wie möglich mit in die Entwicklung ein. + Die komplette Roadmap gibt es auf dem Trello-Board zum Nachlesen. - [b]Links [/b] + [b]Links[/b] [list] [*] [url=https://discord.com/invite/HN7EVzV]Offizieller Discord[/url] [*] [url=https://trello.com/b/ISQncpJP/shapezio]Roadmap[/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]Hilf zu übersetzen[/url] [*] [url=https://github.com/tobspr/shapez.io]Quelltext (GitHub)[/url] [*] [url=https://github.com/tobspr/shapez.io/blob/master/translations/README.md]Hilf beim Übersetzen[/url] [/list] @@ -156,7 +153,7 @@ mainMenu: # This is shown when using firefox and other browsers which are not supported. browserWarning: >- - Sorry, aber das Spiel wird in deinem Browser langsam laufen! Kaufe die Standalone-Version oder downloade Chrome für die beste Erfahrung! + Sorry, aber das Spiel wird in deinem Browser langsam laufen! Kaufe die Standalone-Version oder verwende Chrome für die beste Erfahrung! savegameLevel: Level savegameLevelUnknown: Unbekanntes Level @@ -274,8 +271,7 @@ dialogs: exportScreenshotWarning: title: Bildschirmfoto exportieren - desc: >- - Hier kannst du ein Bildschirmfoto von deiner ganzen Fabrik erstellen. Für extrem große Fabriken kann das jedoch sehr lange dauern und ggf. zum Spielabsturz führen! + desc: Hier kannst du ein Bildschirmfoto von deiner ganzen Fabrik erstellen. Für extrem große Fabriken kann das jedoch sehr lange dauern und ggf. zum Spielabsturz führen! ingame: # This is shown in the top left corner and displays useful keybindings in @@ -373,7 +369,7 @@ ingame: delivered: title: Abgeliefert description: Zeigt die Menge an Formen, die im zentralen Gebäude abgeliefert werden. - noShapesProduced: Bisher wurden keine Formen produziert. + noShapesProduced: Es werden noch keine Formen produziert oder abgeliefert. # Displays the shapes per minute, e.g. '523 / m' shapesPerMinute: / m @@ -502,7 +498,7 @@ buildings: name: Rotierer (+90°) description: Rotiert Formen gegen den Uhrzeigersinn um 90 Grad. fl: - name: Rotierer (+180°) + name: Rotierer (180°) description: Rotiert die Formen um 180 Grad. stacker: @@ -770,11 +766,18 @@ settings: description: >- Aktiviert das automatische Entfernen von überflüssigen Förderbändern bei der Platzierung von Tunneln. Außerdem funktioniert das Ziehen von Tunneln und überschüssige werden ebenfalls entfernt. + vignette: title: Vignette description: >- Aktiviert den Vignetteneffekt, der den Rand des Bildschirms zunehmend verdunkelt und das Lesen der Textfelder vereinfacht. + rotationByBuilding: + title: Rotation pro Gebäudetyp + description: >- + Jeder Gebäudetyp merkt sich einzeln, in welche Richtung er zeigt. + Das fühlt sich möglicherweise besser an, wenn du häufig zwischen verschiedenen Gebäudetypen wechselst. + compactBuildingInfo: title: Kompakte Gebäudeinformationen description: >- @@ -785,13 +788,6 @@ settings: description: >- Deaktiviert die Warnung, die beim Löschen und Ausschneiden von mehr als 100 Feldern angezeigt wird. - rotationByBuilding: - title: Rotation pro Gebäudetyp - description: >- - Jeder Gebäudetyp merkt sich einzeln, in welche Richtung er zeigt. - Das fühlt sich möglicherweise besser an, wenn du häufig zwischen verschiedenen - Gebäudetypen wechselst. - keybindings: title: Tastenbelegung hint: >- @@ -830,6 +826,8 @@ keybindings: toggleFPSInfo: FPS und Debug-Info an/aus switchLayers: Ebenen wechseln exportScreenshot: Ganze Fabrik als Foto exportieren + + # --- Do not translate the values in this section belt: *belt splitter: *splitter underground_belt: *underground_belt @@ -840,6 +838,7 @@ keybindings: mixer: *mixer painter: *painter trash: *trash + # --- pipette: Pipette rotateWhilePlacing: Rotieren @@ -847,6 +846,7 @@ keybindings: Modifikator: stattdessen gegen den UZS rotieren cycleBuildingVariants: Variante wählen confirmMassDelete: Massenlöschung bestätigen + pasteLastBlueprint: Letzte Blaupause einfügen cycleBuildings: Gebäude rotieren lockBeltDirection: Bandplaner aktivieren switchDirectionLockSide: >- @@ -860,7 +860,6 @@ keybindings: placementDisableAutoOrientation: Automatische Orientierung deaktivieren placeMultiple: Im Platziermodus bleiben placeInverse: Automatische Förderbandorientierung invertieren - pasteLastBlueprint: Letzte Blaupause einfügen advanced_processor: Farbinvertierer energy_generator: Stromgenerator wire: Stromkabel @@ -881,9 +880,8 @@ about: Der Soundtrack wurde von Peppsen komponiert! Klasse Typ.

- Abschließend möchte ich meinem Kumpel Niklas danken! Ohne unsere - etlichen gemeinsamen Stunden in Factorio wäre dieses Projekt nie zustande gekommen. + Abschließend möchte ich meinem Kumpel Niklas danken! + Ohne unsere etlichen gemeinsamen Stunden in Factorio wäre dieses Projekt nie zustande gekommen. changelog: title: Änderungen