From f28c2cd0fc1c9121f191726addd2496c7645d93c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 20 Nov 2022 22:58:00 +0100 Subject: [PATCH 01/11] Isometric example with NavMesh path finding. --- examples/isometric-game/isometric-game.json | 3238 +++++++++++++------ 1 file changed, 2322 insertions(+), 916 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index 43bf3cfef..b84c10ee4 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -15,6 +15,7 @@ "projectUuid": "9de71fbb-130f-41c4-9778-341467075026", "scaleMode": "linear", "sizeOnStartupMode": "", + "templateSlug": "", "useExternalSourceFiles": false, "version": "1.0.0", "name": "Isometric World", @@ -4228,6 +4229,7 @@ "name": "grass_light_steps.mp3", "preloadAsMusic": false, "preloadAsSound": false, + "preloadInCache": false, "userAdded": true }, { @@ -4530,9 +4532,6 @@ "disableInputWhenNotFocused": true, "mangledName": "Level_321", "name": "Level 1", - "oglFOV": 90, - "oglZFar": 500, - "oglZNear": 1, "r": 45, "standardSortMethod": true, "stopSoundsOnStartup": true, @@ -4548,7 +4547,7 @@ "gridColor": 0, "gridAlpha": 0.5, "snap": true, - "zoomFactor": 0.4899999847412109, + "zoomFactor": 0.3833333129882819, "windowMask": false }, "objectsGroups": [ @@ -4577,7 +4576,6 @@ "customSize": true, "height": 128, "layer": "", - "locked": false, "name": "Mindy", "persistentUuid": "1280b677-7168-488e-a7a0-05efad670a43", "width": 128, @@ -4593,7 +4591,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "3a9ef9d8-2771-42c4-895c-46e8deb98a55", "width": 300, @@ -4609,7 +4606,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "ac3ac782-077c-4499-86ff-63c763d0d235", "width": 300, @@ -4630,7 +4626,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "4de43fc6-4fda-45fd-b03a-94c983306ba6", "width": 300, @@ -4651,7 +4646,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Block", "persistentUuid": "e7171963-cf83-499a-ac11-ffc3f996ee86", "width": 300, @@ -4667,7 +4661,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Block", "persistentUuid": "97f3badf-c0e2-4e47-bf54-74784f1bc806", "width": 300, @@ -4683,7 +4676,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "8ecfa155-67c6-480f-b55c-c115a0c7a454", "width": 300, @@ -4704,7 +4696,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "cf56dc22-378f-4868-a105-67d36581d37c", "width": 300, @@ -4725,7 +4716,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Bridge", "persistentUuid": "905c05cf-7d6b-425b-99ab-934a3a8a65a6", "width": 300, @@ -4741,7 +4731,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "3b51800f-532a-47e9-9b9a-641c773f81c9", "width": 300, @@ -4762,12 +4751,11 @@ "customSize": true, "height": 35, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "7a37ce36-7c51-44cf-99cc-00e9d7a8ba09", "width": 200, - "x": 758.9955444335938, - "y": 490.54461669921875, + "x": 770.7310180664062, + "y": 506.12615966796875, "zOrder": 700, "numberProperties": [], "stringProperties": [], @@ -4778,7 +4766,6 @@ "customSize": true, "height": 20, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "e9620adc-e951-45f4-aee9-6bf0ac52c6e5", "width": 209, @@ -4794,7 +4781,6 @@ "customSize": true, "height": 60, "layer": "", - "locked": false, "name": "Bat", "persistentUuid": "a1e7a693-56fb-453c-bd7a-133a68da3774", "width": 60, @@ -4810,7 +4796,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "620ab50d-d358-4c3a-81ee-d76f3cb4cf73", "width": 0, @@ -4826,7 +4811,6 @@ "customSize": true, "height": 214, "layer": "", - "locked": false, "name": "Rock", "persistentUuid": "94ffca77-30f6-4ff3-8282-13a8da3bd164", "width": 107, @@ -4842,7 +4826,6 @@ "customSize": true, "height": 128, "layer": "", - "locked": false, "name": "WoodSign", "persistentUuid": "7301be6c-64d6-4f87-a718-8ff5cf796a39", "width": 64, @@ -4858,7 +4841,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Block", "persistentUuid": "88c25da3-cdd4-4923-ba78-00c842aadc02", "width": 300, @@ -4874,7 +4856,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "6c7a24d4-8c07-4364-8df5-f3aacd2cdd7f", "width": 300, @@ -4895,7 +4876,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "cd650dea-c5a0-4670-8240-9049a8253405", "width": 300, @@ -4916,7 +4896,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "271e1d2c-420c-47b8-a4c5-424036ac3750", "width": 294, @@ -4932,7 +4911,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "a4347e0a-416d-468a-b032-541e62c5f0cb", "width": 294, @@ -4948,7 +4926,6 @@ "customSize": true, "height": 214, "layer": "", - "locked": false, "name": "Rock", "persistentUuid": "1d59ad64-a5ef-4fac-8b65-f1d74af609e0", "width": 107, @@ -4964,7 +4941,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "9296442d-dba0-46af-ad91-f43c4dc285f2", "width": 300, @@ -4985,7 +4961,6 @@ "customSize": true, "height": 100, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "96c674bd-fa6e-43e7-b68d-297025f1134c", "width": 40, @@ -5001,7 +4976,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "217f17da-0775-4ca3-abfb-35d5a5577f6c", "width": 300, @@ -5022,7 +4996,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "b9795740-d3b5-4948-a3f7-db77974bd121", "width": 300, @@ -5043,7 +5016,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "6e7d3aac-0e26-4b17-9d60-fcd1a53d46fc", "width": 294, @@ -5059,7 +5031,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "9fd54406-3c86-4548-824c-acab801884de", "width": 294, @@ -5075,7 +5046,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "4a71cf64-23a3-4bf7-b143-604c3f24b671", "width": 294, @@ -5091,7 +5061,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "aaa764c5-8cce-4f2c-8250-76fbb4d49e2b", "width": 294, @@ -5107,7 +5076,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "04686a5d-7019-47e7-b146-a5e03c564009", "width": 300, @@ -5128,7 +5096,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "9cb8c3fa-df70-4f65-87b1-022e5c34319b", "width": 300, @@ -5149,7 +5116,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "8436a0c3-6fb4-473c-9355-828871a898b5", "width": 300, @@ -5170,7 +5136,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "eefd3c49-b032-46e4-ad25-7ec79d57815a", "width": 0, @@ -5186,7 +5151,6 @@ "customSize": true, "height": 294, "layer": "Level2", - "locked": false, "name": "FloorGrass", "persistentUuid": "9a18187f-50b2-47d6-bb89-cc5ae5a60181", "width": 294, @@ -5202,7 +5166,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "32b846d9-ba9a-4f7d-ac98-8102e20d64aa", "width": 187.5, @@ -5218,7 +5181,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "4fbcdda0-da8d-4357-a516-3cb240b4bb27", "width": 187.5, @@ -5234,7 +5196,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "da2a89dd-02c7-4baf-99b0-6c87f3785922", "width": 187.5, @@ -5250,7 +5211,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "bb399c9f-0271-4fe6-b49b-697f0d50a880", "width": 187.5, @@ -5266,7 +5226,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "d71d2ee7-a888-46ff-9f8d-ff00b0e78f9a", "width": 187.5, @@ -5282,7 +5241,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "c7eb264c-ae4d-4b0e-99d5-fc9ee8a5eed4", "width": 187.5, @@ -5298,7 +5256,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "ad594c02-842f-4c5e-a4aa-e6c1117647d1", "width": 187.5, @@ -5314,7 +5271,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "d91da7ca-2ac5-4139-96f4-eec982cc9081", "width": 187.5, @@ -5330,7 +5286,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "e6db7d99-ac63-4626-9fcc-598a7392c5b8", "width": 187.5, @@ -5346,7 +5301,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "05760cab-a2ff-41f8-9c29-9f0eb66f0821", "width": 187.5, @@ -5362,7 +5316,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "7ae916d9-6716-4389-8d2c-0c70776c4395", "width": 187.5, @@ -5378,7 +5331,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "aa64a032-6e9b-4b6f-a2b5-61313b56621f", "width": 187.5, @@ -5394,7 +5346,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "b384f1f2-d1ca-4407-a17c-1009cf826e04", "width": 187.5, @@ -5410,7 +5361,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "1a407889-5da2-4634-acaf-3b228b4d2d70", "width": 187.5, @@ -5426,7 +5376,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "24468fa0-bcee-4ad7-8ab0-4c0ccb7747a1", "width": 187.5, @@ -5442,7 +5391,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "1e33a872-b994-4a10-b61d-b63d130fe3bc", "width": 187.5, @@ -5458,7 +5406,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "acb404e6-e16a-47fe-bd6d-a73d07b0c0d6", "width": 187.5, @@ -5474,7 +5421,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "09a86bf7-c30e-48d8-b46c-8d5b83110e6b", "width": 187.5, @@ -5490,7 +5436,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "83dfaab3-0bb9-4fdb-8a57-230ec97321e8", "width": 187.5, @@ -5506,7 +5451,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "c6bd811b-a8ac-47af-ac19-1842ef2bcbf3", "width": 187.5, @@ -5522,7 +5466,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "9d1f9939-d36b-42fe-9a34-59d94a1be464", "width": 187.5, @@ -5538,7 +5481,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "8653f62e-ddae-429b-af47-806a348fd009", "width": 187.5, @@ -5554,7 +5496,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "d954dfff-a505-44a6-ab56-59d31c506bef", "width": 187.5, @@ -5570,7 +5511,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "65b03822-9d35-4ff7-b235-2e718d4a3604", "width": 187.5, @@ -5586,7 +5526,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "f2734c50-f078-448f-9e98-30c361646c13", "width": 187.5, @@ -5602,7 +5541,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "05be920d-b4d1-4697-aaae-469303de310a", "width": 187.5, @@ -5618,7 +5556,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "29ebbef0-088e-4150-97d9-d29eec61d9a0", "width": 187.5, @@ -5634,7 +5571,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "8931d117-47c7-4fb2-8c86-9ebce76e72b6", "width": 300, @@ -5655,7 +5591,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "39a9233b-4395-4cb7-946d-ca867cc7ad4c", "width": 300, @@ -5676,7 +5611,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "8ed7ab3b-0983-4c6b-87d9-829f907b8880", "width": 300, @@ -5697,7 +5631,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Bridge", "persistentUuid": "91d1ee6b-f87f-4562-bdb9-1da928568ee6", "width": 300, @@ -5713,12 +5646,11 @@ "customSize": true, "height": 35, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "39083ed8-a9e8-4e90-b3c0-d5e607025509", "width": 200, - "x": 1513.8753662109375, - "y": 564.275146484375, + "x": 1521.7310791015625, + "y": 588.1261596679688, "zOrder": 700, "numberProperties": [], "stringProperties": [], @@ -5729,12 +5661,11 @@ "customSize": true, "height": 20, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "4f9439e2-61c8-4cc2-9536-0ce34942cce9", "width": 209, - "x": 1446.74755859375, - "y": 535.8841552734375, + "x": 1437.5601806640625, + "y": 534.6781005859375, "zOrder": 700, "numberProperties": [], "stringProperties": [], @@ -5745,7 +5676,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "16c45f54-fed4-4213-b5db-ad76f47af715", "width": 300, @@ -5761,7 +5691,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "d2de52a5-7d54-428a-8df9-d9e3cff7ae9d", "width": 300, @@ -5777,7 +5706,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "7af023fe-cc01-4406-831f-ec12c295f8b1", "width": 300, @@ -5798,7 +5726,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "40111908-320d-436e-8025-1fb41493a1ea", "width": 294, @@ -5814,7 +5741,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "b20581a7-d93f-499b-a983-a9aefaaac4ff", "width": 294, @@ -5830,7 +5756,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "a987c8f4-cb49-4d01-8683-e99d18cc218f", "width": 294, @@ -5846,7 +5771,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "5e9b3e32-adb8-47f3-8bb1-326c7268c852", "width": 294, @@ -5862,7 +5786,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "e13fc3a7-899b-4eb9-b5a7-3864ec593fe1", "width": 294, @@ -5878,7 +5801,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "d1890568-2359-4a54-8369-79460a0346e2", "width": 294, @@ -5894,7 +5816,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "da90cd2b-876b-40e1-adee-80ccfe72c06b", "width": 294, @@ -5910,7 +5831,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "3a7ac0ef-20a4-4894-9706-09340cf21eb2", "width": 294, @@ -5926,7 +5846,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "de4bc3c1-24d1-4d95-a5d2-44cadc726309", "width": 300, @@ -5947,7 +5866,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "44ba03a9-9b37-4db3-a314-1a54c3f1d7fc", "width": 300, @@ -5968,7 +5886,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "de67af8e-8fbb-4065-94e2-6003fc2f35c6", "width": 300, @@ -5989,7 +5906,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "899cf9cc-6d5b-4de8-9958-260dcc63dc89", "width": 300, @@ -6010,7 +5926,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "71b5dc1b-45cf-48b7-9b06-2441eb0c4423", "width": 300, @@ -6031,7 +5946,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "336e5c10-1929-470c-8581-193b195ad690", "width": 300, @@ -6052,7 +5966,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "c956f9f7-5585-4479-965a-3e4591871fcf", "width": 300, @@ -6073,7 +5986,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "50a0c81d-5e56-45b9-b29e-682be52ca039", "width": 294, @@ -6089,7 +6001,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Block", "persistentUuid": "da8153fa-5318-455e-955f-1d86d8bbdb6e", "width": 300, @@ -6105,7 +6016,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "f22cadc9-2633-42b6-8c46-4a61db0e2695", "width": 0, @@ -6121,7 +6031,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "3b81f327-b139-483f-8234-d5b6f75f9964", "width": 294, @@ -6137,7 +6046,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "1b9b46c8-a6a2-48a6-b61c-862a38817bbf", "width": 294, @@ -6153,7 +6061,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "8ce00b78-191a-48d2-9055-331c8abf221b", "width": 0, @@ -6169,7 +6076,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "74e5d32b-fa8a-435d-a1a9-45a5453ab129", "width": 0, @@ -6185,7 +6091,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "ecda8d96-8d79-42b0-a0bc-c3ed1f168a59", "width": 300, @@ -6206,7 +6111,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "f1d73397-7a46-4034-9cb3-249db892a1c9", "width": 300, @@ -6227,7 +6131,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "9b05780e-f571-4b8d-b684-efa6d2c6abc1", "width": 300, @@ -6248,7 +6151,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "21fcc982-015e-4343-b429-e5fca73ea721", "width": 300, @@ -6269,7 +6171,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "d17ab168-74f4-49d3-bfb0-48d72e6d8a4d", "width": 300, @@ -6290,7 +6191,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "324482b4-a3bb-478f-8481-6b35c73bf429", "width": 300, @@ -6311,7 +6211,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "13177bf9-72b0-4269-8373-8fc7e244b8ad", "width": 300, @@ -6332,7 +6231,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "ebc1d67c-4dab-41e8-8a88-f52b3bd946b1", "width": 300, @@ -6353,7 +6251,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "8eb854ab-6054-46da-b036-ac1168dc450b", "width": 300, @@ -6374,7 +6271,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "097571e3-c663-42d9-bf9f-c8b70ec76b78", "width": 300, @@ -6395,7 +6291,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Block", "persistentUuid": "0f1e5f55-6994-4a84-82de-e7f8d54606bc", "width": 300, @@ -6411,7 +6306,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Block", "persistentUuid": "7e5c90eb-e820-40f3-ac45-83f0453cdd88", "width": 300, @@ -6427,7 +6321,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Block", "persistentUuid": "8e2a20e3-ab2d-473e-95e0-04b44019d33a", "width": 300, @@ -6443,7 +6336,6 @@ "customSize": true, "height": 294, "layer": "", - "locked": false, "name": "FloorGrass", "persistentUuid": "2a7585c3-b7e9-4d0f-9fe0-1f1587e0b220", "width": 294, @@ -6459,7 +6351,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "d85bd024-f8b9-42b0-babc-758ae0f690f2", "width": 0, @@ -6475,7 +6366,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "88f947bd-7610-4b60-a612-530e45bdfed1", "width": 187.5, @@ -6491,7 +6381,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "6611325f-d7a0-4f3c-9545-e74845a42abf", "width": 187.5, @@ -6507,7 +6396,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "44e90aba-c110-41ad-a8c8-d628800baf85", "width": 187.5, @@ -6523,7 +6411,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "52692566-93de-4d39-8ba6-e7503f3a60e3", "width": 187.5, @@ -6539,7 +6426,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "3d768e9a-b147-4045-ba98-a66dfe5291aa", "width": 187.5, @@ -6555,7 +6441,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "034eb219-4c0d-417a-b593-ddc927902c8d", "width": 187.5, @@ -6571,7 +6456,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "d979c8e0-a179-48e8-a3ee-e70ed5ec8d1a", "width": 187.5, @@ -6587,7 +6471,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "8220f0d4-9ce2-4820-8bda-32937a60f130", "width": 187.5, @@ -6603,7 +6486,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "0e1a1c35-097c-41c7-8db2-8eaac2a4280e", "width": 187.5, @@ -6619,7 +6501,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "a3dd7ac8-e261-4cdf-9c1d-2eeac7683f21", "width": 187.5, @@ -6635,7 +6516,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "51959a78-690f-48ec-b24c-27201efcda7f", "width": 187.5, @@ -6651,7 +6531,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "e2c85b84-50af-4d32-8d30-36f7da88d72c", "width": 187.5, @@ -6667,7 +6546,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "670c7c61-6e2a-42a0-88c4-ddd4d4fb2e39", "width": 187.5, @@ -6683,7 +6561,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "a40d4584-d1ff-4b37-a55a-d8c3f752eac6", "width": 187.5, @@ -6699,7 +6576,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "84a32a53-d16b-4ee4-b1b9-5ada9148fba7", "width": 187.5, @@ -6715,7 +6591,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "5c8630b2-5fdb-49c5-bad2-0615c4fc56b3", "width": 187.5, @@ -6731,7 +6606,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "66fadd18-49ae-4276-b9e4-9032e527d0c9", "width": 187.5, @@ -6747,7 +6621,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "668691ea-b4d8-4b81-920b-46b1342c7a1e", "width": 187.5, @@ -6763,7 +6636,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "74b9bbad-71fe-4ff1-8166-9d48c10054d1", "width": 187.5, @@ -6779,7 +6651,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "660c9b4a-9ea6-428f-ac84-a7fc9de9cadf", "width": 187.5, @@ -6795,7 +6666,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "acd77898-dad0-430d-9853-3ce758859eb8", "width": 187.5, @@ -6811,7 +6681,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "85294e27-cf75-4e8b-9cde-925bad414afe", "width": 187.5, @@ -6827,7 +6696,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "962054c4-79b6-40a7-bc78-8144c6c85915", "width": 187.5, @@ -6843,7 +6711,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "1e0c6c26-2a6f-427d-9c2d-970ea83137d0", "width": 187.5, @@ -6859,7 +6726,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "4d11113f-cbf6-4e6b-b798-d10978a7f6bd", "width": 187.5, @@ -6875,7 +6741,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "4c09b9b2-d215-4f2e-96f0-6f74b71e04fa", "width": 187.5, @@ -6891,7 +6756,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "11a018cb-8ffe-4193-8cc1-0f0c7426d20b", "width": 187.5, @@ -6907,7 +6771,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "f8332642-6fdf-405f-8c74-712c8fa045e5", "width": 187.5, @@ -6923,7 +6786,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "bf7fea7d-fd48-4c9e-a137-55444d07a397", "width": 187.5, @@ -6939,7 +6801,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "1f372ba2-6a34-4c52-98dc-317a22a75380", "width": 187.5, @@ -6955,7 +6816,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "b2932130-4fe5-4206-873d-af4ac80e1cb1", "width": 187.5, @@ -6971,7 +6831,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "84594d42-dcbe-4248-a813-44b39b05d18e", "width": 187.5, @@ -6987,7 +6846,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "fe7bd793-a97f-43a0-8b44-71e32d5658ad", "width": 187.5, @@ -7003,7 +6861,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "7f5560e9-f876-4961-9a4b-a1da1964544e", "width": 187.5, @@ -7019,7 +6876,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "8a688e5f-d777-4bed-91f7-e1a16bd2fe05", "width": 187.5, @@ -7035,7 +6891,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "f99dd36b-b5a6-4b58-a01a-25f32e913aaf", "width": 187.5, @@ -7051,7 +6906,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "6c993935-389f-4daf-8b3d-146231adaa2a", "width": 187.5, @@ -7067,7 +6921,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "6dfd2faf-94e8-4986-956a-cb4d8d41a49f", "width": 187.5, @@ -7083,7 +6936,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "edcce033-2ee9-4f50-8ac7-e1c0aba115b4", "width": 187.5, @@ -7099,7 +6951,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "7c84397b-86fd-440b-9b03-4aec6835c494", "width": 187.5, @@ -7115,7 +6966,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "47424b54-cc61-4679-bcb2-f662050c4864", "width": 187.5, @@ -7131,7 +6981,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "23a5f41c-18f0-4cdf-a9d8-a99bb328e8c9", "width": 187.5, @@ -7147,7 +6996,6 @@ "customSize": true, "height": 294, "layer": "Level1", - "locked": false, "name": "BlockGrass", "persistentUuid": "2906a2db-0603-4cf5-ab47-636b0a84f267", "width": 294, @@ -7163,7 +7011,6 @@ "customSize": true, "height": 294, "layer": "Level1", - "locked": false, "name": "BlockGrass", "persistentUuid": "c1552a1f-091d-4df6-a7c4-61240a39041e", "width": 294, @@ -7179,7 +7026,6 @@ "customSize": true, "height": 294, "layer": "Level1", - "locked": false, "name": "BlockGrass", "persistentUuid": "5cb497ba-cc67-4dd5-b88d-4068d464c627", "width": 294, @@ -7195,7 +7041,6 @@ "customSize": true, "height": 294, "layer": "Level1", - "locked": false, "name": "BlockGrass", "persistentUuid": "c4b7568f-ff54-452a-8e2f-dd78acc58320", "width": 294, @@ -7211,7 +7056,6 @@ "customSize": true, "height": 294, "layer": "Level1", - "locked": false, "name": "BlockGrass", "persistentUuid": "6704aabe-1168-47f6-938d-381c30c73d68", "width": 294, @@ -7227,7 +7071,6 @@ "customSize": true, "height": 300, "layer": "Level1", - "locked": false, "name": "TopBlock", "persistentUuid": "2647c390-2d25-42bd-a8e0-17bdd4625e70", "width": 300, @@ -7243,7 +7086,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "26ed79f8-d0e0-40ae-b31d-145190ebb709", "width": 300, @@ -7264,7 +7106,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "871985ce-55e4-41c6-8c0d-8dda61697d93", "width": 187.5, @@ -7280,7 +7121,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "5f9cd15a-930f-4d86-b721-3ccf729286ac", "width": 187.5, @@ -7296,7 +7136,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "1e71ed60-bd3d-4722-99c0-0634e382fde5", "width": 187.5, @@ -7312,7 +7151,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "daf008de-8319-4fe0-a683-9d9e0bc9f260", "width": 187.5, @@ -7328,7 +7166,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "7daee715-9ce1-42f7-8d99-fde9a9795759", "width": 187.5, @@ -7344,7 +7181,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "383b0c13-5304-40b5-896d-b64d43f6105e", "width": 75, @@ -7360,7 +7196,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "e18107ae-7019-47cd-8f58-f53856818448", "width": 75, @@ -7376,7 +7211,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "f78e15aa-66d8-4a88-ac6f-29517c3fecda", "width": 75, @@ -7392,7 +7226,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "83813e78-620e-424c-9303-be5b2f35cb6a", "width": 75, @@ -7408,7 +7241,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "76213f1b-4b88-425d-b72a-4048fd3297e5", "width": 75, @@ -7424,7 +7256,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "b39a15f5-28fa-47ed-82cf-ea22b4700393", "width": 75, @@ -7440,7 +7271,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "05b5e96d-9cb2-4850-8280-00327e50392b", "width": 75, @@ -7456,7 +7286,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "c0338cfe-f451-404e-9540-ca0562bdf6aa", "width": 75, @@ -7472,7 +7301,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "4dba0f65-b33b-4b4b-b254-825f7778ea5b", "width": 75, @@ -7488,7 +7316,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "23abe22b-37d3-49cd-979d-01c148f908d1", "width": 75, @@ -7504,7 +7331,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "648d6a72-7831-4387-b2ea-b6b2eecb084f", "width": 75, @@ -7520,7 +7346,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "7d1263b1-5ba8-4f58-a1f5-d67842bdce92", "width": 75, @@ -7536,7 +7361,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "7878f3fa-d84d-422e-a005-1522bb755080", "width": 75, @@ -7552,7 +7376,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "0895d5d7-2044-4970-9710-e84aa27876cf", "width": 75, @@ -7568,7 +7391,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "bcae9740-2b54-426e-83bc-12a2c1a51d71", "width": 75, @@ -7584,7 +7406,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "25dc4b4f-c956-4e3d-977a-a814b342daf3", "width": 75, @@ -7600,7 +7421,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "b678b2e5-4d63-4207-9624-944a934b831b", "width": 75, @@ -7616,7 +7436,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "e30e4342-ccc1-4903-9a5c-aa757777e53e", "width": 75, @@ -7632,7 +7451,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "00f2a89e-1aab-41ec-a973-ed36d78de250", "width": 75, @@ -7648,7 +7466,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "46050504-4273-48d0-aac8-cf5c7d6e9497", "width": 75, @@ -7664,7 +7481,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "01fb6819-f07f-4c08-81af-e4e13b2f1113", "width": 75, @@ -7680,7 +7496,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "f56d15b4-a387-4f6b-8331-eb6211e101a9", "width": 75, @@ -7696,7 +7511,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "16906543-8b5a-4ba7-9477-db7c836542a8", "width": 75, @@ -7712,7 +7526,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "d8003c37-4f78-43bb-b2a3-22a6721cfd54", "width": 75, @@ -7728,7 +7541,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "f474e996-4d96-41c3-82b9-2ef49e5cb40b", "width": 75, @@ -7744,7 +7556,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "6b18129b-1ed1-4976-94ca-a6de36464145", "width": 75, @@ -7760,7 +7571,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "3fd6a780-94ef-45a5-970a-afecbe7ad6ad", "width": 75, @@ -7776,7 +7586,6 @@ "customSize": true, "height": 50, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "e3d5ea8c-405c-43a9-843e-1b518819d0de", "width": 100, @@ -7792,7 +7601,6 @@ "customSize": true, "height": 50, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "98a8bdd9-704b-4b9c-a468-2cdf5687bc0c", "width": 100, @@ -7808,7 +7616,6 @@ "customSize": true, "height": 50, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "caa42066-b296-45d8-bd4c-f80d58808008", "width": 100, @@ -7824,7 +7631,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "b807310d-514e-4be7-9b8d-90c3551327e1", "width": 300, @@ -7845,7 +7651,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "9b0ccc66-c681-45b3-952c-1a783e6e1c7f", "width": 300, @@ -7866,7 +7671,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "af7eea8a-a2b7-43f5-aeb9-96e029771d15", "width": 300, @@ -7887,7 +7691,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "979cd25f-b76c-4eb7-aa4f-a677df52dc79", "width": 300, @@ -7908,7 +7711,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "f92a245d-22c2-4c94-8813-4b9fe3ee60b4", "width": 300, @@ -7929,7 +7731,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "855ace29-6dfe-4623-837d-7b00b37e27e1", "width": 300, @@ -7950,7 +7751,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "954a937a-1f1a-4fc4-b491-538ab8483243", "width": 300, @@ -7971,7 +7771,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "98e39f11-f1e1-4c90-bbfa-48365d049d16", "width": 300, @@ -7992,7 +7791,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "cea391cf-740e-42eb-9239-fd91358c486d", "width": 300, @@ -8013,7 +7811,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "565a0e9e-99a8-407e-a270-b9c9dcd04e7f", "width": 300, @@ -8034,7 +7831,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "d4ba3dd1-8a14-4d6c-aa45-05d5a1f9de74", "width": 300, @@ -8055,7 +7851,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "d24e7c4a-b159-4fa2-a597-711d09dacfce", "width": 300, @@ -8076,7 +7871,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "b5b8f88f-56c0-419e-bf43-cdcc56194b5b", "width": 300, @@ -8097,7 +7891,6 @@ "customSize": true, "height": 300, "layer": "", - "locked": false, "name": "Floor", "persistentUuid": "1cb5f5e0-21a8-4c84-95c7-4b2ced6da56b", "width": 300, @@ -8118,7 +7911,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "2acb12d7-ab50-4159-ade9-94b0e29c10d3", "width": 0, @@ -8134,7 +7926,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "4dcbc3ca-3b5b-43a9-bf0b-e31ec232fc97", "width": 0, @@ -8150,7 +7941,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "68ddd6f6-93cf-46f5-883e-8310efae9e2d", "width": 0, @@ -8166,7 +7956,6 @@ "customSize": false, "height": 0, "layer": "", - "locked": false, "name": "Tree", "persistentUuid": "befa1c8c-78cf-4bbb-80fd-fa6de7445ccb", "width": 0, @@ -8182,7 +7971,6 @@ "customSize": true, "height": 50, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "3739281e-4fc8-40eb-abc2-19a6b65432f5", "width": 100, @@ -8198,7 +7986,6 @@ "customSize": true, "height": 50, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "12aa1ec8-f059-4d30-9cbb-1f3d9e1f9ae6", "width": 100, @@ -8214,7 +8001,6 @@ "customSize": true, "height": 50, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "a4e9b246-ab44-4e74-b504-05a426dac1dd", "width": 100, @@ -8230,7 +8016,6 @@ "customSize": true, "height": 50, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "826438fd-ac18-4e67-85cb-ce25b0040c15", "width": 100, @@ -8246,7 +8031,6 @@ "customSize": true, "height": 50, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "adac05b5-e696-4e38-85be-30dc475434a2", "width": 100, @@ -8262,7 +8046,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "da964be4-e94d-47fa-b65a-7412daf86cae", "width": 187.5, @@ -8278,7 +8061,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "e981e3a2-ee1b-446a-84e2-2a80b2cb793f", "width": 187.5, @@ -8294,7 +8076,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "fe6bed35-f485-4325-aaad-7eaa2e27e521", "width": 187.5, @@ -8310,7 +8091,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileRowObstacle", "persistentUuid": "2aa4171c-0d97-4959-8c59-d0a765c295cf", "width": 187.5, @@ -8326,7 +8106,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "28fc8e38-e86a-430b-89bf-a2189852cb45", "width": 75, @@ -8342,7 +8121,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "5ff563bb-5e41-4dca-9861-0cad23e3f3ad", "width": 187.5, @@ -8358,7 +8136,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "eb9eecaf-6ceb-4055-a186-c0b715b79a7a", "width": 187.5, @@ -8374,7 +8151,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "ab1ccfc8-852e-4881-bc26-4318ccd946c6", "width": 187.5, @@ -8390,7 +8166,6 @@ "customSize": true, "height": 93.75, "layer": "Obstacle", - "locked": false, "name": "TileColumnObstacle", "persistentUuid": "5ddde7a9-5fc5-42f1-9e67-246bfe4361a2", "width": 187.5, @@ -8406,7 +8181,6 @@ "customSize": true, "height": 37.5, "layer": "Obstacle", - "locked": false, "name": "TileObstacle", "persistentUuid": "dd945699-bb0a-4a70-b03a-fc358193694c", "width": 75, @@ -8422,7 +8196,6 @@ "customSize": true, "height": 60, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "d9394e91-e326-4833-8d11-07d1876000d6", "width": 100, @@ -8438,7 +8211,6 @@ "customSize": true, "height": 50, "layer": "Obstacle", - "locked": false, "name": "SmallObstacle", "persistentUuid": "0fa55318-e195-422c-a8ac-4c6d39a82209", "width": 100, @@ -8454,7 +8226,6 @@ "customSize": false, "height": 0, "layer": "Level2", - "locked": false, "name": "Tree", "persistentUuid": "e8eacc5f-3db9-47f7-9aa6-ecf5792ccfb8", "width": 0, @@ -8464,10 +8235,26 @@ "numberProperties": [], "stringProperties": [], "initialVariables": [] + }, + { + "angle": 0, + "customSize": false, + "height": 0, + "layer": "", + "name": "NavMesh", + "persistentUuid": "25b352c9-30bf-40ee-898b-7c1e23677a08", + "width": 0, + "x": -37.5, + "y": -56.25, + "zOrder": 5022, + "numberProperties": [], + "stringProperties": [], + "initialVariables": [] } ], "objects": [ { + "assetStoreId": "", "name": "Mindy", "tags": "", "type": "Sprite", @@ -8475,6 +8262,17 @@ "variables": [], "effects": [], "behaviors": [ + { + "name": "NavMeshPathfindingBehavior", + "type": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "Acceleration": 400, + "MaxSpeed": 200, + "AngularMaxSpeed": 180, + "RotateObject": false, + "AngleOffset": 0, + "CollisionShape": "Dot at center", + "ExtraBorder": 20 + }, { "name": "TopDownMovement", "type": "TopDownMovementBehavior::TopDownMovementBehavior", @@ -23528,6 +23326,7 @@ ] }, { + "assetStoreId": "", "name": "Bat", "tags": "", "type": "Sprite", @@ -25064,6 +24863,7 @@ ] }, { + "assetStoreId": "", "name": "TopBlock", "tags": "", "type": "Sprite", @@ -25109,6 +24909,7 @@ ] }, { + "assetStoreId": "", "name": "BlockGrass", "tags": "", "type": "Sprite", @@ -25154,6 +24955,7 @@ ] }, { + "assetStoreId": "", "name": "Block", "tags": "", "type": "Sprite", @@ -25199,6 +25001,7 @@ ] }, { + "assetStoreId": "", "name": "Bridge", "tags": "", "type": "Sprite", @@ -25239,6 +25042,7 @@ ] }, { + "assetStoreId": "", "name": "FloorGrass", "tags": "", "type": "Sprite", @@ -25303,6 +25107,7 @@ ] }, { + "assetStoreId": "", "name": "Floor", "tags": "", "type": "Sprite", @@ -25554,6 +25359,7 @@ ] }, { + "assetStoreId": "", "name": "Tree", "tags": "", "type": "Sprite", @@ -25599,6 +25405,7 @@ ] }, { + "assetStoreId": "", "name": "Rock", "tags": "", "type": "Sprite", @@ -25644,6 +25451,7 @@ ] }, { + "assetStoreId": "", "name": "WoodSign", "tags": "", "type": "Sprite", @@ -25689,6 +25497,7 @@ ] }, { + "assetStoreId": "", "height": 32, "name": "SmallObstacle", "tags": "", @@ -25705,6 +25514,7 @@ ] }, { + "assetStoreId": "", "name": "TileRowObstacle", "tags": "", "type": "Sprite", @@ -25769,6 +25579,7 @@ ] }, { + "assetStoreId": "", "name": "TileColumnObstacle", "tags": "", "type": "Sprite", @@ -25833,6 +25644,7 @@ ] }, { + "assetStoreId": "", "name": "TileObstacle", "tags": "", "type": "Sprite", @@ -25895,6 +25707,30 @@ ] } ] + }, + { + "assetStoreId": "", + "name": "NavMesh", + "tags": "", + "type": "PrimitiveDrawing::Drawer", + "variables": [], + "effects": [], + "behaviors": [], + "fillOpacity": 32, + "outlineSize": 1, + "outlineOpacity": 64, + "fillColor": { + "b": 0, + "g": 0, + "r": 0 + }, + "outlineColor": { + "b": 0, + "g": 0, + "r": 0 + }, + "absoluteCoordinates": true, + "clearBetweenFrames": false } ], "events": [ @@ -25903,29 +25739,47 @@ "conditions": [ { "type": { - "inverted": false, "value": "DepartScene" }, "parameters": [ "" - ], - "subInstructions": [] + ] } ], "actions": [ { "type": { - "inverted": false, "value": "HideLayer" }, "parameters": [ "Obstacle", "\"Obstacle\"" - ], - "subInstructions": [] + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetDestination" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "Mindy.X()", + "Mindy.Y()", + "Mindy.Y()" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::DrawNavMesh" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "NavMesh", + "\"255;187;0\"" + ] } - ], - "events": [] + ] }, { "colorB": 228, @@ -25967,31 +25821,36 @@ "conditions": [ { "type": { - "inverted": false, "value": "TopDownMovementBehavior::IsMoving" }, "parameters": [ "Mindy", "TopDownMovement" - ], - "subInstructions": [] + ] } ], "actions": [ { "type": { - "inverted": false, "value": "ChangeAnimation" }, "parameters": [ "Mindy", "=", "1 + mod(round((Mindy.TopDownMovement::Angle())/45+1), 8)" - ], - "subInstructions": [] + ] + }, + { + "type": { + "value": "ActivateBehavior" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "no" + ] } - ], - "events": [] + ] }, { "type": "BuiltinCommonInstructions::Standard", @@ -26004,25 +25863,69 @@ "parameters": [ "Mindy", "TopDownMovement" - ], - "subInstructions": [] + ] + }, + { + "type": { + "inverted": true, + "value": "BehaviorActivated" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior" + ] } ], "actions": [ { "type": { - "inverted": false, "value": "ChangeAnimation" }, "parameters": [ "Mindy", "=", "0" - ], - "subInstructions": [] + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "TopDownMovementBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "TopDownMovement" + ] + }, + { + "type": { + "inverted": true, + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "" + ] } ], - "events": [] + "actions": [ + { + "type": { + "value": "ChangeAnimation" + }, + "parameters": [ + "Mindy", + "=", + "0" + ] + } + ] }, { "type": "BuiltinCommonInstructions::Comment", @@ -26042,149 +25945,95 @@ "conditions": [ { "type": { - "inverted": false, "value": "MouseButtonPressed" }, "parameters": [ "", "Left" - ], - "subInstructions": [] + ] } ], - "actions": [], - "events": [ + "actions": [ { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "inverted": false, - "value": "MouseY" - }, - "parameters": [ - "", - "<", - "Mindy.Y() - 15", - "\"\"", - "" - ], - "subInstructions": [] - } - ], - "actions": [ - { - "type": { - "inverted": false, - "value": "TopDownMovementBehavior::SimulateUpKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ], - "subInstructions": [] - } - ], - "events": [] + "type": { + "value": "ActivateBehavior" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "yes" + ] }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "inverted": false, - "value": "MouseY" - }, - "parameters": [ - "", - ">", - "Mindy.Y() + 15", - "\"\"", - "" - ], - "subInstructions": [] - } - ], - "actions": [ - { - "type": { - "inverted": false, - "value": "TopDownMovementBehavior::SimulateDownKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ], - "subInstructions": [] - } - ], - "events": [] + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetDestination" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "MouseX(\"\", 0)", + "MouseY(\"\", 0)", + "MouseY(\"\", 0)" + ] }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "inverted": false, - "value": "MouseX" - }, - "parameters": [ - "", - "<", - "Mindy.X() - 15", - "\"\"", - "" - ], - "subInstructions": [] - } - ], - "actions": [ - { - "type": { - "inverted": false, - "value": "TopDownMovementBehavior::SimulateLeftKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ], - "subInstructions": [] - } - ], - "events": [] + "type": { + "value": "ModVarObjet" + }, + "parameters": [ + "Mindy", + "nextNodeIndex", + "=", + "0" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior" + ] }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "inverted": false, - "value": "MouseX" - }, - "parameters": [ - "", - ">", - "Mindy.X() + 15", - "\"\"", - "" - ], - "subInstructions": [] - } - ], - "actions": [ - { - "type": { - "inverted": false, - "value": "TopDownMovementBehavior::SimulateRightKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ], - "subInstructions": [] - } - ], - "events": [] + "type": { + "value": "VarObjet" + }, + "parameters": [ + "Mindy", + "nextNodeIndex", + "!=", + "Mindy.NavMeshPathfindingBehavior::NextNodeIndex()" + ] + } + ], + "actions": [ + { + "type": { + "value": "ModVarObjet" + }, + "parameters": [ + "Mindy", + "nextNodeIndex", + "=", + "Mindy.NavMeshPathfindingBehavior::NextNodeIndex()" + ] + }, + { + "type": { + "value": "ChangeAnimation" + }, + "parameters": [ + "Mindy", + "=", + "1 + mod(round((Mindy.NavMeshPathfindingBehavior::MovementAngle())/45), 8)" + ] } ] }, @@ -26206,7 +26055,6 @@ "conditions": [ { "type": { - "inverted": false, "value": "Gamepads::C_Axis_pushed" }, "parameters": [ @@ -26215,31 +26063,26 @@ "\"LEFT\"", "\"UP\"", "" - ], - "subInstructions": [] + ] } ], "actions": [ { "type": { - "inverted": false, "value": "TopDownMovementBehavior::SimulateUpKey" }, "parameters": [ "Mindy", "TopDownMovement" - ], - "subInstructions": [] + ] } - ], - "events": [] + ] }, { "type": "BuiltinCommonInstructions::Standard", "conditions": [ { "type": { - "inverted": false, "value": "Gamepads::C_Axis_pushed" }, "parameters": [ @@ -26248,31 +26091,26 @@ "\"LEFT\"", "\"DOWN\"", "" - ], - "subInstructions": [] + ] } ], "actions": [ { "type": { - "inverted": false, "value": "TopDownMovementBehavior::SimulateDownKey" }, "parameters": [ "Mindy", "TopDownMovement" - ], - "subInstructions": [] + ] } - ], - "events": [] + ] }, { "type": "BuiltinCommonInstructions::Standard", "conditions": [ { "type": { - "inverted": false, "value": "Gamepads::C_Axis_pushed" }, "parameters": [ @@ -26281,31 +26119,26 @@ "\"LEFT\"", "\"LEFT\"", "" - ], - "subInstructions": [] + ] } ], "actions": [ { "type": { - "inverted": false, "value": "TopDownMovementBehavior::SimulateLeftKey" }, "parameters": [ "Mindy", "TopDownMovement" - ], - "subInstructions": [] + ] } - ], - "events": [] + ] }, { "type": "BuiltinCommonInstructions::Standard", "conditions": [ { "type": { - "inverted": false, "value": "Gamepads::C_Axis_pushed" }, "parameters": [ @@ -26314,24 +26147,20 @@ "\"LEFT\"", "\"RIGHT\"", "" - ], - "subInstructions": [] + ] } ], "actions": [ { "type": { - "inverted": false, "value": "TopDownMovementBehavior::SimulateRightKey" }, "parameters": [ "Mindy", "TopDownMovement" - ], - "subInstructions": [] + ] } - ], - "events": [] + ] } ], "parameters": [] @@ -26351,7 +26180,6 @@ "actions": [ { "type": { - "inverted": false, "value": "CentreCamera" }, "parameters": [ @@ -26360,12 +26188,10 @@ "yes", "\"\"", "0" - ], - "subInstructions": [] + ] }, { "type": { - "inverted": false, "value": "CentreCamera" }, "parameters": [ @@ -26374,12 +26200,10 @@ "yes", "\"Level1\"", "0" - ], - "subInstructions": [] + ] }, { "type": { - "inverted": false, "value": "CentreCamera" }, "parameters": [ @@ -26388,11 +26212,9 @@ "yes", "\"Level2\"", "0" - ], - "subInstructions": [] + ] } - ], - "events": [] + ] } ], "parameters": [] @@ -26416,17 +26238,14 @@ "actions": [ { "type": { - "inverted": false, "value": "SeparateFromObjects" }, "parameters": [ "Mindy", "Obstacle" - ], - "subInstructions": [] + ] } - ], - "events": [] + ] }, { "colorB": 228, @@ -26450,14 +26269,12 @@ "Bat", "100", "" - ], - "subInstructions": [] + ] } ], "actions": [ { "type": { - "inverted": false, "value": "AddForceVersPos" }, "parameters": [ @@ -26466,11 +26283,9 @@ "Mindy.PointY(\"origin\") - 60", "150", "0" - ], - "subInstructions": [] + ] } - ], - "events": [] + ] } ], "parameters": [] @@ -26489,14 +26304,65 @@ "conditions": [ { "type": { - "inverted": false, - "value": "TopDownMovementBehavior::IsMoving" + "value": "BuiltinCommonInstructions::Or" }, - "parameters": [ - "Mindy", - "TopDownMovement" - ], - "subInstructions": [] + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "TopDownMovementBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "TopDownMovement" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::And" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "BehaviorActivated" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior" + ] + }, + { + "type": { + "value": "AjoutObjConcern" + }, + "parameters": [ + "", + "Mindy" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "" + ] + }, + { + "type": { + "value": "AjoutObjConcern" + }, + "parameters": [ + "", + "Mindy" + ] + } + ] + } + ] } ], "actions": [], @@ -26506,29 +26372,24 @@ "conditions": [ { "type": { - "inverted": false, "value": "CollisionPoint" }, "parameters": [ "FloorGrass", "Mindy.PointX(\"footcollis\")", "Mindy.PointY(\"footcollis\")" - ], - "subInstructions": [] + ] }, { "type": { - "inverted": false, "value": "BuiltinCommonInstructions::Once" }, - "parameters": [], - "subInstructions": [] + "parameters": [] } ], "actions": [ { "type": { - "inverted": false, "value": "PlaySoundCanal" }, "parameters": [ @@ -26538,198 +26399,1909 @@ "yes", "5", "" - ], - "subInstructions": [] + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CollisionPoint" + }, + "parameters": [ + "Floor", + "Mindy.PointX(\"footcollis\")", + "Mindy.PointY(\"footcollis\")" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::Once" + }, + "parameters": [] } ], - "events": [] + "actions": [ + { + "type": { + "value": "StopSoundCanal" + }, + "parameters": [ + "", + "1" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "TopDownMovementBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "TopDownMovement" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::Once" + }, + "parameters": [] + } + ], + "actions": [ + { + "type": { + "value": "StopSoundCanal" + }, + "parameters": [ + "", + "1" + ] + } + ] + } + ], + "parameters": [] + } + ], + "layers": [ + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "followBaseLayerCamera": false, + "isLightingLayer": false, + "name": "", + "visibility": true, + "cameras": [ + { + "defaultSize": true, + "defaultViewport": true, + "height": 0, + "viewportBottom": 1, + "viewportLeft": 0, + "viewportRight": 1, + "viewportTop": 0, + "width": 0 + } + ], + "effects": [] + }, + { + "ambientLightColorB": 167801455, + "ambientLightColorG": 6025424, + "ambientLightColorR": 7792048, + "followBaseLayerCamera": false, + "isLightingLayer": false, + "name": "Level1", + "visibility": true, + "cameras": [], + "effects": [] + }, + { + "ambientLightColorB": 167801455, + "ambientLightColorG": 6025424, + "ambientLightColorR": 7792048, + "followBaseLayerCamera": false, + "isLightingLayer": false, + "name": "Level2", + "visibility": true, + "cameras": [], + "effects": [] + }, + { + "ambientLightColorB": 200, + "ambientLightColorG": 200, + "ambientLightColorR": 200, + "followBaseLayerCamera": false, + "isLightingLayer": false, + "name": "UI", + "visibility": true, + "cameras": [], + "effects": [] + }, + { + "ambientLightColorB": 9845048, + "ambientLightColorG": 6025424, + "ambientLightColorR": 10009488, + "followBaseLayerCamera": false, + "isLightingLayer": false, + "name": "Obstacle", + "visibility": true, + "cameras": [], + "effects": [] + } + ], + "behaviorsSharedData": [ + { + "name": "NavMeshPathfindingBehavior", + "type": "NavMeshPathfinding::NavMeshPathfindingBehavior" + }, + { + "name": "NavMeshPathfindingObstacleBehavior", + "type": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "areaTopBound": -150, + "cellSize": 20, + "viewpoint": "Isometry 2:1 (26.565°)", + "areaBottomBound": 1700, + "areaLeftBound": -250, + "areaRightBound": 1900, + "Viewpoint": "Isometry 2:1 (26.565°)", + "AreaRightBound": 2000, + "AreaTopBound": -200, + "AreaBottomBound": 1700, + "CellSize": 10, + "AreaLeftBound": -300 + }, + { + "name": "TopDownMovement", + "type": "TopDownMovementBehavior::TopDownMovementBehavior" + }, + { + "name": "YSort", + "type": "YSort::YSort" + } + ] + } + ], + "externalEvents": [ + { + "associatedLayout": "Level 1", + "lastChangeTimeStamp": 0, + "name": "Base_Event", + "events": [] + } + ], + "eventsFunctionsExtensions": [ + { + "author": "", + "category": "Movement", + "extensionNamespace": "", + "fullName": "Navigation mesh pathfinding (experimental)", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQo8L3N0eWxlPg0KPHBvbHlsaW5lIGNsYXNzPSJzdDAiIHBvaW50cz0iMTcsMjYgNSwyNiA1LDQgMjcsNCAyNywyNiAyNSwyNiAiLz4NCjxwb2x5bGluZSBjbGFzcz0ic3QwIiBwb2ludHM9IjUsMTkgMTYsOCAyNywxOSAiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxMiIgeTE9IjQiIHgyPSIxOSIgeTI9IjExIi8+DQo8bGluZSBjbGFzcz0ic3QwIiB4MT0iMjIiIHkxPSI0IiB4Mj0iMjIiIHkyPSIxNCIvPg0KPHBhdGggY2xhc3M9InN0MCIgZD0iTTIwLjUsMTcuNWMtMS45LTEuOS01LjEtMS45LTcsMGMtMS45LDEuOS0xLjksNS4xLDAsN2MxLjQsMS41LDMuNiwxLjgsNS40LDEuMWMwLjYtMC4yLDEuMi0wLjYsMS42LTEuMQ0KCUMyMi40LDIyLjYsMjIuNCwxOS41LDIwLjUsMTcuNXoiLz4NCjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yMS41LDIzbDMuOSwzLjZjMC44LDAuOCwwLjgsMiwwLDIuOGwwLDBjLTAuOCwwLjgtMiwwLjgtMi44LDBsLTMuNS0zLjUiLz4NCjwvc3ZnPg0K", + "name": "NavMeshPathfinding", + "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/Line Hero Pack/Master/SVG/Maps and Navigation/Maps and Navigation_map_find_search.svg", + "shortDescription": "Pathfinding allows to compute an efficient path for objects, avoiding obstacles on the way.", + "version": "0.1.0", + "description": "Compare to the built-in pathfinding behavior, this one aims to:\n- better respect obstacle shapes\n- find pathes faster if obstacles don't move\n", + "tags": [ + "navmesh", + "pathfinding" + ], + "authorIds": [ + "IWykYNRvhCZBN3vEgKEbBPOR3Oc2" + ], + "dependencies": [], + "eventsFunctions": [ + { + "description": "Define JavaScript classes.", + "fullName": "Define JavaScript classes", + "functionType": "Action", + "name": "DefineJavaScript", + "private": true, + "sentence": "Define JavaScript classes", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "GlobalVariableAsBoolean" + }, + "parameters": [ + "__NavMeshPathfinding_ClassesDefined", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetGlobalVariableAsBoolean" + }, + "parameters": [ + "__NavMeshPathfinding_ClassesDefined", + "True" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nfunction __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\n/**\n * Stripped down version of Phaser's Vector2 with just the functionality needed for navmeshes.\n *\n * @export\n * @class Vector2\n */\nvar Vector2 = /** @class */ (function () {\n function Vector2(x, y) {\n if (x === void 0) { x = 0; }\n if (y === void 0) { y = 0; }\n this.x = x;\n this.y = y;\n }\n Vector2.prototype.equals = function (v) {\n return this.x === v.x && this.y === v.y;\n };\n Vector2.prototype.angle = function (v) {\n return Math.atan2(v.y - this.y, v.x - this.x);\n };\n Vector2.prototype.distance = function (v) {\n var dx = v.x - this.x;\n var dy = v.y - this.y;\n return Math.sqrt(dx * dx + dy * dy);\n };\n Vector2.prototype.add = function (v) {\n this.x += v.x;\n this.y += v.y;\n };\n Vector2.prototype.subtract = function (v) {\n this.x -= v.x;\n this.y -= v.y;\n };\n Vector2.prototype.clone = function () {\n return new Vector2(this.x, this.y);\n };\n return Vector2;\n}());\n\nvar GridNode = /** @class */ (function () {\n function GridNode(weight) {\n this.h = 0;\n this.g = 0;\n this.f = 0;\n this.closed = false;\n this.visited = false;\n this.parent = null;\n this.weight = weight;\n }\n GridNode.prototype.isWall = function () {\n return this.weight === 0;\n };\n GridNode.prototype.clean = function () {\n this.f = 0;\n this.g = 0;\n this.h = 0;\n this.visited = false;\n this.closed = false;\n this.parent = null;\n };\n return GridNode;\n}());\n\n/**\n * A class that represents a navigable polygon with a navmesh. It is built on top of a\n * {@link Polygon}. It implements the properties and fields that javascript-astar needs - weight,\n * toString, isWall and getCost. See GPS test from astar repo for structure:\n * https://github.com/bgrins/javascript-astar/blob/master/test/tests.js\n */\nvar NavPoly = /** @class */ (function (_super) {\n __extends(NavPoly, _super);\n /**\n * Creates an instance of NavPoly.\n */\n function NavPoly(id, polygon) {\n var _this = _super.call(this, 1) || this;\n _this.id = id;\n _this.polygon = polygon;\n _this.edges = polygon.edges;\n _this.neighbors = [];\n _this.portals = [];\n _this.centroid = _this.calculateCentroid();\n _this.boundingRadius = _this.calculateRadius();\n return _this;\n }\n /**\n * Returns an array of points that form the polygon.\n */\n NavPoly.prototype.getPoints = function () {\n return this.polygon.points;\n };\n /**\n * Check if the given point-like object is within the polygon.\n */\n NavPoly.prototype.contains = function (point) {\n // Phaser's polygon check doesn't handle when a point is on one of the edges of the line. Note:\n // check numerical stability here. It would also be good to optimize this for different shapes.\n return this.polygon.contains(point.x, point.y) || this.isPointOnEdge(point);\n };\n /**\n * Only rectangles are supported, so this calculation works, but this is not actually the centroid\n * calculation for a polygon. This is just the average of the vertices - proper centroid of a\n * polygon factors in the area.\n */\n NavPoly.prototype.calculateCentroid = function () {\n var centroid = new Vector2(0, 0);\n var length = this.polygon.points.length;\n this.polygon.points.forEach(function (p) { return centroid.add(p); });\n centroid.x /= length;\n centroid.y /= length;\n return centroid;\n };\n /**\n * Calculate the radius of a circle that circumscribes the polygon.\n */\n NavPoly.prototype.calculateRadius = function () {\n var boundingRadius = 0;\n for (var _i = 0, _a = this.polygon.points; _i < _a.length; _i++) {\n var point = _a[_i];\n var d = this.centroid.distance(point);\n if (d > boundingRadius)\n boundingRadius = d;\n }\n return boundingRadius;\n };\n /**\n * Check if the given point-like object is on one of the edges of the polygon.\n */\n NavPoly.prototype.isPointOnEdge = function (_a) {\n var x = _a.x, y = _a.y;\n for (var _i = 0, _b = this.edges; _i < _b.length; _i++) {\n var edge = _b[_i];\n if (edge.pointOnSegment(x, y))\n return true;\n }\n return false;\n };\n NavPoly.prototype.destroy = function () {\n this.neighbors = [];\n this.portals = [];\n };\n // === jsastar methods ===\n NavPoly.prototype.toString = function () {\n return \"NavPoly(id: \" + this.id + \" at: \" + this.centroid + \")\";\n };\n NavPoly.prototype.isWall = function () {\n return false;\n };\n NavPoly.prototype.centroidDistance = function (navPolygon) {\n return this.centroid.distance(navPolygon.centroid);\n };\n NavPoly.prototype.getCost = function (navPolygon) {\n //TODO the cost method should not be in the Node\n return this.centroidDistance(navPolygon);\n };\n return NavPoly;\n}(GridNode));\n\n/**\n * A graph memory structure\n */\nvar Graph = /** @class */ (function () {\n /**\n * A graph memory structure\n * @param {Array} gridIn 2D array of input weights\n * @param {Object} [options]\n * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed\n */\n function Graph(nodes, options) {\n this.dirtyNodes = [];\n options = options || {};\n this.nodes = nodes;\n this.diagonal = !!options.diagonal;\n this.init();\n }\n Graph.prototype.init = function () {\n this.dirtyNodes = [];\n for (var i = 0; i < this.nodes.length; i++) {\n this.nodes[i].clean();\n }\n };\n Graph.prototype.cleanDirty = function () {\n for (var i = 0; i < this.dirtyNodes.length; i++) {\n this.dirtyNodes[i].clean();\n }\n this.dirtyNodes = [];\n };\n Graph.prototype.markDirty = function (node) {\n this.dirtyNodes.push(node);\n };\n return Graph;\n}());\n\n/**\n * Graph for javascript-astar. It implements the functionality for astar. See GPS test from astar\n * repo for structure: https://github.com/bgrins/javascript-astar/blob/master/test/tests.js\n *\n * @class NavGraph\n * @private\n */\nvar NavGraph = /** @class */ (function (_super) {\n __extends(NavGraph, _super);\n function NavGraph(navPolygons) {\n var _this = _super.call(this, navPolygons) || this;\n _this.nodes = navPolygons;\n _this.init();\n return _this;\n }\n NavGraph.prototype.neighbors = function (navPolygon) {\n return navPolygon.neighbors;\n };\n NavGraph.prototype.navHeuristic = function (navPolygon1, navPolygon2) {\n return navPolygon1.centroidDistance(navPolygon2);\n };\n NavGraph.prototype.destroy = function () {\n this.cleanDirty();\n this.nodes = [];\n };\n return NavGraph;\n}(Graph));\n\n/**\n * Calculate the distance squared between two points. This is an optimization to a square root when\n * you just need to compare relative distances without needing to know the specific distance.\n * @param a\n * @param b\n */\nfunction distanceSquared(a, b) {\n var dx = b.x - a.x;\n var dy = b.y - a.y;\n return dx * dx + dy * dy;\n}\n/**\n * Project a point onto a line segment.\n * JS Source: http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment\n * @param point\n * @param line\n */\nfunction projectPointToEdge(point, line) {\n var a = line.start;\n var b = line.end;\n // Consider the parametric equation for the edge's line, p = a + t (b - a). We want to find\n // where our point lies on the line by solving for t:\n // t = [(p-a) . (b-a)] / |b-a|^2\n var l2 = distanceSquared(a, b);\n var t = ((point.x - a.x) * (b.x - a.x) + (point.y - a.y) * (b.y - a.y)) / l2;\n // We clamp t from [0,1] to handle points outside the segment vw.\n t = clamp(t, 0, 1);\n // Project onto the segment\n var p = new Vector2(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y));\n return p;\n}\n/**\n * Twice the area of the triangle formed by a, b and c.\n */\nfunction triarea2(a, b, c) {\n var ax = b.x - a.x;\n var ay = b.y - a.y;\n var bx = c.x - a.x;\n var by = c.y - a.y;\n return bx * ay - ax * by;\n}\n/**\n * Clamp the given value between min and max.\n */\nfunction clamp(value, min, max) {\n if (value < min)\n value = min;\n if (value > max)\n value = max;\n return value;\n}\n/**\n * Check if two values are within a small margin of one another.\n */\nfunction almostEqual(value1, value2, errorMargin) {\n if (errorMargin === void 0) { errorMargin = 0.0001; }\n if (Math.abs(value1 - value2) <= errorMargin)\n return true;\n else\n return false;\n}\n/**\n * Find the smallest angle difference between two angles\n * https://gist.github.com/Aaronduino/4068b058f8dbc34b4d3a9eedc8b2cbe0\n */\nfunction angleDifference(x, y) {\n var a = x - y;\n var i = a + Math.PI;\n var j = Math.PI * 2;\n a = i - Math.floor(i / j) * j; // (a+180) % 360; this ensures the correct sign\n a -= Math.PI;\n return a;\n}\n/**\n * Check if two lines are collinear (within a small error margin).\n */\nfunction areCollinear(line1, line2, errorMargin) {\n if (errorMargin === void 0) { errorMargin = 0.0001; }\n // Figure out if the two lines are equal by looking at the area of the triangle formed\n // by their points\n var area1 = triarea2(line1.start, line1.end, line2.start);\n var area2 = triarea2(line1.start, line1.end, line2.end);\n if (almostEqual(area1, 0, errorMargin) && almostEqual(area2, 0, errorMargin)) {\n return true;\n }\n else\n return false;\n}\n\n// Mostly sourced from PatrolJS at the moment. TODO: come back and reimplement this as an incomplete\n/**\n * @private\n */\nvar Channel = /** @class */ (function () {\n function Channel() {\n this.portals = [];\n this.path = [];\n }\n Channel.prototype.push = function (p1, p2) {\n if (p2 === undefined)\n p2 = p1;\n this.portals.push({\n left: p1,\n right: p2,\n });\n };\n Channel.prototype.stringPull = function () {\n var portals = this.portals;\n var pts = [];\n // Init scan state\n var apexIndex = 0;\n var leftIndex = 0;\n var rightIndex = 0;\n var portalApex = portals[0].left;\n var portalLeft = portals[0].left;\n var portalRight = portals[0].right;\n // Add start point.\n pts.push(portalApex);\n for (var i = 1; i < portals.length; i++) {\n // Find the next portal vertices\n var left = portals[i].left;\n var right = portals[i].right;\n // Update right vertex.\n if (triarea2(portalApex, portalRight, right) <= 0.0) {\n if (portalApex.equals(portalRight) || triarea2(portalApex, portalLeft, right) > 0.0) {\n // Tighten the funnel.\n portalRight = right;\n rightIndex = i;\n }\n else {\n // Right vertex just crossed over the left vertex, so the left vertex should\n // now be part of the path.\n pts.push(portalLeft);\n // Restart scan from portal left point.\n // Make current left the new apex.\n portalApex = portalLeft;\n apexIndex = leftIndex;\n // Reset portal\n portalLeft = portalApex;\n portalRight = portalApex;\n leftIndex = apexIndex;\n rightIndex = apexIndex;\n // Restart scan\n i = apexIndex;\n continue;\n }\n }\n // Update left vertex.\n if (triarea2(portalApex, portalLeft, left) >= 0.0) {\n if (portalApex.equals(portalLeft) || triarea2(portalApex, portalRight, left) < 0.0) {\n // Tighten the funnel.\n portalLeft = left;\n leftIndex = i;\n }\n else {\n // Left vertex just crossed over the right vertex, so the right vertex should\n // now be part of the path\n pts.push(portalRight);\n // Restart scan from portal right point.\n // Make current right the new apex.\n portalApex = portalRight;\n apexIndex = rightIndex;\n // Reset portal\n portalLeft = portalApex;\n portalRight = portalApex;\n leftIndex = apexIndex;\n rightIndex = apexIndex;\n // Restart scan\n i = apexIndex;\n continue;\n }\n }\n }\n if (pts.length === 0 || !pts[pts.length - 1].equals(portals[portals.length - 1].left)) {\n // Append last point to path.\n pts.push(portals[portals.length - 1].left);\n }\n this.path = pts;\n return pts;\n };\n return Channel;\n}());\n\n/**\n * Stripped down version of Phaser's Line with just the functionality needed for navmeshes.\n *\n * @export\n * @class Line\n */\nvar Line = /** @class */ (function () {\n function Line(x1, y1, x2, y2) {\n this.start = new Vector2(x1, y1);\n this.end = new Vector2(x2, y2);\n this.left = Math.min(x1, x2);\n this.right = Math.max(x1, x2);\n this.top = Math.min(y1, y2);\n this.bottom = Math.max(y1, y2);\n }\n Line.prototype.pointOnSegment = function (x, y) {\n return (x >= this.left &&\n x <= this.right &&\n y >= this.top &&\n y <= this.bottom &&\n this.pointOnLine(x, y));\n };\n Line.prototype.pointOnLine = function (x, y) {\n // Compare slope of line start -> xy to line start -> line end\n return (x - this.left) * (this.bottom - this.top) === (this.right - this.left) * (y - this.top);\n };\n return Line;\n}());\n\n/**\n * Stripped down version of Phaser's Polygon with just the functionality needed for navmeshes.\n *\n * @export\n * @class Polygon\n */\nvar Polygon = /** @class */ (function () {\n function Polygon(points, closed) {\n if (closed === void 0) { closed = true; }\n this.isClosed = closed;\n this.points = points;\n this.edges = [];\n for (var i = 1; i < points.length; i++) {\n var p1 = points[i - 1];\n var p2 = points[i];\n this.edges.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n if (this.isClosed) {\n var first = points[0];\n var last = points[points.length - 1];\n this.edges.push(new Line(first.x, first.y, last.x, last.y));\n }\n }\n Polygon.prototype.contains = function (x, y) {\n var inside = false;\n for (var i = -1, j = this.points.length - 1; ++i < this.points.length; j = i) {\n var ix = this.points[i].x;\n var iy = this.points[i].y;\n var jx = this.points[j].x;\n var jy = this.points[j].y;\n if (((iy <= y && y < jy) || (jy <= y && y < iy)) &&\n x < ((jx - ix) * (y - iy)) / (jy - iy) + ix) {\n inside = !inside;\n }\n }\n return inside;\n };\n return Polygon;\n}());\n\nvar BinaryHeap = /** @class */ (function () {\n function BinaryHeap(scoreFunction) {\n this.content = new Array();\n this.scoreFunction = scoreFunction;\n }\n BinaryHeap.prototype.push = function (element) {\n // Add the new element to the end of the array.\n this.content.push(element);\n // Allow it to sink down.\n this.sinkDown(this.content.length - 1);\n };\n BinaryHeap.prototype.pop = function () {\n // Store the first element so we can return it later.\n var result = this.content[0];\n // Get the element at the end of the array.\n var end = this.content.pop();\n if (!end)\n return;\n // If there are any elements left, put the end element at the\n // start, and let it bubble up.\n if (this.content.length > 0) {\n this.content[0] = end;\n this.bubbleUp(0);\n }\n return result;\n };\n BinaryHeap.prototype.remove = function (node) {\n var i = this.content.indexOf(node);\n // When it is found, the process seen in 'pop' is repeated\n // to fill up the hole.\n var end = this.content.pop();\n if (!end)\n return;\n if (i !== this.content.length - 1) {\n this.content[i] = end;\n if (this.scoreFunction(end) < this.scoreFunction(node)) {\n this.sinkDown(i);\n }\n else {\n this.bubbleUp(i);\n }\n }\n };\n BinaryHeap.prototype.size = function () {\n return this.content.length;\n };\n BinaryHeap.prototype.rescoreElement = function (node) {\n this.sinkDown(this.content.indexOf(node));\n };\n BinaryHeap.prototype.sinkDown = function (n) {\n // Fetch the element that has to be sunk.\n var element = this.content[n];\n // When at 0, an element can not sink any further.\n while (n > 0) {\n // Compute the parent element's index, and fetch it.\n var parentN = ((n + 1) >> 1) - 1;\n var parent = this.content[parentN];\n // Swap the elements if the parent is greater.\n if (this.scoreFunction(element) < this.scoreFunction(parent)) {\n this.content[parentN] = element;\n this.content[n] = parent;\n // Update 'n' to continue at the new position.\n n = parentN;\n }\n // Found a parent that is less, no need to sink any further.\n else {\n break;\n }\n }\n };\n BinaryHeap.prototype.bubbleUp = function (n) {\n // Look up the target element and its score.\n var length = this.content.length;\n var element = this.content[n];\n var elemScore = this.scoreFunction(element);\n while (true) {\n // Compute the indices of the child elements.\n var child2N = (n + 1) << 1;\n var child1N = child2N - 1;\n // This is used to store the new position of the element, if any.\n var swap = null;\n var child1Score = 0;\n // If the first child exists (is inside the array)...\n if (child1N < length) {\n // Look it up and compute its score.\n var child1 = this.content[child1N];\n child1Score = this.scoreFunction(child1);\n // If the score is less than our element's, we need to swap.\n if (child1Score < elemScore) {\n swap = child1N;\n }\n }\n // Do the same checks for the other child.\n if (child2N < length) {\n var child2 = this.content[child2N];\n var child2Score = this.scoreFunction(child2);\n if (child2Score < (swap === null ? elemScore : child1Score)) {\n swap = child2N;\n }\n }\n // If the element needs to be moved, swap it, and continue.\n if (swap !== null) {\n this.content[n] = this.content[swap];\n this.content[swap] = element;\n n = swap;\n }\n // Otherwise, we are done.\n else {\n break;\n }\n }\n };\n return BinaryHeap;\n}());\n\n// The following implementation of the A* algorithm is from:\nvar AStar = /** @class */ (function () {\n function AStar() {\n }\n /**\n * Perform an A* Search on a graph given a start and end node.\n * @param {Graph} graph\n * @param {GridNode} start\n * @param {GridNode} end\n * @param {Object} [options]\n * @param {bool} [options.closest] Specifies whether to return the\n path to the closest node if the target is unreachable.\n * @param {Function} [options.heuristic] Heuristic function (see\n * astar.heuristics).\n */\n AStar.prototype.search = function (graph, start, end, \n // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html\n heuristic, closest) {\n if (closest === void 0) { closest = false; }\n graph.cleanDirty();\n var openHeap = this.getHeap();\n var closestNode = start; // set the start node to be the closest if required\n start.h = heuristic(start, end);\n graph.markDirty(start);\n openHeap.push(start);\n while (openHeap.size() > 0) {\n // Grab the lowest f(x) to process next. Heap keeps this sorted for us.\n var currentNode = openHeap.pop();\n // never happen\n if (!currentNode)\n return [];\n // End case -- result has been found, return the traced path.\n if (currentNode === end) {\n return this.pathTo(currentNode);\n }\n // Normal case -- move currentNode from open to closed, process each of its neighbors.\n currentNode.closed = true;\n // Find all neighbors for the current node.\n var neighbors = graph.neighbors(currentNode);\n for (var i = 0, il = neighbors.length; i < il; ++i) {\n var neighbor = neighbors[i];\n if (neighbor.closed || neighbor.isWall()) {\n // Not a valid node to process, skip to next neighbor.\n continue;\n }\n // The g score is the shortest distance from start to current node.\n // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.\n var gScore = currentNode.g + neighbor.getCost(currentNode);\n var beenVisited = neighbor.visited;\n if (!beenVisited || gScore < neighbor.g) {\n // Found an optimal (so far) path to this node. Take score for node to see how good it is.\n neighbor.visited = true;\n neighbor.parent = currentNode;\n neighbor.h = neighbor.h || heuristic(neighbor, end);\n neighbor.g = gScore;\n neighbor.f = neighbor.g + neighbor.h;\n graph.markDirty(neighbor);\n if (closest) {\n // If the neighbor is closer than the current closestNode or if it's equally close but has\n // a cheaper path than the current closest node then it becomes the closest node\n if (neighbor.h < closestNode.h ||\n (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) {\n closestNode = neighbor;\n }\n }\n if (!beenVisited) {\n // Pushing to heap will put it in proper place based on the 'f' value.\n openHeap.push(neighbor);\n }\n else {\n // Already seen the node, but since it has been rescored we need to reorder it in the heap\n openHeap.rescoreElement(neighbor);\n }\n }\n }\n }\n if (closest) {\n return this.pathTo(closestNode);\n }\n // No result was found - empty array signifies failure to find path.\n return [];\n };\n AStar.prototype.pathTo = function (node) {\n var curr = node;\n var path = new Array();\n while (curr.parent) {\n path.unshift(curr);\n curr = curr.parent;\n }\n return path;\n };\n AStar.prototype.getHeap = function () {\n return new BinaryHeap(function (node) {\n return node.f;\n });\n };\n return AStar;\n}());\n\n/**\n * The `NavMesh` class is the workhorse that represents a navigation mesh built from a series of\n * polygons. Once built, the mesh can be asked for a path from one point to another point. Some\n * internal terminology usage:\n * - neighbor: a polygon that shares part of an edge with another polygon\n * - portal: when two neighbor's have edges that overlap, the portal is the overlapping line segment\n * - channel: the path of polygons from starting point to end point\n * - pull the string: run the funnel algorithm on the channel so that the path hugs the edges of the\n * channel. Equivalent to having a string snaking through a hallway and then pulling it taut.\n */\nvar NavMesh = /** @class */ (function () {\n /**\n * @param meshPolygonPoints Array where each element is an array of point-like objects that\n * defines a polygon.\n * @param meshShrinkAmount The amount (in pixels) that the navmesh has been shrunk around\n * obstacles (a.k.a the amount obstacles have been expanded).\n */\n function NavMesh(meshPolygonPoints, meshShrinkAmount) {\n if (meshShrinkAmount === void 0) { meshShrinkAmount = 0; }\n this.meshShrinkAmount = meshShrinkAmount;\n // Convert the PolyPoints[] into NavPoly instances.\n this.navPolygons = meshPolygonPoints.map(function (polyPoints, i) { return new NavPoly(i, new Polygon(polyPoints)); });\n this.calculateNeighbors();\n // Astar graph of connections between polygons\n this.graph = new NavGraph(this.navPolygons);\n }\n /**\n * Get the NavPolys that are in this navmesh.\n */\n NavMesh.prototype.getPolygons = function () {\n return this.navPolygons;\n };\n /**\n * Cleanup method to remove references.\n */\n NavMesh.prototype.destroy = function () {\n this.graph.destroy();\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var poly = _a[_i];\n poly.destroy();\n }\n this.navPolygons = [];\n };\n /**\n * Find if the given point is within any of the polygons in the mesh.\n * @param point\n */\n NavMesh.prototype.isPointInMesh = function (point) {\n return this.navPolygons.some(function (navPoly) { return navPoly.contains(point); });\n };\n /**\n * Find the closest point in the mesh to the given point. If the point is already in the mesh,\n * this will give you that point. If the point is outside of the mesh, this will attempt to\n * project this point into the mesh (up to the given maxAllowableDist). This returns an object\n * with:\n * - distance - from the given point to the mesh\n * - polygon - the one the point is closest to, or null\n * - point - the point inside the mesh, or null\n * @param point\n * @param maxAllowableDist\n */\n NavMesh.prototype.findClosestMeshPoint = function (point, maxAllowableDist) {\n if (maxAllowableDist === void 0) { maxAllowableDist = Number.POSITIVE_INFINITY; }\n var minDistance = maxAllowableDist;\n var closestPoly = null;\n var pointOnClosestPoly = null;\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var navPoly = _a[_i];\n // If we are inside a poly, we've got the closest.\n if (navPoly.contains(point)) {\n minDistance = 0;\n closestPoly = navPoly;\n pointOnClosestPoly = point;\n break;\n }\n // Is the poly close enough to warrant a more accurate check? Point is definitely outside of\n // the polygon. Distance - Radius is the smallest possible distance to an edge of the poly.\n // This will underestimate distance, but that's perfectly fine.\n var r = navPoly.boundingRadius;\n var d = navPoly.centroid.distance(point);\n if (d - r < minDistance) {\n var result = this.projectPointToPolygon(point, navPoly);\n if (result.distance < minDistance) {\n minDistance = result.distance;\n closestPoly = navPoly;\n pointOnClosestPoly = result.point;\n }\n }\n }\n return { distance: minDistance, polygon: closestPoly, point: pointOnClosestPoly };\n };\n /**\n * Find a path from the start point to the end point using this nav mesh.\n * @param startPoint A point-like object in the form {x, y}\n * @param endPoint A point-like object in the form {x, y}\n * @returns An array of points if a path is found, or null if no path\n */\n NavMesh.prototype.findPath = function (startPoint, endPoint) {\n var startPoly = null;\n var endPoly = null;\n var startDistance = Number.MAX_VALUE;\n var endDistance = Number.MAX_VALUE;\n var d, r;\n var startVector = new Vector2(startPoint.x, startPoint.y);\n var endVector = new Vector2(endPoint.x, endPoint.y);\n // Find the closest poly for the starting and ending point\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var navPoly = _a[_i];\n r = navPoly.boundingRadius;\n // Start\n d = navPoly.centroid.distance(startVector);\n if (d <= startDistance && d <= r && navPoly.contains(startVector)) {\n startPoly = navPoly;\n startDistance = d;\n }\n // End\n d = navPoly.centroid.distance(endVector);\n if (d <= endDistance && d <= r && navPoly.contains(endVector)) {\n endPoly = navPoly;\n endDistance = d;\n }\n }\n // If the end point wasn't inside a polygon, run a more liberal check that allows a point\n // to be within meshShrinkAmount radius of a polygon\n if (!endPoly && this.meshShrinkAmount > 0) {\n for (var _b = 0, _c = this.navPolygons; _b < _c.length; _b++) {\n var navPoly = _c[_b];\n r = navPoly.boundingRadius + this.meshShrinkAmount;\n d = navPoly.centroid.distance(endVector);\n if (d <= r) {\n var distance = this.projectPointToPolygon(endVector, navPoly).distance;\n if (distance <= this.meshShrinkAmount && distance < endDistance) {\n endPoly = navPoly;\n endDistance = distance;\n }\n }\n }\n }\n // No matching polygons locations for the end, so no path found\n // because start point is valid normally, check end point first\n if (!endPoly)\n return null;\n // Same check as above, but for the start point\n if (!startPoly && this.meshShrinkAmount > 0) {\n for (var _d = 0, _e = this.navPolygons; _d < _e.length; _d++) {\n var navPoly = _e[_d];\n // Check if point is within bounding circle to avoid extra projection calculations\n r = navPoly.boundingRadius + this.meshShrinkAmount;\n d = navPoly.centroid.distance(startVector);\n if (d <= r) {\n // Check if projected point is within range of a polygon and is closer than the\n // previous point\n var distance = this.projectPointToPolygon(startVector, navPoly).distance;\n if (distance <= this.meshShrinkAmount && distance < startDistance) {\n startPoly = navPoly;\n startDistance = distance;\n }\n }\n }\n }\n // No matching polygons locations for the start, so no path found\n if (!startPoly)\n return null;\n // If the start and end polygons are the same, return a direct path\n if (startPoly === endPoly)\n return [startVector, endVector];\n // Search!\n var astarPath = new AStar().search(this.graph, startPoly, endPoly, this.graph.navHeuristic);\n // While the start and end polygons may be valid, no path between them\n if (astarPath.length === 0)\n return null;\n // jsastar drops the first point from the path, but the funnel algorithm needs it\n astarPath.unshift(startPoly);\n // We have a path, so now time for the funnel algorithm\n var channel = new Channel();\n channel.push(startVector);\n for (var i = 0; i < astarPath.length - 1; i++) {\n var navPolygon = astarPath[i];\n var nextNavPolygon = astarPath[i + 1];\n // Find the portal\n var portal = null;\n for (var i_1 = 0; i_1 < navPolygon.neighbors.length; i_1++) {\n if (navPolygon.neighbors[i_1].id === nextNavPolygon.id) {\n portal = navPolygon.portals[i_1];\n }\n }\n if (!portal)\n throw new Error(\"Path was supposed to be found, but portal is missing!\");\n // Push the portal vertices into the channel\n channel.push(portal.start, portal.end);\n }\n channel.push(endVector);\n // Pull a string along the channel to run the funnel\n channel.stringPull();\n // Clone path, excluding duplicates\n var lastPoint = null;\n var phaserPath = new Array();\n for (var _f = 0, _g = channel.path; _f < _g.length; _f++) {\n var p = _g[_f];\n var newPoint = p.clone();\n if (!lastPoint || !newPoint.equals(lastPoint))\n phaserPath.push(newPoint);\n lastPoint = newPoint;\n }\n return phaserPath;\n };\n NavMesh.prototype.calculateNeighbors = function () {\n // Fill out the neighbor information for each navpoly\n for (var i = 0; i < this.navPolygons.length; i++) {\n var navPoly = this.navPolygons[i];\n for (var j = i + 1; j < this.navPolygons.length; j++) {\n var otherNavPoly = this.navPolygons[j];\n // Check if the other navpoly is within range to touch\n var d = navPoly.centroid.distance(otherNavPoly.centroid);\n if (d > navPoly.boundingRadius + otherNavPoly.boundingRadius)\n continue;\n // The are in range, so check each edge pairing\n for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {\n var edge = _a[_i];\n for (var _b = 0, _c = otherNavPoly.edges; _b < _c.length; _b++) {\n var otherEdge = _c[_b];\n // If edges aren't collinear, not an option for connecting navpolys\n if (!areCollinear(edge, otherEdge))\n continue;\n // If they are collinear, check if they overlap\n var overlap = this.getSegmentOverlap(edge, otherEdge);\n if (!overlap)\n continue;\n // Connections are symmetric!\n navPoly.neighbors.push(otherNavPoly);\n otherNavPoly.neighbors.push(navPoly);\n // Calculate the portal between the two polygons - this needs to be in\n // counter-clockwise order, relative to each polygon\n var p1 = overlap[0], p2 = overlap[1];\n var edgeStartAngle = navPoly.centroid.angle(edge.start);\n var a1 = navPoly.centroid.angle(overlap[0]);\n var a2 = navPoly.centroid.angle(overlap[1]);\n var d1 = angleDifference(edgeStartAngle, a1);\n var d2 = angleDifference(edgeStartAngle, a2);\n if (d1 < d2) {\n navPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n else {\n navPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));\n }\n edgeStartAngle = otherNavPoly.centroid.angle(otherEdge.start);\n a1 = otherNavPoly.centroid.angle(overlap[0]);\n a2 = otherNavPoly.centroid.angle(overlap[1]);\n d1 = angleDifference(edgeStartAngle, a1);\n d2 = angleDifference(edgeStartAngle, a2);\n if (d1 < d2) {\n otherNavPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n else {\n otherNavPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));\n }\n // Two convex polygons shouldn't be connected more than once! (Unless\n // there are unnecessary vertices...)\n }\n }\n }\n }\n };\n // Check two collinear line segments to see if they overlap by sorting the points.\n // Algorithm source: http://stackoverflow.com/a/17152247\n NavMesh.prototype.getSegmentOverlap = function (line1, line2) {\n var points = [\n { line: line1, point: line1.start },\n { line: line1, point: line1.end },\n { line: line2, point: line2.start },\n { line: line2, point: line2.end },\n ];\n points.sort(function (a, b) {\n if (a.point.x < b.point.x)\n return -1;\n else if (a.point.x > b.point.x)\n return 1;\n else {\n if (a.point.y < b.point.y)\n return -1;\n else if (a.point.y > b.point.y)\n return 1;\n else\n return 0;\n }\n });\n // If the first two points in the array come from the same line, no overlap\n var noOverlap = points[0].line === points[1].line;\n // If the two middle points in the array are the same coordinates, then there is a\n // single point of overlap.\n var singlePointOverlap = points[1].point.equals(points[2].point);\n if (noOverlap || singlePointOverlap)\n return null;\n else\n return [points[1].point, points[2].point];\n };\n /**\n * Project a point onto a polygon in the shortest distance possible.\n *\n * @param {Phaser.Point} point The point to project\n * @param {NavPoly} navPoly The navigation polygon to test against\n * @returns {{point: Phaser.Point, distance: number}}\n */\n NavMesh.prototype.projectPointToPolygon = function (point, navPoly) {\n var closestProjection = null;\n var closestDistance = Number.MAX_VALUE;\n for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {\n var edge = _a[_i];\n var projectedPoint = projectPointToEdge(point, edge);\n var d = point.distance(projectedPoint);\n if (closestProjection === null || d < closestDistance) {\n closestDistance = d;\n closestProjection = projectedPoint;\n }\n }\n return { point: closestProjection, distance: closestDistance };\n };\n return NavMesh;\n}());\n\n/**\n * This implementation is strongly inspired from CritterAI class \"Geometry\".\n */\nvar Geometry = /** @class */ (function () {\n function Geometry() {\n }\n /**\n * Returns TRUE if line segment AB intersects with line segment CD in any\n * manner. Either collinear or at a single point.\n * @param ax The x-value for point (ax, ay) in line segment AB.\n * @param ay The y-value for point (ax, ay) in line segment AB.\n * @param bx The x-value for point (bx, by) in line segment AB.\n * @param by The y-value for point (bx, by) in line segment AB.\n * @param cx The x-value for point (cx, cy) in line segment CD.\n * @param cy The y-value for point (cx, cy) in line segment CD.\n * @param dx The x-value for point (dx, dy) in line segment CD.\n * @param dy The y-value for point (dx, dy) in line segment CD.\n * @return TRUE if line segment AB intersects with line segment CD in any\n * manner.\n */\n Geometry.segmentsIntersect = function (ax, ay, bx, by, cx, cy, dx, dy) {\n // This is modified 2D line-line intersection/segment-segment\n // intersection test.\n var deltaABx = bx - ax;\n var deltaABy = by - ay;\n var deltaCAx = ax - cx;\n var deltaCAy = ay - cy;\n var deltaCDx = dx - cx;\n var deltaCDy = dy - cy;\n var numerator = deltaCAy * deltaCDx - deltaCAx * deltaCDy;\n var denominator = deltaABx * deltaCDy - deltaABy * deltaCDx;\n // Perform early exit tests.\n if (denominator === 0 && numerator !== 0) {\n // If numerator is zero, then the lines are colinear.\n // Since it isn't, then the lines must be parallel.\n return false;\n }\n // Lines intersect. But do the segments intersect?\n // Forcing float division on both of these via casting of the\n // denominator.\n var factorAB = numerator / denominator;\n var factorCD = (deltaCAy * deltaABx - deltaCAx * deltaABy) / denominator;\n // Determine the type of intersection\n if (factorAB >= 0.0 &&\n factorAB <= 1.0 &&\n factorCD >= 0.0 &&\n factorCD <= 1.0) {\n return true; // The two segments intersect.\n }\n // The lines intersect, but segments to not.\n return false;\n };\n /**\n * Returns the distance squared from the point to the line segment.\n *\n * Behavior is undefined if the the closest distance is outside the\n * line segment.\n *\n * @param px The x-value of point (px, py).\n * @param py The y-value of point (px, py)\n * @param ax The x-value of the line segment's vertex A.\n * @param ay The y-value of the line segment's vertex A.\n * @param bx The x-value of the line segment's vertex B.\n * @param by The y-value of the line segment's vertex B.\n * @return The distance squared from the point (px, py) to line segment AB.\n */\n Geometry.getPointSegmentDistanceSq = function (px, py, ax, ay, bx, by) {\n // Reference: http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/\n //\n // The goal of the algorithm is to find the point on line segment AB\n // that is closest to P and then calculate the distance between P\n // and that point.\n var deltaABx = bx - ax;\n var deltaABy = by - ay;\n var deltaAPx = px - ax;\n var deltaAPy = py - ay;\n var segmentABLengthSq = deltaABx * deltaABx + deltaABy * deltaABy;\n if (segmentABLengthSq === 0) {\n // AB is not a line segment. So just return\n // distanceSq from P to A\n return deltaAPx * deltaAPx + deltaAPy * deltaAPy;\n }\n var u = (deltaAPx * deltaABx + deltaAPy * deltaABy) / segmentABLengthSq;\n if (u < 0) {\n // Closest point on line AB is outside outside segment AB and\n // closer to A. So return distanceSq from P to A.\n return deltaAPx * deltaAPx + deltaAPy * deltaAPy;\n }\n else if (u > 1) {\n // Closest point on line AB is outside segment AB and closer to B.\n // So return distanceSq from P to B.\n return (px - bx) * (px - bx) + (py - by) * (py - by);\n }\n // Closest point on lineAB is inside segment AB. So find the exact\n // point on AB and calculate the distanceSq from it to P.\n // The calculation in parenthesis is the location of the point on\n // the line segment.\n var deltaX = ax + u * deltaABx - px;\n var deltaY = ay + u * deltaABy - py;\n return deltaX * deltaX + deltaY * deltaY;\n };\n return Geometry;\n}());\n\n/**\n * A cell that holds data needed by the 1st steps of the NavMesh generation.\n */\nvar RasterizationCell = /** @class */ (function () {\n function RasterizationCell(x, y) {\n /**\n * 0 means there is an obstacle in the cell.\n * See {@link RegionGenerator}\n */\n this.distanceToObstacle = Number.MAX_VALUE;\n this.regionID = RasterizationCell.NULL_REGION_ID;\n this.distanceToRegionCore = 0;\n /**\n * If a cell is connected to one or more external regions then the\n * flag will be a 4 bit value where connections are recorded as\n * follows:\n * - bit1 = neighbor0\n * - bit2 = neighbor1\n * - bit3 = neighbor2\n * - bit4 = neighbor3\n * With the meaning of the bits as follows:\n * - 0 = neighbor in same region.\n * - 1 = neighbor not in same region (neighbor may be the obstacle\n * region or a real region).\n *\n * See {@link ContourBuilder}\n */\n this.contourFlags = 0;\n this.x = x;\n this.y = y;\n this.clear();\n }\n RasterizationCell.prototype.clear = function () {\n this.distanceToObstacle = Number.MAX_VALUE;\n this.regionID = RasterizationCell.NULL_REGION_ID;\n this.distanceToRegionCore = 0;\n this.contourFlags = 0;\n };\n /** A cell that has not been assigned to any region yet */\n RasterizationCell.NULL_REGION_ID = 0;\n /**\n * A cell that contains an obstacle.\n *\n * The value is the same as NULL_REGION_ID because the cells that are\n * not assigned to any region at the end of the flooding algorithm are\n * the obstacle cells.\n */\n RasterizationCell.OBSTACLE_REGION_ID = 0;\n return RasterizationCell;\n}());\n\nvar RasterizationGrid = /** @class */ (function () {\n function RasterizationGrid(left, top, right, bottom, cellWidth, cellHeight) {\n this.regionCount = 0;\n this.cellWidth = cellWidth;\n this.cellHeight = cellHeight;\n this.originX = left - cellWidth;\n this.originY = top - cellHeight;\n var dimX = 2 + Math.ceil((right - left) / cellWidth);\n var dimY = 2 + Math.ceil((bottom - top) / cellHeight);\n this.cells = [];\n for (var y = 0; y < dimY; y++) {\n this.cells[y] = [];\n for (var x = 0; x < dimX; x++) {\n this.cells[y][x] = new RasterizationCell(x, y);\n }\n }\n }\n RasterizationGrid.prototype.clear = function () {\n for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {\n var row = _a[_i];\n for (var _b = 0, row_1 = row; _b < row_1.length; _b++) {\n var cell = row_1[_b];\n cell.clear();\n }\n }\n this.regionCount = 0;\n };\n /**\n *\n * @param position the position on the scene\n * @param gridPosition the position on the grid\n * @returns the position on the grid\n */\n RasterizationGrid.prototype.convertToGridBasis = function (position, gridPosition) {\n gridPosition.x = (position.x - this.originX) / this.cellWidth;\n gridPosition.y = (position.y - this.originY) / this.cellHeight;\n return gridPosition;\n };\n /**\n *\n * @param gridPosition the position on the grid\n * @param position the position on the scene\n * @returns the position on the scene\n */\n RasterizationGrid.prototype.convertFromGridBasis = function (gridPosition, position) {\n position.x = gridPosition.x * this.cellWidth + this.originX;\n position.y = gridPosition.y * this.cellHeight + this.originY;\n return position;\n };\n RasterizationGrid.prototype.get = function (x, y) {\n return this.cells[y][x];\n };\n RasterizationGrid.prototype.getNeighbor = function (cell, direction) {\n var delta = RasterizationGrid.neighbor8Deltas[direction];\n return this.cells[cell.y + delta.y][cell.x + delta.x];\n };\n RasterizationGrid.prototype.dimY = function () {\n return this.cells.length;\n };\n RasterizationGrid.prototype.dimX = function () {\n var firstColumn = this.cells[0];\n return firstColumn ? firstColumn.length : 0;\n };\n RasterizationGrid.prototype.obstacleDistanceMax = function () {\n var max = 0;\n for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {\n var cellRow = _a[_i];\n for (var _b = 0, cellRow_1 = cellRow; _b < cellRow_1.length; _b++) {\n var cell = cellRow_1[_b];\n if (cell.distanceToObstacle > max) {\n max = cell.distanceToObstacle;\n }\n }\n }\n return max;\n };\n RasterizationGrid.neighbor4Deltas = [\n { x: -1, y: 0 },\n { x: 0, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: -1 },\n ];\n RasterizationGrid.neighbor8Deltas = [\n { x: -1, y: 0 },\n { x: 0, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: -1 },\n { x: 1, y: 1 },\n { x: -1, y: 1 },\n { x: -1, y: -1 },\n { x: 1, y: -1 },\n ];\n return RasterizationGrid;\n}());\n\n/**\n * Builds a set of contours from the region information contained in\n * {@link RasterizationCell}. It does this by locating and \"walking\" the edges.\n *\n * This implementation is strongly inspired from CritterAI class \"ContourSetBuilder\".\n * http://www.critterai.org/projects/nmgen_study/contourgen.html\n */\nvar ContourBuilder = /** @class */ (function () {\n function ContourBuilder() {\n // These are working lists whose content changes with each iteration\n // of the up coming loop. They represent the detailed and simple\n // contour vertices.\n // Initial sizing is arbitrary.\n this.workingRawVertices = new Array(256);\n this.workingSimplifiedVertices = new Array(64);\n }\n /**\n * Generates a contour set from the provided {@link RasterizationGrid}\n *\n * The provided field is expected to contain region information.\n * Behavior is undefined if the provided field is malformed or incomplete.\n *\n * This operation overwrites the flag fields for all cells in the\n * provided field. So the flags must be saved and restored if they are\n * important.\n *\n * @param grid A fully generated field.\n * @param threshold The maximum distance (in cells) the edge of the contour\n * may deviate from the source geometry when the rastered obstacles are\n * vectorized.\n *\n * Setting it to:\n * - 1 ensure that an aliased edge won't be split to more edges.\n * - more that 1 will reduce the number of edges but the obstacles edges\n * will be followed with less accuracy.\n * - less that 1 might be more accurate but it may try to follow the\n * aliasing and be a lot less accurate.\n *\n * Values under 1 can be useful in specific cases:\n * - when edges are horizontal or vertical, there is no aliasing so value\n * near 0 can do better results.\n * - when edges are 45° multiples, aliased vertex won't be farther than\n * sqrt(2)/2 so values over 0.71 should give good results but not\n * necessarily better than 1.\n *\n * @return The contours generated from the field.\n */\n ContourBuilder.prototype.buildContours = function (grid, threshold) {\n var contours = new Array(grid.regionCount);\n contours.length = 0;\n var contoursByRegion = new Array(grid.regionCount);\n var discardedContours = 0;\n // Set the flags on all cells in non-obstacle regions to indicate which\n // edges are connected to external regions.\n //\n // Reference: Neighbor search and nomenclature.\n // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch\n //\n // If a cell has no connections to external regions or is\n // completely surrounded by other regions (a single cell island),\n // its flag will be zero.\n //\n // If a cell is connected to one or more external regions then the\n // flag will be a 4 bit value where connections are recorded as\n // follows:\n // bit1 = neighbor0\n // bit2 = neighbor1\n // bit3 = neighbor2\n // bit4 = neighbor3\n // With the meaning of the bits as follows:\n // 0 = neighbor in same region.\n // 1 = neighbor not in same region (neighbor may be the obstacle\n // region or a real region).\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n // Note: This algorithm first sets the flag bits such that\n // 1 = \"neighbor is in the same region\". At the end it inverts\n // the bits so flags are as expected.\n // Default to \"not connected to any external region\".\n cell.contourFlags = 0;\n if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // Don't care about cells in the obstacle region.\n continue;\n for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {\n var delta = RasterizationGrid.neighbor4Deltas[direction];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (cell.regionID === neighbor.regionID) {\n // Neighbor is in same region as this cell.\n // Set the bit for this neighbor to 1 (Will be inverted later).\n cell.contourFlags |= 1 << direction;\n }\n }\n // Invert the bits so a bit value of 1 indicates neighbor NOT in\n // same region.\n cell.contourFlags ^= 0xf;\n if (cell.contourFlags === 0xf) {\n // This is an island cell (All neighbors are from other regions)\n // Get rid of flags.\n cell.contourFlags = 0;\n console.warn(\"Discarded contour: Island cell. Can't form a contour. Region: \" +\n cell.regionID);\n discardedContours++;\n }\n }\n }\n // Loop through all cells looking for cells on the edge of a region.\n //\n // At this point, only cells with flags != 0 are edge cells that\n // are part of a region contour.\n //\n // The process of building a contour will clear the flags on all cells\n // that make up the contour to ensure they are only processed once.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID ||\n cell.contourFlags === 0) {\n // cell is either: Part of the obstacle region, does not\n // represent an edge cell, or was already processed during\n // an earlier iteration.\n continue;\n }\n this.workingRawVertices.length = 0;\n this.workingSimplifiedVertices.length = 0;\n // The cell is part of an unprocessed region's contour.\n // Locate a direction of the cell's edge which points toward\n // another region (there is at least one).\n var startDirection = 0;\n while ((cell.contourFlags & (1 << startDirection)) === 0) {\n startDirection++;\n }\n // We now have a cell that is part of a contour and a direction\n // that points to a different region (obstacle or real).\n // Build the contour.\n this.buildRawContours(grid, cell, startDirection, this.workingRawVertices);\n // Perform post processing on the contour in order to\n // create the final, simplified contour.\n this.generateSimplifiedContour(cell.regionID, this.workingRawVertices, this.workingSimplifiedVertices, threshold);\n // The CritterAI implementation filters polygons with less than\n // 3 vertices, but they are needed to filter vertices in the middle\n // (not on an obstacle region border).\n var contour = Array.from(this.workingSimplifiedVertices);\n contours.push(contour);\n contoursByRegion[cell.regionID] = contour;\n }\n }\n if (contours.length + discardedContours !== grid.regionCount - 1) {\n // The only valid state is one contour per region.\n //\n // The only time this should occur is if an invalid contour\n // was formed or if a region resulted in multiple\n // contours (bad region data).\n //\n // IMPORTANT: While a mismatch may not be a fatal error,\n // it should be addressed since it can result in odd,\n // hard to spot anomalies later in the pipeline.\n //\n // A known cause is if a region fully encompasses another\n // region. In such a case, two contours will be formed.\n // The normal outer contour and an inner contour.\n // The CleanNullRegionBorders algorithm protects\n // against internal encompassed obstacle regions.\n console.error(\"Contour generation failed: Detected contours does\" +\n \" not match the number of regions. Regions: \" +\n (grid.regionCount - 1) +\n \", Detected contours: \" +\n (contours.length + discardedContours) +\n \" (Actual: \" +\n contours.length +\n \", Discarded: \" +\n discardedContours +\n \")\");\n // The CritterAI implementation has more detailed logs.\n // They can be interesting for debugging.\n }\n this.filterNonObstacleVertices(contours, contoursByRegion);\n return contours;\n };\n /**\n * Search vertices that are not shared with the obstacle region and\n * remove them.\n *\n * Some contours will have no vertex left.\n *\n * @param contours\n * @param contoursByRegion Some regions may have been discarded\n * so contours index can't be used.\n */\n ContourBuilder.prototype.filterNonObstacleVertices = function (contours, contoursByRegion) {\n // This was not part of the CritterAI implementation.\n // The removed vertex is merged on the nearest of the edges other extremity\n // that is on an obstacle border.\n var commonVertexContours = new Array(5);\n var commonVertexIndexes = new Array(5);\n // Each pass only filter vertex that have an edge other extremity on an obstacle.\n // Vertex depth (in number of edges to reach an obstacle) is reduces by\n // at least one by each pass.\n var movedAnyVertex = false;\n do {\n movedAnyVertex = false;\n for (var _i = 0, contours_1 = contours; _i < contours_1.length; _i++) {\n var contour = contours_1[_i];\n for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {\n var vertex = contour[vertexIndex];\n var nextVertex = contour[(vertexIndex + 1) % contour.length];\n if (vertex.region !== RasterizationCell.OBSTACLE_REGION_ID &&\n nextVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n // This is a vertex in the middle. It must be removed.\n // Search the contours around the vertex.\n //\n // Typically a contour point to its neighbor and it form a cycle.\n //\n // \\ C /\n // \\ /\n // A | B\n // |\n //\n // C -> B -> A -> C\n //\n // There can be more than 3 contours even if it's rare.\n commonVertexContours.length = 0;\n commonVertexIndexes.length = 0;\n commonVertexContours.push(contour);\n commonVertexIndexes.push(vertexIndex);\n var errorFound = false;\n var commonVertex = vertex;\n do {\n var neighborContour = contoursByRegion[commonVertex.region];\n if (!neighborContour) {\n errorFound = true;\n if (commonVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n console.warn(\"contour already discarded: \" + commonVertex.region);\n }\n break;\n }\n var foundVertex = false;\n for (var neighborVertexIndex = 0; neighborVertexIndex < neighborContour.length; neighborVertexIndex++) {\n var neighborVertex = neighborContour[neighborVertexIndex];\n if (neighborVertex.x === commonVertex.x &&\n neighborVertex.y === commonVertex.y) {\n commonVertexContours.push(neighborContour);\n commonVertexIndexes.push(neighborVertexIndex);\n commonVertex = neighborVertex;\n foundVertex = true;\n break;\n }\n }\n if (!foundVertex) {\n errorFound = true;\n console.error(\"Can't find a common vertex with a neighbor contour. There is probably a superposition.\");\n break;\n }\n } while (commonVertex !== vertex);\n if (errorFound) {\n continue;\n }\n if (commonVertexContours.length < 3) {\n console.error(\"The vertex is shared by only \" + commonVertexContours.length + \" regions.\");\n }\n var shorterEdgeContourIndex = -1;\n var edgeLengthMin = Number.MAX_VALUE;\n for (var index = 0; index < commonVertexContours.length; index++) {\n var vertexContour = commonVertexContours[index];\n var vertexIndex_1 = commonVertexIndexes[index];\n var previousVertex = vertexContour[(vertexIndex_1 - 1 + vertexContour.length) %\n vertexContour.length];\n if (previousVertex.region === RasterizationCell.OBSTACLE_REGION_ID) {\n var deltaX = previousVertex.x - vertex.x;\n var deltaY = previousVertex.y - vertex.y;\n var lengthSq = deltaX * deltaX + deltaY * deltaY;\n if (lengthSq < edgeLengthMin) {\n edgeLengthMin = lengthSq;\n shorterEdgeContourIndex = index;\n }\n }\n }\n if (shorterEdgeContourIndex === -1) {\n // A vertex has no neighbor on an obstacle.\n // It will be solved in next iterations.\n continue;\n }\n // Merge the vertex on the other extremity of the smallest of the 3 edges.\n //\n // \\ C /\n // \\ /\n // A | B\n // |\n //\n // - the shortest edge is between A and B\n // - the Y will become a V\n // - vertices are store clockwise\n // - there can be more than one C (it's rare)\n // This is B\n var shorterEdgeContour = commonVertexContours[shorterEdgeContourIndex];\n var shorterEdgeVertexIndex = commonVertexIndexes[shorterEdgeContourIndex];\n var shorterEdgeExtremityVertex = shorterEdgeContour[(shorterEdgeVertexIndex - 1 + shorterEdgeContour.length) %\n shorterEdgeContour.length];\n // This is A\n var shorterEdgeOtherContourIndex = (shorterEdgeContourIndex + 1) % commonVertexContours.length;\n var shorterEdgeOtherContour = commonVertexContours[shorterEdgeOtherContourIndex];\n var shorterEdgeOtherVertexIndex = commonVertexIndexes[shorterEdgeOtherContourIndex];\n for (var index = 0; index < commonVertexContours.length; index++) {\n if (index === shorterEdgeContourIndex ||\n index === shorterEdgeOtherContourIndex) {\n continue;\n }\n // These are C\n var commonVertexContour = commonVertexContours[index];\n var commonVertexIndex = commonVertexIndexes[index];\n // Move the vertex to an obstacle border\n var movedVertex = commonVertexContour[commonVertexIndex];\n movedVertex.x = shorterEdgeExtremityVertex.x;\n movedVertex.y = shorterEdgeExtremityVertex.y;\n movedVertex.region = RasterizationCell.NULL_REGION_ID;\n }\n // There is no more border between A and B,\n // update the region from B to C.\n shorterEdgeOtherContour[(shorterEdgeOtherVertexIndex + 1) % shorterEdgeOtherContour.length].region =\n shorterEdgeOtherContour[shorterEdgeOtherVertexIndex].region;\n // Remove in A and B the vertex that's been move in C.\n shorterEdgeContour.splice(shorterEdgeVertexIndex, 1);\n shorterEdgeOtherContour.splice(shorterEdgeOtherVertexIndex, 1);\n movedAnyVertex = true;\n }\n }\n }\n } while (movedAnyVertex);\n // Clean the polygons from identical vertices.\n //\n // This can happen with 2 vertices regions.\n // 2 edges are superposed and there extremity is the same.\n // One is move over the other.\n // I could observe this with a region between 2 regions\n // where one of one of these 2 regions were also encompassed.\n // A bit like a rainbow, 2 big regions: the land, the sky\n // and 2 regions for the colors.\n //\n // The vertex can't be removed during the process because\n // they hold data used by other merging.\n //\n // Some contour will have no vertex left.\n // It more efficient to let the next step ignore them.\n for (var _a = 0, contours_2 = contours; _a < contours_2.length; _a++) {\n var contour = contours_2[_a];\n for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {\n var vertex = contour[vertexIndex];\n var nextVertexIndex = (vertexIndex + 1) % contour.length;\n var nextVertex = contour[nextVertexIndex];\n if (vertex.x === nextVertex.x && vertex.y === nextVertex.y) {\n contour.splice(nextVertexIndex, 1);\n vertexIndex--;\n }\n }\n }\n };\n /**\n * Walk around the edge of this cell's region gathering vertices that\n * represent the corners of each cell on the sides that are external facing.\n *\n * There will be two or three vertices for each edge cell:\n * Two for cells that don't represent a change in edge direction. Three\n * for cells that represent a change in edge direction.\n *\n * The output array will contain vertices ordered as follows:\n * (x, y, z, regionID) where regionID is the region (obstacle or real) that\n * this vertex is considered to be connected to.\n *\n * WARNING: Only run this operation on cells that are already known\n * to be on a region edge. The direction must also be pointing to a\n * valid edge. Otherwise behavior will be undefined.\n *\n * @param grid the grid of cells\n * @param startCell A cell that is known to be on the edge of a region\n * (part of a region contour).\n * @param startDirection The direction of the edge of the cell that is\n * known to point\n * across the region edge.\n * @param outContourVertices The list of vertices that represent the edge\n * of the region.\n */\n ContourBuilder.prototype.buildRawContours = function (grid, startCell, startDirection, outContourVertices) {\n // Flaw in Algorithm:\n //\n // This method of contour generation can result in an inappropriate\n // impassable seam between two adjacent regions in the following case:\n //\n // 1. One region connects to another region on two sides in an\n // uninterrupted manner (visualize one region wrapping in an L\n // shape around the corner of another).\n // 2. At the corner shared by the two regions, a change in height\n // occurs.\n //\n // In this case, the two regions should share a corner vertex\n // (an obtuse corner vertex for one region and an acute corner\n // vertex for the other region).\n //\n // In reality, though this algorithm will select the same (x, z)\n // coordinates for each region's corner vertex, the vertex heights\n // may differ, eventually resulting in an impassable seam.\n // It is a bit hard to describe the stepping portion of this algorithm.\n // One way to visualize it is to think of a robot sitting on the\n // floor facing a known wall. It then does the following to skirt\n // the wall:\n // 1. If there is a wall in front of it, turn clockwise in 90 degrees\n // increments until it finds the wall is gone.\n // 2. Move forward one step.\n // 3. Turn counter-clockwise by 90 degrees.\n // 4. Repeat from step 1 until it finds itself at its original\n // location facing its original direction.\n //\n // See also: http://www.critterai.org/projects/nmgen_study/contourgen.html#robotwalk\n var cell = startCell;\n var direction = startDirection;\n var loopCount = 0;\n do {\n // Note: The design of this loop is such that the cell variable\n // will always reference an edge cell from the same region as\n // the start cell.\n if ((cell.contourFlags & (1 << direction)) !== 0) {\n // The current direction is pointing toward an edge.\n // Get this edge's vertex.\n var delta = ContourBuilder.leftVertexOfFacingCellBorderDeltas[direction];\n var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);\n outContourVertices.push({\n x: cell.x + delta.x,\n y: cell.y + delta.y,\n region: neighbor.regionID,\n });\n // Remove the flag for this edge. We never need to consider\n // it again since we have a vertex for this edge.\n cell.contourFlags &= ~(1 << direction);\n // Rotate in clockwise direction.\n direction = (direction + 1) & 0x3;\n }\n else {\n // The current direction does not point to an edge. So it\n // must point to a neighbor cell in the same region as the\n // current cell. Move to the neighbor and swing the search\n // direction back one increment (counterclockwise).\n // By moving the direction back one increment we guarantee we\n // don't miss any edges.\n var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);\n cell = neighbor;\n direction = (direction + 3) & 0x3; // Rotate counterclockwise.\n }\n // The loop limit is arbitrary. It exists only to guarantee that\n // bad input data doesn't result in an infinite loop.\n // The only down side of this loop limit is that it limits the\n // number of detectable edge vertices (the longer the region edge\n // and the higher the number of \"turns\" in a region's edge, the less\n // edge vertices can be detected for that region).\n } while (!(cell === startCell && direction === startDirection) &&\n ++loopCount < 65535);\n return outContourVertices;\n };\n /**\n * Takes a group of vertices that represent a region contour and changes\n * it in the following manner:\n * - For any edges that connect to non-obstacle regions, remove all\n * vertices except the start and end vertices for that edge (this\n * smooths the edges between non-obstacle regions into a straight line).\n * - Runs an algorithm's against the contour to follow the edge more closely.\n *\n * @param regionID The region the contour was derived from.\n * @param sourceVertices The source vertices that represent the complex\n * contour.\n * @param outVertices The simplified contour vertices.\n * @param threshold The maximum distance the edge of the contour may deviate\n * from the source geometry.\n */\n ContourBuilder.prototype.generateSimplifiedContour = function (regionID, sourceVertices, outVertices, threshold) {\n var noConnections = true;\n for (var _i = 0, sourceVertices_1 = sourceVertices; _i < sourceVertices_1.length; _i++) {\n var sourceVertex = sourceVertices_1[_i];\n if (sourceVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n noConnections = false;\n break;\n }\n }\n // Seed the simplified contour with the mandatory edges\n // (At least one edge).\n if (noConnections) {\n // This contour represents an island region surrounded only by the\n // obstacle region. Seed the simplified contour with the source's\n // lower left (ll) and upper right (ur) vertices.\n var lowerLeftX = sourceVertices[0].x;\n var lowerLeftY = sourceVertices[0].y;\n var lowerLeftIndex = 0;\n var upperRightX = sourceVertices[0].x;\n var upperRightY = sourceVertices[0].y;\n var upperRightIndex = 0;\n for (var index = 0; index < sourceVertices.length; index++) {\n var sourceVertex = sourceVertices[index];\n var x = sourceVertex.x;\n var y = sourceVertex.y;\n if (x < lowerLeftX || (x === lowerLeftX && y < lowerLeftY)) {\n lowerLeftX = x;\n lowerLeftY = y;\n lowerLeftIndex = index;\n }\n if (x >= upperRightX || (x === upperRightX && y > upperRightY)) {\n upperRightX = x;\n upperRightY = y;\n upperRightIndex = index;\n }\n }\n // The region attribute is used to store an index locally in this function.\n // TODO Maybe there is a way to do this cleanly and keep no memory footprint.\n // Seed the simplified contour with this edge.\n outVertices.push({\n x: lowerLeftX,\n y: lowerLeftY,\n region: lowerLeftIndex,\n });\n outVertices.push({\n x: upperRightX,\n y: upperRightY,\n region: upperRightIndex,\n });\n }\n else {\n // The contour shares edges with other non-obstacle regions.\n // Seed the simplified contour with a new vertex for every\n // location where the region connection changes. These are\n // vertices that are important because they represent portals\n // to other regions.\n for (var index = 0; index < sourceVertices.length; index++) {\n var sourceVert = sourceVertices[index];\n if (sourceVert.region !==\n sourceVertices[(index + 1) % sourceVertices.length].region) {\n // The current vertex has a different region than the\n // next vertex. So there is a change in vertex region.\n outVertices.push({\n x: sourceVert.x,\n y: sourceVert.y,\n region: index,\n });\n }\n }\n }\n this.matchObstacleRegionEdges(sourceVertices, outVertices, threshold);\n if (outVertices.length < 2) {\n // It will be ignored by the triangulation.\n // It should be rare enough not to handle it now.\n console.warn(\"A region is encompassed in another region. It will be ignored.\");\n }\n // There can be polygons with only 2 vertices when a region is between\n // 2 non-obstacles regions. It's still a useful information to filter\n // vertices in the middle (not on an obstacle region border).\n // In this case, the CritterAI implementation adds a 3rd point to avoid\n // invisible polygons, but it makes it difficult to filter it later.\n // Replace the index pointers in the output list with region IDs.\n for (var _a = 0, outVertices_1 = outVertices; _a < outVertices_1.length; _a++) {\n var outVertex = outVertices_1[_a];\n outVertex.region = sourceVertices[outVertex.region].region;\n }\n };\n /**\n * Applies an algorithm to contours which results in obstacle-region edges\n * following the original detail source geometry edge more closely.\n * http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple\n *\n * Adds vertices from the source list to the result list such that\n * if any obstacle region vertices are compared against the result list,\n * none of the vertices will be further from the obstacle region edges than\n * the allowed threshold.\n *\n * Only obstacle-region edges are operated on. All other edges are\n * ignored.\n *\n * The result vertices is expected to be seeded with at least two\n * source vertices.\n *\n * @param sourceVertices\n * @param inoutResultVertices\n * @param threshold The maximum distance the edge of the contour may deviate\n * from the source geometry.\n */\n ContourBuilder.prototype.matchObstacleRegionEdges = function (sourceVertices, inoutResultVertices, threshold) {\n // This implementation is strongly inspired from CritterAI class \"MatchNullRegionEdges\".\n // Loop through all edges in this contour.\n //\n // NOTE: The simplifiedVertCount in the loop condition\n // increases over iterations. That is what keeps the loop going beyond\n // the initial vertex count.\n var resultIndexA = 0;\n while (resultIndexA < inoutResultVertices.length) {\n var resultIndexB = (resultIndexA + 1) % inoutResultVertices.length;\n // The line segment's beginning vertex.\n var ax = inoutResultVertices[resultIndexA].x;\n var az = inoutResultVertices[resultIndexA].y;\n var sourceIndexA = inoutResultVertices[resultIndexA].region;\n // The line segment's ending vertex.\n var bx = inoutResultVertices[resultIndexB].x;\n var bz = inoutResultVertices[resultIndexB].y;\n var sourceIndexB = inoutResultVertices[resultIndexB].region;\n // The source index of the next vertex to test (the vertex just\n // after the current vertex in the source vertex list).\n var testedSourceIndex = (sourceIndexA + 1) % sourceVertices.length;\n var maxDeviation = 0;\n // Default to no index. No new vert to add.\n var toInsertSourceIndex = -1;\n if (sourceVertices[testedSourceIndex].region ===\n RasterizationCell.OBSTACLE_REGION_ID) {\n // This test vertex is part of a obstacle region edge.\n // Loop through the source vertices until the end vertex\n // is found, searching for the vertex that is farthest from\n // the line segment formed by the begin/end vertices.\n //\n // Visualizations:\n // http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple\n while (testedSourceIndex !== sourceIndexB) {\n var deviation = Geometry.getPointSegmentDistanceSq(sourceVertices[testedSourceIndex].x, sourceVertices[testedSourceIndex].y, ax, az, bx, bz);\n if (deviation > maxDeviation) {\n // A new maximum deviation was detected.\n maxDeviation = deviation;\n toInsertSourceIndex = testedSourceIndex;\n }\n // Move to the next vertex.\n testedSourceIndex = (testedSourceIndex + 1) % sourceVertices.length;\n }\n }\n if (toInsertSourceIndex !== -1 && maxDeviation > threshold * threshold) {\n // A vertex was found that is further than allowed from the\n // current edge. Add this vertex to the contour.\n inoutResultVertices.splice(resultIndexA + 1, 0, {\n x: sourceVertices[toInsertSourceIndex].x,\n y: sourceVertices[toInsertSourceIndex].y,\n region: toInsertSourceIndex,\n });\n // Not incrementing the vertex since we need to test the edge\n // formed by vertA and this this new vertex on the next\n // iteration of the loop.\n }\n // This edge segment does not need to be altered. Move to\n // the next vertex.\n else\n resultIndexA++;\n }\n };\n ContourBuilder.leftVertexOfFacingCellBorderDeltas = [\n { x: 0, y: 1 },\n { x: 1, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: 0 },\n ];\n return ContourBuilder;\n}());\n\n/**\n * Builds convex polygons from the provided polygons.\n *\n * This implementation is strongly inspired from CritterAI class \"PolyMeshFieldBuilder\".\n * http://www.critterai.org/projects/nmgen_study/polygen.html\n */\nvar ConvexPolygonGenerator = /** @class */ (function () {\n function ConvexPolygonGenerator() {\n }\n /**\n * Builds convex polygons from the provided polygons.\n * @param concavePolygons The content is manipulated during the operation\n * and it will be left in an undefined state at the end of\n * the operation.\n * @param maxVerticesPerPolygon cap the vertex number in return polygons.\n * @return convex polygons.\n */\n ConvexPolygonGenerator.prototype.splitToConvexPolygons = function (concavePolygons, maxVerticesPerPolygon) {\n // The maximum possible number of polygons assuming that all will\n // be triangles.\n var maxPossiblePolygons = 0;\n // The maximum vertices found in a single contour.\n var maxVerticesPerContour = 0;\n for (var _i = 0, concavePolygons_1 = concavePolygons; _i < concavePolygons_1.length; _i++) {\n var contour = concavePolygons_1[_i];\n var count = contour.length;\n maxPossiblePolygons += count - 2;\n maxVerticesPerContour = Math.max(maxVerticesPerContour, count);\n }\n // Each list is initialized to a size that will minimize resizing.\n var convexPolygons = new Array(maxPossiblePolygons);\n convexPolygons.length = 0;\n // Various working variables.\n // (Values are meaningless outside of the iteration)\n var workingContourFlags = new Array(maxVerticesPerContour);\n workingContourFlags.length = 0;\n var workingPolygons = new Array(maxVerticesPerContour + 1);\n workingPolygons.length = 0;\n var workingMergeInfo = {\n lengthSq: -1,\n polygonAVertexIndex: -1,\n polygonBVertexIndex: -1,\n };\n var workingMergedPolygon = new Array(maxVerticesPerPolygon);\n workingMergedPolygon.length = 0;\n var _loop_1 = function (contour) {\n if (contour.length < 3) {\n return \"continue\";\n }\n // Initialize the working polygon array.\n workingPolygons.length = 0;\n // Triangulate the contour.\n var foundAnyTriangle = false;\n this_1.triangulate(contour, workingContourFlags, function (p1, p2, p3) {\n var workingPolygon = new Array(maxVerticesPerPolygon);\n workingPolygon.length = 0;\n workingPolygon.push(p1);\n workingPolygon.push(p2);\n workingPolygon.push(p3);\n workingPolygons.push(workingPolygon);\n foundAnyTriangle = true;\n });\n if (!foundAnyTriangle) {\n /*\n * Failure of the triangulation.\n * This is known to occur if the source polygon is\n * self-intersecting or the source region contains internal\n * holes. In both cases, the problem is likely due to bad\n * region formation.\n */\n console.error(\"Polygon generation failure: Could not triangulate contour.\");\n console.error(\"contour:\" +\n contour.map(function (point) { return point.x + \" \" + point.y; }).join(\" ; \"));\n return \"continue\";\n }\n if (maxVerticesPerPolygon > 3) {\n // Merging of triangles into larger polygons is permitted.\n // Continue until no polygons can be found to merge.\n // http://www.critterai.org/nmgen_polygen#mergepolys\n while (true) {\n var longestMergeEdge = -1;\n var bestPolygonA = [];\n var polygonAVertexIndex = -1; // Start of the shared edge.\n var bestPolygonB = [];\n var polygonBVertexIndex = -1; // Start of the shared edge.\n var bestPolygonBIndex = -1;\n // Loop through all but the last polygon looking for the\n // best polygons to merge in this iteration.\n for (var indexA = 0; indexA < workingPolygons.length - 1; indexA++) {\n var polygonA = workingPolygons[indexA];\n for (var indexB = indexA + 1; indexB < workingPolygons.length; indexB++) {\n var polygonB = workingPolygons[indexB];\n // Can polyB merge with polyA?\n this_1.getPolyMergeInfo(polygonA, polygonB, maxVerticesPerPolygon, workingMergeInfo);\n if (workingMergeInfo.lengthSq > longestMergeEdge) {\n // polyB has the longest shared edge with\n // polyA found so far. Save the merge\n // information.\n longestMergeEdge = workingMergeInfo.lengthSq;\n bestPolygonA = polygonA;\n polygonAVertexIndex = workingMergeInfo.polygonAVertexIndex;\n bestPolygonB = polygonB;\n polygonBVertexIndex = workingMergeInfo.polygonBVertexIndex;\n bestPolygonBIndex = indexB;\n }\n }\n }\n if (longestMergeEdge <= 0)\n // No valid merges found during this iteration.\n break;\n // Found polygons to merge. Perform the merge.\n /*\n * Fill the mergedPoly array.\n * Start the vertex at the end of polygon A's shared edge.\n * Add all vertices until looping back to the vertex just\n * before the start of the shared edge. Repeat for\n * polygon B.\n *\n * Duplicate vertices are avoided, while ensuring we get\n * all vertices, since each loop drops the vertex that\n * starts its polygon's shared edge and:\n *\n * PolyAStartVert == PolyBEndVert and\n * PolyAEndVert == PolyBStartVert.\n */\n var vertCountA = bestPolygonA.length;\n var vertCountB = bestPolygonB.length;\n workingMergedPolygon.length = 0;\n for (var i = 0; i < vertCountA - 1; i++)\n workingMergedPolygon.push(bestPolygonA[(polygonAVertexIndex + 1 + i) % vertCountA]);\n for (var i = 0; i < vertCountB - 1; i++)\n workingMergedPolygon.push(bestPolygonB[(polygonBVertexIndex + 1 + i) % vertCountB]);\n // Copy the merged polygon over the top of polygon A.\n bestPolygonA.length = 0;\n Array.prototype.push.apply(bestPolygonA, workingMergedPolygon);\n // Remove polygon B\n workingPolygons.splice(bestPolygonBIndex, 1);\n }\n }\n // Polygon creation for this contour is complete.\n // Add polygons to the global polygon array\n Array.prototype.push.apply(convexPolygons, workingPolygons);\n };\n var this_1 = this;\n // Split every concave polygon into convex polygons.\n for (var _a = 0, concavePolygons_2 = concavePolygons; _a < concavePolygons_2.length; _a++) {\n var contour = concavePolygons_2[_a];\n _loop_1(contour);\n }\n // The original implementation builds polygon adjacency information.\n // but the library for the pathfinding already does it.\n return convexPolygons;\n };\n /**\n * Checks two polygons to see if they can be merged. If a merge is\n * allowed, provides data via the outResult argument (see {@link PolyMergeResult}).\n *\n * @param polygonA The polygon A\n * @param polygonB The polygon B\n * @param maxVerticesPerPolygon cap the vertex number in return polygons.\n * @param outResult contains merge information.\n */\n ConvexPolygonGenerator.prototype.getPolyMergeInfo = function (polygonA, polygonB, maxVerticesPerPolygon, outResult) {\n outResult.lengthSq = -1; // Default to invalid merge\n outResult.polygonAVertexIndex = -1;\n outResult.polygonBVertexIndex = -1;\n var vertexCountA = polygonA.length;\n var vertexCountB = polygonB.length;\n // If the merged polygon would would have to many vertices, do not\n // merge. Subtracting two since to take into account the effect of\n // a merge.\n if (vertexCountA + vertexCountB - 2 > maxVerticesPerPolygon)\n return;\n // Check if the polygons share an edge.\n for (var indexA = 0; indexA < vertexCountA; indexA++) {\n // Get the vertex indices for the polygonA edge\n var vertexA = polygonA[indexA];\n var nextVertexA = polygonA[(indexA + 1) % vertexCountA];\n // Search polygonB for matches.\n for (var indexB = 0; indexB < vertexCountB; indexB++) {\n // Get the vertex indices for the polygonB edge.\n var vertexB = polygonB[indexB];\n var nextVertexB = polygonB[(indexB + 1) % vertexCountB];\n // === can be used because vertices comme from the same concave polygon.\n if (vertexA === nextVertexB && nextVertexA === vertexB) {\n // The vertex indices for this edge are the same and\n // sequenced in opposite order. So the edge is shared.\n outResult.polygonAVertexIndex = indexA;\n outResult.polygonBVertexIndex = indexB;\n }\n }\n }\n if (outResult.polygonAVertexIndex === -1)\n // No common edge, cannot merge.\n return;\n // Check to see if the merged polygon would be convex.\n //\n // Gets the vertices near the section where the merge would occur.\n // Do they form a concave section? If so, the merge is invalid.\n //\n // Note that the following algorithm is only valid for clockwise\n // wrapped convex polygons.\n var sharedVertMinus = polygonA[(outResult.polygonAVertexIndex - 1 + vertexCountA) % vertexCountA];\n var sharedVert = polygonA[outResult.polygonAVertexIndex];\n var sharedVertPlus = polygonB[(outResult.polygonBVertexIndex + 2) % vertexCountB];\n if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {\n // The shared vertex (center) is not to the left of segment\n // vertMinus->vertPlus. For a clockwise wrapped polygon, this\n // indicates a concave section. Merged polygon would be concave.\n // Invalid merge.\n return;\n }\n sharedVertMinus =\n polygonB[(outResult.polygonBVertexIndex - 1 + vertexCountB) % vertexCountB];\n sharedVert = polygonB[outResult.polygonBVertexIndex];\n sharedVertPlus =\n polygonA[(outResult.polygonAVertexIndex + 2) % vertexCountA];\n if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {\n // The shared vertex (center) is not to the left of segment\n // vertMinus->vertPlus. For a clockwise wrapped polygon, this\n // indicates a concave section. Merged polygon would be concave.\n // Invalid merge.\n return;\n }\n // Get the vertex indices that form the shared edge.\n sharedVertMinus = polygonA[outResult.polygonAVertexIndex];\n sharedVert = polygonA[(outResult.polygonAVertexIndex + 1) % vertexCountA];\n // Store the lengthSq of the shared edge.\n var deltaX = sharedVertMinus.x - sharedVert.x;\n var deltaZ = sharedVertMinus.y - sharedVert.y;\n outResult.lengthSq = deltaX * deltaX + deltaZ * deltaZ;\n };\n /**\n * Attempts to triangulate a polygon.\n *\n * @param vertices the polygon to be triangulate.\n * The content is manipulated during the operation\n * and it will be left in an undefined state at the end of\n * the operation.\n * @param vertexFlags only used internally\n * @param outTriangles is called for each triangle derived\n * from the original polygon.\n * @return The number of triangles generated. Or, if triangulation\n * failed, a negative number.\n */\n ConvexPolygonGenerator.prototype.triangulate = function (vertices, vertexFlags, outTriangles) {\n // Terminology, concepts and such:\n //\n // This algorithm loops around the edges of a polygon looking for\n // new internal edges to add that will partition the polygon into a\n // new valid triangle internal to the starting polygon. During each\n // iteration the shortest potential new edge is selected to form that\n // iteration's new triangle.\n //\n // Triangles will only be formed if a single new edge will create\n // a triangle. Two new edges will never be added during a single\n // iteration. This means that the triangulated portions of the\n // original polygon will only contain triangles and the only\n // non-triangle polygon will exist in the untriangulated portion\n // of the original polygon.\n //\n // \"Partition edge\" refers to a potential new edge that will form a\n // new valid triangle.\n //\n // \"Center\" vertex refers to the vertex in a potential new triangle\n // which, if the triangle is formed, will be external to the\n // remaining untriangulated portion of the polygon. Since it\n // is now external to the polygon, it can't be used to form any\n // new triangles.\n //\n // Some documentation refers to \"iPlus2\" even though the variable is\n // not in scope or does not exist for that section of code. For\n // documentation purposes, iPlus2 refers to the 2nd vertex after the\n // primary vertex.\n // E.g.: i, iPlus1, and iPlus2.\n //\n // Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#triangulation\n // Loop through all vertices, flagging all indices that represent\n // a center vertex of a valid new triangle.\n vertexFlags.length = vertices.length;\n for (var i = 0; i < vertices.length; i++) {\n var iPlus1 = (i + 1) % vertices.length;\n var iPlus2 = (i + 2) % vertices.length;\n // A triangle formed by i, iPlus1, and iPlus2 will result\n // in a valid internal triangle.\n // Flag the center vertex (iPlus1) to indicate a valid triangle\n // location.\n vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, iPlus2, vertices);\n }\n // Loop through the vertices creating triangles. When there is only a\n // single triangle left, the operation is complete.\n //\n // When a valid triangle is formed, remove its center vertex. So for\n // each loop, a single vertex will be removed.\n //\n // At the start of each iteration the indices list is in the following\n // state:\n // - Represents a simple polygon representing the un-triangulated\n // portion of the original polygon.\n // - All valid center vertices are flagged.\n while (vertices.length > 3) {\n // Find the shortest new valid edge.\n // NOTE: i and iPlus1 are defined in two different scopes in\n // this section. So be careful.\n // Loop through all indices in the remaining polygon.\n var minLengthSq = Number.MAX_VALUE;\n var minLengthSqVertexIndex = -1;\n for (var i_1 = 0; i_1 < vertices.length; i_1++) {\n if (vertexFlags[(i_1 + 1) % vertices.length]) {\n // Indices i, iPlus1, and iPlus2 are known to form a\n // valid triangle.\n var vert = vertices[i_1];\n var vertPlus2 = vertices[(i_1 + 2) % vertices.length];\n // Determine the length of the partition edge.\n // (i -> iPlus2)\n var deltaX = vertPlus2.x - vert.x;\n var deltaY = vertPlus2.y - vert.y;\n var lengthSq = deltaX * deltaX + deltaY * deltaY;\n if (lengthSq < minLengthSq) {\n minLengthSq = lengthSq;\n minLengthSqVertexIndex = i_1;\n }\n }\n }\n if (minLengthSqVertexIndex === -1)\n // Could not find a new triangle. Triangulation failed.\n // This happens if there are three or more vertices\n // left, but none of them are flagged as being a\n // potential center vertex.\n return;\n var i = minLengthSqVertexIndex;\n var iPlus1 = (i + 1) % vertices.length;\n // Add the new triangle to the output.\n outTriangles(vertices[i], vertices[iPlus1], vertices[(i + 2) % vertices.length]);\n // iPlus1, the \"center\" vert in the new triangle, is now external\n // to the untriangulated portion of the polygon. Remove it from\n // the vertices list since it cannot be a member of any new\n // triangles.\n vertices.splice(iPlus1, 1);\n vertexFlags.splice(iPlus1, 1);\n if (iPlus1 === 0 || iPlus1 >= vertices.length) {\n // The vertex removal has invalidated iPlus1 and/or i. So\n // force a wrap, fixing the indices so they reference the\n // correct indices again. This only occurs when the new\n // triangle is formed across the wrap location of the polygon.\n // Case 1: i = 14, iPlus1 = 15, iPlus2 = 0\n // Case 2: i = 15, iPlus1 = 0, iPlus2 = 1;\n i = vertices.length - 1;\n iPlus1 = 0;\n }\n // At this point i and iPlus1 refer to the two indices from a\n // successful triangulation that will be part of another new\n // triangle. We now need to re-check these indices to see if they\n // can now be the center index in a potential new partition.\n vertexFlags[i] = ConvexPolygonGenerator.isValidPartition((i - 1 + vertices.length) % vertices.length, iPlus1, vertices);\n vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, (i + 2) % vertices.length, vertices);\n }\n // Only 3 vertices remain.\n // Add their triangle to the output list.\n outTriangles(vertices[0], vertices[1], vertices[2]);\n };\n /**\n * Check if the line segment formed by vertex A and vertex B will\n * form a valid partition of the polygon.\n *\n * I.e. the line segment AB is internal to the polygon and will not\n * cross existing line segments.\n *\n * Assumptions:\n * - The vertices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * - indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if the line segment formed by vertex A and vertex B will\n * form a valid partition of the polygon.\n */\n ConvexPolygonGenerator.isValidPartition = function (indexA, indexB, vertices) {\n // First check whether the segment AB lies within the internal\n // angle formed at A (this is the faster check).\n // If it does, then perform the more costly check.\n return (ConvexPolygonGenerator.liesWithinInternalAngle(indexA, indexB, vertices) &&\n !ConvexPolygonGenerator.hasIllegalEdgeIntersection(indexA, indexB, vertices));\n };\n /**\n * Check if vertex B lies within the internal angle of the polygon\n * at vertex A.\n *\n * Vertex B does not have to be within the polygon border. It just has\n * be be within the area encompassed by the internal angle formed at\n * vertex A.\n *\n * This operation is a fast way of determining whether a line segment\n * can possibly form a valid polygon partition. If this test returns\n * FALSE, then more expensive checks can be skipped.\n *\n * Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#anglecheck\n *\n * Special case:\n * FALSE is returned if vertex B lies directly on either of the rays\n * cast from vertex A along its associated polygon edges. So the test\n * on vertex B is exclusive of the polygon edges.\n *\n * Assumptions:\n * - The vertices and indices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * -indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if vertex B lies within the internal angle of\n * the polygon at vertex A.\n */\n ConvexPolygonGenerator.liesWithinInternalAngle = function (indexA, indexB, vertices) {\n // Get pointers to the main vertices being tested.\n var vertexA = vertices[indexA];\n var vertexB = vertices[indexB];\n // Get pointers to the vertices just before and just after vertA.\n var vertexAMinus = vertices[(indexA - 1 + vertices.length) % vertices.length];\n var vertexAPlus = vertices[(indexA + 1) % vertices.length];\n // First, find which of the two angles formed by the line segments\n // AMinus->A->APlus is internal to (pointing towards) the polygon.\n // Then test to see if B lies within the area formed by that angle.\n // TRUE if A is left of or on line AMinus->APlus\n if (ConvexPolygonGenerator.isLeftOrCollinear(vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y, vertexAPlus.x, vertexAPlus.y))\n // The angle internal to the polygon is <= 180 degrees\n // (non-reflex angle).\n // Test to see if B lies within this angle.\n return (ConvexPolygonGenerator.isLeft(\n // TRUE if B is left of line A->AMinus\n vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y) &&\n // TRUE if B is right of line A->APlus\n ConvexPolygonGenerator.isRight(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y));\n // The angle internal to the polygon is > 180 degrees (reflex angle).\n // Test to see if B lies within the external (<= 180 degree) angle and\n // flip the result. (If B lies within the external angle, it can't\n // lie within the internal angle)\n return !(\n // TRUE if B is left of or on line A->APlus\n (ConvexPolygonGenerator.isLeftOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y) &&\n // TRUE if B is right of or on line A->AMinus\n ConvexPolygonGenerator.isRightOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y)));\n };\n /**\n * Check if the line segment AB intersects any edges not already\n * connected to one of the two vertices.\n *\n * Assumptions:\n * - The vertices and indices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * - indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if the line segment AB intersects any edges not already\n * connected to one of the two vertices.\n */\n ConvexPolygonGenerator.hasIllegalEdgeIntersection = function (indexA, indexB, vertices) {\n // Get pointers to the primary vertices being tested.\n var vertexA = vertices[indexA];\n var vertexB = vertices[indexB];\n // Loop through the polygon edges.\n for (var edgeBeginIndex = 0; edgeBeginIndex < vertices.length; edgeBeginIndex++) {\n var edgeEndIndex = (edgeBeginIndex + 1) % vertices.length;\n if (edgeBeginIndex === indexA ||\n edgeBeginIndex === indexB ||\n edgeEndIndex === indexA ||\n edgeEndIndex === indexB) {\n continue;\n }\n // Neither of the test indices are endpoints of this edge.\n // Get this edge's vertices.\n var edgeBegin = vertices[edgeBeginIndex];\n var edgeEnd = vertices[edgeEndIndex];\n if ((edgeBegin.x === vertexA.x && edgeBegin.y === vertexA.y) ||\n (edgeBegin.x === vertexB.x && edgeBegin.y === vertexB.y) ||\n (edgeEnd.x === vertexA.x && edgeEnd.y === vertexA.y) ||\n (edgeEnd.x === vertexB.x && edgeEnd.y === vertexB.y)) {\n // One of the test vertices is co-located\n // with one of the endpoints of this edge (this is a\n // test of the actual position of the vertices rather than\n // simply the index check performed earlier).\n // Skip this edge.\n continue;\n }\n // This edge is not connected to either of the test vertices.\n // If line segment AB intersects with this edge, then the\n // intersection is illegal.\n // I.e. New edges cannot cross existing edges.\n if (Geometry.segmentsIntersect(vertexA.x, vertexA.y, vertexB.x, vertexB.y, edgeBegin.x, edgeBegin.y, edgeEnd.x, edgeEnd.y)) {\n return true;\n }\n }\n return false;\n };\n /**\n * Check if point P is to the left of line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the left of line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isLeft = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) < 0;\n };\n /**\n * Check if point P is to the left of line AB when looking\n * from A to B or is collinear with line AB.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the left of line AB when looking\n * from A to B, or is collinear with line AB.\n */\n ConvexPolygonGenerator.isLeftOrCollinear = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) <= 0;\n };\n /**\n * Check if point P is to the right of line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the right of line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isRight = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) > 0;\n };\n /**\n * Check if point P is to the right of or on line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the right of or on line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isRightOrCollinear = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) >= 0;\n };\n /**\n * The absolute value of the returned value is two times the area of the\n * triangle defined by points (A, B, C).\n *\n * A positive value indicates:\n * - Counterclockwise wrapping of the points.\n * - Point B lies to the right of line AC, looking from A to C.\n *\n * A negative value indicates:\n * - Clockwise wrapping of the points.<\n * - Point B lies to the left of line AC, looking from A to C.\n *\n * A value of zero indicates that all points are collinear or\n * represent the same point.\n *\n * This is a fast operation.\n *\n * @param ax The x-value for point (ax, ay) for vertex A of the triangle.\n * @param ay The y-value for point (ax, ay) for vertex A of the triangle.\n * @param bx The x-value for point (bx, by) for vertex B of the triangle.\n * @param by The y-value for point (bx, by) for vertex B of the triangle.\n * @param cx The x-value for point (cx, cy) for vertex C of the triangle.\n * @param cy The y-value for point (cx, cy) for vertex C of the triangle.\n * @return The signed value of two times the area of the triangle defined\n * by the points (A, B, C).\n */\n ConvexPolygonGenerator.getSignedAreaX2 = function (ax, ay, bx, by, cx, cy) {\n // References:\n // http://softsurfer.com/Archive/algorithm_0101/algorithm_0101.htm#Modern%20Triangles\n // http://mathworld.wolfram.com/TriangleArea.html (Search for \"signed\")\n return (bx - ax) * (cy - ay) - (cx - ax) * (by - ay);\n };\n return ConvexPolygonGenerator;\n}());\n\nvar GridCoordinateConverter = /** @class */ (function () {\n function GridCoordinateConverter() {\n }\n /**\n *\n * @param gridPosition the position on the grid\n * @param position the position on the scene\n * @param scaleY for isometry\n * @returns the position on the scene\n */\n GridCoordinateConverter.prototype.convertFromGridBasis = function (grid, polygons) {\n // point can be shared so them must be copied to be scaled.\n return polygons.map(function (polygon) {\n return polygon.map(function (point) { return grid.convertFromGridBasis(point, { x: 0, y: 0 }); });\n });\n };\n return GridCoordinateConverter;\n}());\n\n/**\n * It rasterizes obstacles on a grid.\n *\n * It flags cells as obstacle to be used by {@link RegionGenerator}.\n */\nvar ObstacleRasterizer = /** @class */ (function () {\n function ObstacleRasterizer() {\n this.workingNodes = new Array(8);\n this.gridBasisIterable = new GridBasisIterable();\n }\n /**\n * Rasterize obstacles on a grid.\n * @param grid\n * @param obstacles\n */\n ObstacleRasterizer.prototype.rasterizeObstacles = function (grid, obstacles) {\n var obstaclesItr = obstacles[Symbol.iterator]();\n for (var next = obstaclesItr.next(); !next.done; next = obstaclesItr.next()) {\n var obstacle = next.value;\n this.gridBasisIterable.set(grid, obstacle);\n var vertices = this.gridBasisIterable;\n var minX = Number.MAX_VALUE;\n var maxX = -Number.MAX_VALUE;\n var minY = Number.MAX_VALUE;\n var maxY = -Number.MAX_VALUE;\n var verticesItr = vertices[Symbol.iterator]();\n for (var next_1 = verticesItr.next(); !next_1.done; next_1 = verticesItr.next()) {\n var vertex = next_1.value;\n minX = Math.min(minX, vertex.x);\n maxX = Math.max(maxX, vertex.x);\n minY = Math.min(minY, vertex.y);\n maxY = Math.max(maxY, vertex.y);\n }\n minX = Math.max(Math.floor(minX), 0);\n maxX = Math.min(Math.ceil(maxX), grid.dimX());\n minY = Math.max(Math.floor(minY), 0);\n maxY = Math.min(Math.ceil(maxY), grid.dimY());\n this.fillPolygon(vertices, minX, maxX, minY, maxY, function (x, y) { return (grid.get(x, y).distanceToObstacle = 0); });\n }\n };\n ObstacleRasterizer.prototype.fillPolygon = function (vertices, minX, maxX, minY, maxY, fill) {\n // The following implementation of the scan-line polygon fill algorithm\n // is strongly inspired from:\n // https://alienryderflex.com/polygon_fill/\n // The original implementation was under this license:\n // public-domain code by Darel Rex Finley, 2007\n // This implementation differ with the following:\n // - it handles float vertices\n // so it focus on pixels center\n // - it is conservative to thin vertical or horizontal polygons\n var fillAnyPixels = false;\n this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {\n for (var pixelX = minX; pixelX < maxX; pixelX++) {\n fillAnyPixels = true;\n fill(pixelX, pixelY);\n }\n });\n if (fillAnyPixels) {\n return;\n }\n this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {\n // conserve thin (less than one cell large) horizontal polygons\n if (minX === maxX) {\n fill(minX, pixelY);\n }\n });\n this.scanX(vertices, minX, maxX, minY, maxY, function (pixelX, minY, maxY) {\n for (var pixelY = minY; pixelY < maxY; pixelY++) {\n fill(pixelX, pixelY);\n }\n // conserve thin (less than one cell large) vertical polygons\n if (minY === maxY) {\n fill(pixelX, minY);\n }\n });\n };\n ObstacleRasterizer.prototype.scanY = function (vertices, minX, maxX, minY, maxY, checkAndFillY) {\n var workingNodes = this.workingNodes;\n // Loop through the rows of the image.\n for (var pixelY = minY; pixelY < maxY; pixelY++) {\n var pixelCenterY = pixelY + 0.5;\n // Build a list of nodes.\n workingNodes.length = 0;\n //let j = vertices.length - 1;\n var verticesItr = vertices[Symbol.iterator]();\n var next = verticesItr.next();\n var vertex = next.value;\n // The iterator always return the same instance.\n // It must be copied to be save for later.\n var firstVertexX = vertex.x;\n var firstVertexY = vertex.y;\n while (!next.done) {\n var previousVertexX = vertex.x;\n var previousVertexY = vertex.y;\n next = verticesItr.next();\n if (next.done) {\n vertex.x = firstVertexX;\n vertex.y = firstVertexY;\n }\n else {\n vertex = next.value;\n }\n if ((vertex.y <= pixelCenterY && pixelCenterY < previousVertexY) ||\n (previousVertexY < pixelCenterY && pixelCenterY <= vertex.y)) {\n workingNodes.push(Math.round(vertex.x +\n ((pixelCenterY - vertex.y) / (previousVertexY - vertex.y)) *\n (previousVertexX - vertex.x)));\n }\n }\n // Sort the nodes, via a simple “Bubble” sort.\n {\n var i = 0;\n while (i < workingNodes.length - 1) {\n if (workingNodes[i] > workingNodes[i + 1]) {\n var swap = workingNodes[i];\n workingNodes[i] = workingNodes[i + 1];\n workingNodes[i + 1] = swap;\n if (i > 0)\n i--;\n }\n else {\n i++;\n }\n }\n }\n // Fill the pixels between node pairs.\n for (var i = 0; i < workingNodes.length; i += 2) {\n if (workingNodes[i] >= maxX) {\n break;\n }\n if (workingNodes[i + 1] <= minX) {\n continue;\n }\n if (workingNodes[i] < minX) {\n workingNodes[i] = minX;\n }\n if (workingNodes[i + 1] > maxX) {\n workingNodes[i + 1] = maxX;\n }\n checkAndFillY(pixelY, workingNodes[i], workingNodes[i + 1]);\n }\n }\n };\n ObstacleRasterizer.prototype.scanX = function (vertices, minX, maxX, minY, maxY, checkAndFillX) {\n var workingNodes = this.workingNodes;\n // Loop through the columns of the image.\n for (var pixelX = minX; pixelX < maxX; pixelX++) {\n var pixelCenterX = pixelX + 0.5;\n // Build a list of nodes.\n workingNodes.length = 0;\n var verticesItr = vertices[Symbol.iterator]();\n var next = verticesItr.next();\n var vertex = next.value;\n // The iterator always return the same instance.\n // It must be copied to be save for later.\n var firstVertexX = vertex.x;\n var firstVertexY = vertex.y;\n while (!next.done) {\n var previousVertexX = vertex.x;\n var previousVertexY = vertex.y;\n next = verticesItr.next();\n if (next.done) {\n vertex.x = firstVertexX;\n vertex.y = firstVertexY;\n }\n else {\n vertex = next.value;\n }\n if ((vertex.x < pixelCenterX && pixelCenterX < previousVertexX) ||\n (previousVertexX < pixelCenterX && pixelCenterX < vertex.x)) {\n workingNodes.push(Math.round(vertex.y +\n ((pixelCenterX - vertex.x) / (previousVertexX - vertex.x)) *\n (previousVertexY - vertex.y)));\n }\n }\n // Sort the nodes, via a simple “Bubble” sort.\n {\n var i = 0;\n while (i < workingNodes.length - 1) {\n if (workingNodes[i] > workingNodes[i + 1]) {\n var swap = workingNodes[i];\n workingNodes[i] = workingNodes[i + 1];\n workingNodes[i + 1] = swap;\n if (i > 0)\n i--;\n }\n else {\n i++;\n }\n }\n }\n // Fill the pixels between node pairs.\n for (var i = 0; i < workingNodes.length; i += 2) {\n if (workingNodes[i] >= maxY) {\n break;\n }\n if (workingNodes[i + 1] <= minY) {\n continue;\n }\n if (workingNodes[i] < minY) {\n workingNodes[i] = minY;\n }\n if (workingNodes[i + 1] > maxY) {\n workingNodes[i + 1] = maxY;\n }\n checkAndFillX(pixelX, workingNodes[i], workingNodes[i + 1]);\n }\n }\n };\n return ObstacleRasterizer;\n}());\n/**\n * Iterable that converts coordinates to the grid.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar GridBasisIterable = /** @class */ (function () {\n function GridBasisIterable() {\n this.grid = null;\n this.sceneVertices = [];\n this.verticesItr = this.sceneVertices[Symbol.iterator]();\n this.result = {\n value: { x: 0, y: 0 },\n done: false,\n };\n }\n GridBasisIterable.prototype.set = function (grid, sceneVertices) {\n this.grid = grid;\n this.sceneVertices = sceneVertices;\n };\n GridBasisIterable.prototype[Symbol.iterator] = function () {\n this.verticesItr = this.sceneVertices[Symbol.iterator]();\n return this;\n };\n GridBasisIterable.prototype.next = function () {\n var next = this.verticesItr.next();\n if (next.done) {\n return next;\n }\n this.grid.convertToGridBasis(next.value, this.result.value);\n return this.result;\n };\n return GridBasisIterable;\n}());\n\n/**\n * Build cohesive regions from the non-obstacle space. It uses the data\n * from the obstacles rasterization {@link ObstacleRasterizer}.\n *\n * This implementation is strongly inspired from CritterAI class \"OpenHeightfieldBuilder\".\n *\n * Introduction to Height Fields: http://www.critterai.org/projects/nmgen_study/heightfields.html\n *\n * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html\n */\nvar RegionGenerator = /** @class */ (function () {\n function RegionGenerator() {\n this.obstacleRegionBordersCleaner = new ObstacleRegionBordersCleaner();\n this.floodedCells = new Array(1024);\n this.workingStack = new Array(1024);\n }\n //TODO implement the smoothing pass on the distance field?\n /**\n * Groups cells into cohesive regions using an watershed based algorithm.\n *\n * This operation depends on neighbor and distance field information.\n * So {@link RegionGenerator.generateDistanceField} operations must be\n * run before this operation.\n *\n * @param grid A field with cell distance information fully generated.\n * @param obstacleCellPadding a padding in cells to apply around the\n * obstacles.\n */\n RegionGenerator.prototype.generateRegions = function (grid, obstacleCellPadding) {\n // Watershed Algorithm\n //\n // Reference: http://en.wikipedia.org/wiki/Watershed_%28algorithm%29\n // A good visualization:\n // http://artis.imag.fr/Publications/2003/HDS03/ (PDF)\n //\n // Summary:\n //\n // This algorithm utilizes the cell.distanceToObstacle value, which\n // is generated by the generateDistanceField() operation.\n //\n // Using the watershed analogy, the cells which are furthest from\n // a border (highest distance to border) represent the lowest points\n // in the watershed. A border cell represents the highest possible\n // water level.\n //\n // The main loop iterates, starting at the lowest point in the\n // watershed, then incrementing with each loop until the highest\n // allowed water level is reached. This slowly \"floods\" the cells\n // starting at the lowest points.\n //\n // During each iteration of the loop, cells that are below the\n // current water level are located and an attempt is made to either\n // add them to exiting regions or create new regions from them.\n //\n // During the region expansion phase, if a newly flooded cell\n // borders on an existing region, it is usually added to the region.\n //\n // Any newly flooded cell that survives the region expansion phase\n // is used as a seed for a new region.\n //\n // At the end of the main loop, a final region expansion is\n // performed which should catch any stray cells that escaped region\n // assignment during the main loop.\n // Represents the minimum distance to an obstacle that is considered\n // traversable. I.e. Can't traverse cells closer than this distance\n // to a border. This provides a way of artificially capping the\n // height to which watershed flooding can occur.\n // I.e. Don't let the algorithm flood all the way to the actual border.\n //\n // We add the minimum border distance to take into account the\n // blurring algorithm which can result in a border cell having a\n // border distance > 0.\n var distanceMin = obstacleCellPadding * 2;\n // TODO: EVAL: Figure out why this iteration limit is needed\n // (todo from the CritterAI sources).\n var expandIterations = 4 + distanceMin * 2;\n // Zero is reserved for the obstacle-region. So initializing to 1.\n var nextRegionID = 1;\n var floodedCells = this.floodedCells;\n // Search until the current distance reaches the minimum allowed\n // distance.\n //\n // Note: This loop will not necessarily complete all region\n // assignments. This is OK since a final region assignment step\n // occurs after the loop iteration is complete.\n for (\n // This value represents the current distance from the border which\n // is to be searched. The search starts at the maximum distance then\n // moves toward zero (toward borders).\n //\n // This number will always be divisible by 2.\n var distance = grid.obstacleDistanceMax() & ~1; distance > distanceMin; distance = Math.max(distance - 2, 0)) {\n // Find all cells that are at or below the current \"water level\"\n // and are not already assigned to a region. Add these cells to\n // the flooded cell list for processing.\n floodedCells.length = 0;\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.regionID === RasterizationCell.NULL_REGION_ID &&\n cell.distanceToObstacle >= distance) {\n // The cell is not already assigned a region and is\n // below the current \"water level\". So the cell can be\n // considered for region assignment.\n floodedCells.push(cell);\n }\n }\n }\n if (nextRegionID > 1) {\n // At least one region has already been created, so first\n // try to put the newly flooded cells into existing regions.\n if (distance > 0) {\n this.expandRegions(grid, floodedCells, expandIterations);\n }\n else {\n this.expandRegions(grid, floodedCells, -1);\n }\n }\n // Create new regions for all cells that could not be added to\n // existing regions.\n for (var _i = 0, floodedCells_1 = floodedCells; _i < floodedCells_1.length; _i++) {\n var floodedCell = floodedCells_1[_i];\n if (!floodedCell ||\n floodedCell.regionID !== RasterizationCell.NULL_REGION_ID) {\n // This cell was assigned to a newly created region\n // during an earlier iteration of this loop.\n // So it can be skipped.\n continue;\n }\n // Fill to slightly more than the current \"water level\".\n // This improves efficiency of the algorithm.\n // And it is necessary with the conservative expansion to ensure that\n // more than one cell is added initially to a new regions otherwise\n // no cell could be added to it later because of the conservative\n // constraint.\n var fillTo = Math.max(distance - 2, distanceMin + 1, 1);\n if (this.floodNewRegion(grid, floodedCell, fillTo, nextRegionID)) {\n nextRegionID++;\n }\n }\n }\n // Find all cells that haven't been assigned regions by the main loop\n // (up to the minimum distance).\n floodedCells.length = 0;\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.distanceToObstacle > distanceMin &&\n cell.regionID === RasterizationCell.NULL_REGION_ID) {\n // Not a border or obstacle region cell. Should be in a region.\n floodedCells.push(cell);\n }\n }\n }\n // Perform a final expansion of existing regions.\n // Allow more iterations than normal for this last expansion.\n if (distanceMin > 0) {\n this.expandRegions(grid, floodedCells, expandIterations * 8);\n }\n else {\n this.expandRegions(grid, floodedCells, -1);\n }\n grid.regionCount = nextRegionID;\n this.obstacleRegionBordersCleaner.fixObstacleRegion(grid);\n //TODO Also port FilterOutSmallRegions?\n // The algorithm to remove vertices in the middle (added at the end of\n // ContourBuilder.buildContours) may already filter them and contour are\n // faster to process than cells.\n };\n /**\n * Attempts to find the most appropriate regions to attach cells to.\n *\n * Any cells successfully attached to a region will have their list\n * entry set to null. So any non-null entries in the list will be cells\n * for which a region could not be determined.\n *\n * @param grid\n * @param inoutCells As input, the list of cells available for formation\n * of new regions. As output, the cells that could not be assigned\n * to new regions.\n * @param maxIterations If set to -1, will iterate through completion.\n */\n RegionGenerator.prototype.expandRegions = function (grid, inoutCells, iterationMax) {\n if (inoutCells.length === 0)\n return;\n var skipped = 0;\n for (var iteration = 0; (iteration < iterationMax || iterationMax === -1) &&\n // All cells have either been processed or could not be\n // processed during the last cycle.\n skipped < inoutCells.length; iteration++) {\n // The number of cells in the working list that have been\n // successfully processed or could not be processed successfully\n // for some reason.\n // This value controls when iteration ends.\n skipped = 0;\n for (var index = 0; index < inoutCells.length; index++) {\n var cell = inoutCells[index];\n if (cell === null) {\n // The cell originally at this index location has\n // already been successfully assigned a region. Nothing\n // else to do with it.\n skipped++;\n continue;\n }\n // Default to unassigned.\n var cellRegion = RasterizationCell.NULL_REGION_ID;\n var regionCenterDist = Number.MAX_VALUE;\n for (var _i = 0, _a = RasterizationGrid.neighbor4Deltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.regionID !== RasterizationCell.NULL_REGION_ID) {\n if (neighbor.distanceToRegionCore + 2 < regionCenterDist) {\n // This neighbor is closer to its region core\n // than previously detected neighbors.\n // Conservative expansion constraint:\n // Check to ensure that this neighbor has\n // at least two other neighbors in its region.\n // This makes sure that adding this cell to\n // this neighbor's region will not result\n // in a single width line of cells.\n var sameRegionCount = 0;\n for (var neighborDirection = 0; neighborDirection < 4; neighborDirection++) {\n var nnCell = grid.getNeighbor(neighbor, neighborDirection);\n // There is a diagonal-neighbor\n if (nnCell.regionID === neighbor.regionID) {\n // This neighbor has a neighbor in\n // the same region.\n sameRegionCount++;\n }\n }\n if (sameRegionCount > 1) {\n cellRegion = neighbor.regionID;\n regionCenterDist = neighbor.distanceToRegionCore + 2;\n }\n }\n }\n }\n if (cellRegion !== RasterizationCell.NULL_REGION_ID) {\n // Found a suitable region for this cell to belong to.\n // Mark this index as having been processed.\n inoutCells[index] = null;\n cell.regionID = cellRegion;\n cell.distanceToRegionCore = regionCenterDist;\n }\n else {\n // Could not find an existing region for this cell.\n skipped++;\n }\n }\n }\n };\n /**\n * Creates a new region surrounding a cell, adding neighbor cells to the\n * new region as appropriate.\n *\n * The new region creation will fail if the root cell is on the\n * border of an existing region.\n *\n * All cells added to the new region as part of this process become\n * \"core\" cells with a distance to region core of zero.\n *\n * @param grid\n * @param rootCell The cell used to seed the new region.\n * @param fillToDist The watershed distance to flood to.\n * @param regionID The region ID to use for the new region\n * (if creation is successful).\n * @return true if a new region was created.\n */\n RegionGenerator.prototype.floodNewRegion = function (grid, rootCell, fillToDist, regionID) {\n var workingStack = this.workingStack;\n workingStack.length = 0;\n workingStack.push(rootCell);\n rootCell.regionID = regionID;\n rootCell.distanceToRegionCore = 0;\n var regionSize = 0;\n var cell;\n while ((cell = workingStack.pop())) {\n // Check regions of neighbor cells.\n //\n // If any neighbor is found to have a region assigned, then\n // the current cell can't be in the new region\n // (want standard flooding algorithm to handle deciding which\n // region this cell should go in).\n //\n // Up to 8 neighbors are checked.\n //\n // Neighbor searches:\n // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch\n var isOnRegionBorder = false;\n for (var _i = 0, _a = RasterizationGrid.neighbor8Deltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n isOnRegionBorder =\n neighbor.regionID !== RasterizationCell.NULL_REGION_ID &&\n neighbor.regionID !== regionID;\n if (isOnRegionBorder)\n break;\n }\n if (isOnRegionBorder) {\n cell.regionID = RasterizationCell.NULL_REGION_ID;\n continue;\n }\n regionSize++;\n // If got this far, we know the current cell is part of the new\n // region. Now check its neighbors to see if they should be\n // assigned to this new region.\n for (var _b = 0, _c = RasterizationGrid.neighbor4Deltas; _b < _c.length; _b++) {\n var delta = _c[_b];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.distanceToObstacle >= fillToDist &&\n neighbor.regionID === RasterizationCell.NULL_REGION_ID) {\n neighbor.regionID = regionID;\n neighbor.distanceToRegionCore = 0;\n workingStack.push(neighbor);\n }\n }\n }\n return regionSize > 0;\n };\n /**\n * Generates distance field information.\n * The {@link RasterizationCell.distanceToObstacle} information is generated\n * for all cells in the field.\n *\n * All distance values are relative and do not represent explicit\n * distance values (such as grid unit distance). The algorithm which is\n * used results in an approximation only. It is not exhaustive.\n *\n * The data generated by this operation is required by\n * {@link RegionGenerator.generateRegions}.\n *\n * @param grid A field with cells obstacle information already generated.\n */\n RegionGenerator.prototype.generateDistanceField = function (grid) {\n // close borders\n for (var x = 0; x < grid.dimX(); x++) {\n var leftCell = grid.get(x, 0);\n leftCell.distanceToObstacle = 0;\n var rightCell = grid.get(x, grid.dimY() - 1);\n rightCell.distanceToObstacle = 0;\n }\n for (var y = 1; y < grid.dimY() - 1; y++) {\n var topCell = grid.get(0, y);\n topCell.distanceToObstacle = 0;\n var bottomCell = grid.get(grid.dimX() - 1, y);\n bottomCell.distanceToObstacle = 0;\n }\n // The next two phases basically check the neighbors of a cell and\n // set the cell's distance field to be slightly greater than the\n // neighbor with the lowest border distance. Distance is increased\n // slightly more for diagonal-neighbors than for axis-neighbors.\n // 1st pass\n // During this pass, the following neighbors are checked:\n // (-1, 0) (-1, -1) (0, -1) (1, -1)\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n for (var _i = 0, _a = RegionGenerator.firstPassDeltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +\n delta.distance;\n if (cell.distanceToObstacle > distanceByNeighbor) {\n cell.distanceToObstacle = distanceByNeighbor;\n }\n }\n }\n }\n // 2nd pass\n // During this pass, the following neighbors are checked:\n // (1, 0) (1, 1) (0, 1) (-1, 1)\n //\n // Besides checking different neighbors, this pass performs its\n // grid search in reverse order.\n for (var y = grid.dimY() - 2; y >= 1; y--) {\n for (var x = grid.dimX() - 2; x >= 1; x--) {\n var cell = grid.get(x, y);\n for (var _b = 0, _c = RegionGenerator.secondPassDeltas; _b < _c.length; _b++) {\n var delta = _c[_b];\n var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +\n delta.distance;\n if (cell.distanceToObstacle > distanceByNeighbor) {\n cell.distanceToObstacle = distanceByNeighbor;\n }\n }\n }\n }\n };\n RegionGenerator.firstPassDeltas = [\n { x: -1, y: 0, distance: 2 },\n { x: -1, y: -1, distance: 3 },\n { x: 0, y: -1, distance: 2 },\n { x: 1, y: -1, distance: 3 },\n ];\n RegionGenerator.secondPassDeltas = [\n { x: 1, y: 0, distance: 2 },\n { x: 1, y: 1, distance: 3 },\n { x: 0, y: 1, distance: 2 },\n { x: -1, y: 1, distance: 3 },\n ];\n return RegionGenerator;\n}());\n/**\n * Implements three algorithms that clean up issues that can\n * develop around obstacle region boarders.\n *\n * - Detect and fix encompassed obstacle regions:\n *\n * If a obstacle region is found that is fully encompassed by a single\n * region, then the region will be split into two regions at the\n * obstacle region border.\n *\n * - Detect and fix \"short wrapping\" of obstacle regions:\n *\n * Regions can sometimes wrap slightly around the corner of a obstacle region\n * in a manner that eventually results in the formation of self-intersecting\n * polygons.\n *\n * Example: Before the algorithm is applied:\n * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_08_cornerwrapbefore.jpg\"\n *\n * Example: After the algorithm is applied:\n * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_09_cornerwrapafter.jpg\n *\n * - Detect and fix incomplete obstacle region connections:\n *\n * If a region touches obstacle region only diagonally, then contour detection\n * algorithms may not properly detect the obstacle region connection. This can\n * adversely effect other algorithms in the pipeline.\n *\n * Example: Before algorithm is applied:\n *\n * b b a a a a\n * b b a a a a\n * a a x x x x\n * a a x x x x\n *\n * Example: After algorithm is applied:\n *\n * b b a a a a\n * b b b a a a <-- Cell transferred to region B.\n * a a x x x x\n * a a x x x x\n *\n *\n * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html\n */\nvar ObstacleRegionBordersCleaner = /** @class */ (function () {\n function ObstacleRegionBordersCleaner() {\n this.workingUpLeftOpenCells = new Array(512);\n this.workingDownRightOpenCells = new Array(512);\n this.workingOpenCells = new Array(512);\n }\n /**\n * This operation utilizes {@link RasterizationCell.contourFlags}. It\n * expects the value to be zero on entry, and re-zero's the value\n * on exit.\n *\n * @param grid a grid with fully built regions.\n */\n ObstacleRegionBordersCleaner.prototype.fixObstacleRegion = function (grid) {\n var workingUpLeftOpenCells = this.workingUpLeftOpenCells;\n workingUpLeftOpenCells.length = 0;\n var workingDownRightOpenCells = this.workingDownRightOpenCells;\n workingDownRightOpenCells.length = 0;\n var workingOpenCells = this.workingOpenCells;\n workingOpenCells.length = 0;\n var extremeCells = [\n null,\n null,\n ];\n var nextRegionID = grid.regionCount;\n // Iterate over the cells, trying to find obstacle region borders.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.contourFlags !== 0)\n // Cell was processed in a previous iteration.\n // Ignore it.\n continue;\n cell.contourFlags = 1;\n var workingCell = null;\n var edgeDirection = -1;\n if (cell.regionID !== RasterizationCell.OBSTACLE_REGION_ID) {\n // Not interested in this cell.\n continue;\n }\n // This is a obstacle region cell. See if it\n // connects to a cell in a non-obstacle region.\n edgeDirection = this.getNonNullBorderDirection(grid, cell);\n if (edgeDirection === -1)\n // This cell is not a border cell. Ignore it.\n continue;\n // This is a border cell. Step into the non-null\n // region and swing the direction around 180 degrees.\n workingCell = grid.getNeighbor(cell, edgeDirection);\n edgeDirection = (edgeDirection + 2) & 0x3;\n // Process the obstacle region contour. Detect and fix\n // local issues. Determine if the region is\n // fully encompassed by a single non-obstacle region.\n var isEncompassedNullRegion = this.processNullRegion(grid, workingCell, edgeDirection, extremeCells);\n if (isEncompassedNullRegion) {\n // This cell is part of a group of obstacle region cells\n // that is encompassed within a single non-obstacle region.\n // This is not permitted. Need to fix it.\n this.partialFloodRegion(grid, extremeCells[0], extremeCells[1], nextRegionID);\n nextRegionID++;\n }\n }\n }\n grid.regionCount = nextRegionID;\n // Clear all flags.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n cell.contourFlags = 0;\n }\n }\n };\n /**\n * Partially flood a region away from the specified direction.\n *\n * {@link RasterizationCell.contourFlags}\n * is set to zero for all flooded cells.\n *\n * @param grid\n * @param startCell The cell to start the flood from.\n * @param borderDirection The hard border for flooding. No\n * cells in this direction from the startCell will be flooded.\n * @param newRegionID The region id to assign the flooded\n * cells to.\n */\n ObstacleRegionBordersCleaner.prototype.partialFloodRegion = function (grid, upLeftCell, downRightCell, newRegionID) {\n var upLeftOpenCells = this.workingUpLeftOpenCells;\n var downRightOpenCells = this.workingDownRightOpenCells;\n var workingOpenCells = this.workingOpenCells;\n // The implementation differs from CritterAI to avoid non-contiguous\n // sections. Instead of brushing in one direction, it floods from\n // 2 extremities of the encompassed obstacle region.\n var regionID = upLeftCell.regionID;\n if (regionID === newRegionID) {\n // avoid infinity loop\n console.error(\"Can't create a new region with an ID that already exist.\");\n return;\n }\n // The 1st flooding set a new the regionID\n upLeftCell.regionID = newRegionID;\n upLeftCell.distanceToRegionCore = 0; // This information is lost.\n upLeftOpenCells.length = 0;\n upLeftOpenCells.push(upLeftCell);\n // The 2nd flooding keep the regionID and mark the cell as visited.\n downRightCell.contourFlags = 2;\n downRightCell.distanceToRegionCore = 0; // This information is lost.\n downRightOpenCells.length = 0;\n downRightOpenCells.push(downRightCell);\n var swap;\n workingOpenCells.length = 0;\n while (upLeftOpenCells.length !== 0 || downRightOpenCells.length !== 0) {\n for (var _i = 0, upLeftOpenCells_1 = upLeftOpenCells; _i < upLeftOpenCells_1.length; _i++) {\n var cell = upLeftOpenCells_1[_i];\n for (var direction = 0; direction < 4; direction++) {\n var neighbor = grid.getNeighbor(cell, direction);\n if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {\n continue;\n }\n // Transfer the neighbor to the new region.\n neighbor.regionID = newRegionID;\n neighbor.distanceToRegionCore = 0; // This information is lost.\n workingOpenCells.push(neighbor);\n }\n }\n // This allows to flood the nearest cells first without needing lifo queue.\n // But a queue would take less memory.\n swap = upLeftOpenCells;\n upLeftOpenCells = workingOpenCells;\n workingOpenCells = swap;\n workingOpenCells.length = 0;\n for (var _a = 0, downRightOpenCells_1 = downRightOpenCells; _a < downRightOpenCells_1.length; _a++) {\n var cell = downRightOpenCells_1[_a];\n for (var direction = 0; direction < 4; direction++) {\n var neighbor = grid.getNeighbor(cell, direction);\n if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {\n continue;\n }\n // Keep the neighbor to the current region.\n neighbor.contourFlags = 2;\n neighbor.distanceToRegionCore = 0; // This information is lost.\n workingOpenCells.push(neighbor);\n }\n }\n swap = downRightOpenCells;\n downRightOpenCells = workingOpenCells;\n workingOpenCells = swap;\n workingOpenCells.length = 0;\n }\n };\n /**\n * Detects and fixes bad cell configurations in the vicinity of a\n * obstacle region contour (See class description for details).\n * @param grid\n * @param startCell A cell in a non-obstacle region that borders a null\n * region.\n * @param startDirection The direction of the obstacle region border.\n * @return TRUE if the start cell's region completely encompasses\n * the obstacle region.\n */\n ObstacleRegionBordersCleaner.prototype.processNullRegion = function (grid, startCell, startDirection, extremeCells) {\n // This algorithm traverses the contour. As it does so, it detects\n // and fixes various known dangerous cell configurations.\n //\n // Traversing the contour: A good way to visualize it is to think\n // of a robot sitting on the floor facing a known wall. It then\n // does the following to skirt the wall:\n // 1. If there is a wall in front of it, turn clockwise in 90 degrees\n // increments until it finds the wall is gone.\n // 2. Move forward one step.\n // 3. Turn counter-clockwise by 90 degrees.\n // 4. Repeat from step 1 until it finds itself at its original\n // location facing its original direction.\n //\n // See also: http://www.critterai.org/projects/nmgen_study/regiongen.html#robotwalk\n //\n // As the traversal occurs, the number of acute (90 degree) and\n // obtuse (270 degree) corners are monitored. If a complete contour is\n // detected and (obtuse corners > acute corners), then the null\n // region is inside the contour. Otherwise the obstacle region is\n // outside the contour, which we don't care about.\n var borderRegionID = startCell.regionID;\n // Prepare for loop.\n var cell = startCell;\n var neighbor = null;\n var direction = startDirection;\n var upLeftCell = cell;\n var downRightCell = cell;\n // Initialize monitoring variables.\n var loopCount = 0;\n var acuteCornerCount = 0;\n var obtuseCornerCount = 0;\n var stepsWithoutBorder = 0;\n var borderSeenLastLoop = false;\n var isBorder = true; // Initial value doesn't matter.\n // Assume a single region is connected to the obstacle region\n // until proven otherwise.\n var hasSingleConnection = true;\n // The loop limit exists for the sole reason of preventing\n // an infinite loop in case of bad input data.\n // It is set to a very high value because there is no way of\n // definitively determining a safe smaller value. Setting\n // the value too low can result in rescanning a contour\n // multiple times, killing performance.\n while (++loopCount < 1 << 30) {\n // Get the cell across the border.\n neighbor = grid.getNeighbor(cell, direction);\n // Detect which type of edge this direction points across.\n if (neighbor === null) {\n // It points across a obstacle region border edge.\n isBorder = true;\n }\n else {\n // We never need to perform contour detection\n // on this cell again. So mark it as processed.\n neighbor.contourFlags = 1;\n if (neighbor.regionID === RasterizationCell.OBSTACLE_REGION_ID) {\n // It points across a obstacle region border edge.\n isBorder = true;\n }\n else {\n // This isn't a obstacle region border.\n isBorder = false;\n if (neighbor.regionID !== borderRegionID)\n // It points across a border to a non-obstacle region.\n // This means the current contour can't\n // represent a fully encompassed obstacle region.\n hasSingleConnection = false;\n }\n }\n // Process the border.\n if (isBorder) {\n // It is a border edge.\n if (borderSeenLastLoop) {\n // A border was detected during the last loop as well.\n // Two detections in a row indicates we passed an acute\n // (inner) corner.\n //\n // a x\n // x x\n acuteCornerCount++;\n }\n else if (stepsWithoutBorder > 1) {\n // We have moved at least two cells before detecting\n // a border. This indicates we passed an obtuse\n // (outer) corner.\n //\n // a a\n // a x\n obtuseCornerCount++;\n stepsWithoutBorder = 0;\n // Detect and fix cell configuration issue around this\n // corner.\n if (this.processOuterCorner(grid, cell, direction))\n // A change was made and it resulted in the\n // corner area having multiple region connections.\n hasSingleConnection = false;\n }\n direction = (direction + 1) & 0x3; // Rotate in clockwise direction.\n borderSeenLastLoop = true;\n stepsWithoutBorder = 0;\n }\n else {\n // Not a obstacle region border.\n // Move to the neighbor and swing the search direction back\n // one increment (counterclockwise). By moving the direction\n // back one increment we guarantee we don't miss any edges.\n cell = neighbor;\n direction = (direction + 3) & 0x3; // Rotate counterclockwise direction.\n borderSeenLastLoop = false;\n stepsWithoutBorder++;\n if (cell.x < upLeftCell.x ||\n (cell.x === upLeftCell.x && cell.y < upLeftCell.y)) {\n upLeftCell = cell;\n }\n if (cell.x > downRightCell.x ||\n (cell.x === downRightCell.x && cell.y > downRightCell.y)) {\n downRightCell = cell;\n }\n }\n if (startCell === cell && startDirection === direction) {\n extremeCells[0] = upLeftCell;\n extremeCells[1] = downRightCell;\n // Have returned to the original cell and direction.\n // The search is complete.\n // Is the obstacle region inside the contour?\n return hasSingleConnection && obtuseCornerCount > acuteCornerCount;\n }\n }\n // If got here then the obstacle region boarder is too large to be fully\n // explored. So it can't be encompassed.\n return false;\n };\n /**\n * Detects and fixes cell configuration issues in the vicinity\n * of obtuse (outer) obstacle region corners.\n * @param grid\n * @param referenceCell The cell in a non-obstacle region that is\n * just past the outer corner.\n * @param borderDirection The direction of the obstacle region border.\n * @return TRUE if more than one region connects to the obstacle region\n * in the vicinity of the corner (this may or may not be due to\n * a change made by this operation).\n */\n ObstacleRegionBordersCleaner.prototype.processOuterCorner = function (grid, referenceCell, borderDirection) {\n var hasMultiRegions = false;\n // Get the previous two cells along the border.\n var backOne = grid.getNeighbor(referenceCell, (borderDirection + 3) & 0x3);\n var backTwo = grid.getNeighbor(backOne, borderDirection);\n var testCell;\n if (backOne.regionID !== referenceCell.regionID &&\n // This differ from the CritterAI implementation.\n // To filter vertices in the middle, this must be avoided too:\n // a x\n // b c\n backTwo.regionID !== backOne.regionID) {\n // Dangerous corner configuration.\n //\n // a x\n // b a\n //\n // Need to change to one of the following configurations:\n //\n // b x a x\n // b a b b\n //\n // Reason: During contour detection this type of configuration can\n // result in the region connection being detected as a\n // region-region portal, when it is not. The region connection\n // is actually interrupted by the obstacle region.\n //\n // This configuration has been demonstrated to result in\n // two regions being improperly merged to encompass an\n // internal obstacle region.\n //\n // Example:\n //\n // a a x x x a\n // a a x x a a\n // b b a a a a\n // b b a a a a\n //\n // During contour and connection detection for region b, at no\n // point will the obstacle region be detected. It will appear\n // as if a clean a-b portal exists.\n //\n // An investigation into fixing this issue via updates to the\n // watershed or contour detection algorithms did not turn\n // up a better way of resolving this issue.\n hasMultiRegions = true;\n // Determine how many connections backTwo has to backOne's region.\n testCell = grid.getNeighbor(backOne, (borderDirection + 3) & 0x3);\n var backTwoConnections = 0;\n if (testCell.regionID === backOne.regionID) {\n backTwoConnections++;\n testCell = grid.getNeighbor(testCell, borderDirection);\n if (testCell.regionID === backOne.regionID)\n backTwoConnections++;\n }\n // Determine how many connections the reference cell has\n // to backOne's region.\n var referenceConnections = 0;\n testCell = grid.getNeighbor(backOne, (borderDirection + 2) & 0x3);\n if (testCell.regionID === backOne.regionID) {\n referenceConnections++;\n testCell = grid.getNeighbor(testCell, (borderDirection + 2) & 0x3);\n if (testCell.regionID === backOne.regionID)\n backTwoConnections++;\n }\n // Change the region of the cell that has the most connections\n // to the target region.\n if (referenceConnections > backTwoConnections)\n referenceCell.regionID = backOne.regionID;\n else\n backTwo.regionID = backOne.regionID;\n }\n else if (backOne.regionID === referenceCell.regionID &&\n backTwo.regionID === referenceCell.regionID) {\n // Potential dangerous short wrap.\n //\n // a x\n // a a\n //\n // Example of actual problem configuration:\n //\n // b b x x\n // b a x x <- Short wrap.\n // b a a a\n //\n // In the above case, the short wrap around the corner of the\n // obstacle region has been demonstrated to cause self-intersecting\n // polygons during polygon formation.\n //\n // This algorithm detects whether or not one (and only one)\n // of the axis neighbors of the corner should be re-assigned to\n // a more appropriate region.\n //\n // In the above example, the following configuration is more\n // appropriate:\n //\n // b b x x\n // b b x x <- Change to this row.\n // b a a a\n // Check to see if backTwo should be in a different region.\n var selectedRegion = this.selectedRegionID(grid, backTwo, (borderDirection + 1) & 0x3, (borderDirection + 2) & 0x3);\n if (selectedRegion === backTwo.regionID) {\n // backTwo should not be re-assigned. How about\n // the reference cell?\n selectedRegion = this.selectedRegionID(grid, referenceCell, borderDirection, (borderDirection + 3) & 0x3);\n if (selectedRegion !== referenceCell.regionID) {\n // The reference cell should be reassigned\n // to a new region.\n referenceCell.regionID = selectedRegion;\n hasMultiRegions = true;\n }\n }\n else {\n // backTwo should be re-assigned to a new region.\n backTwo.regionID = selectedRegion;\n hasMultiRegions = true;\n }\n }\n else\n hasMultiRegions = true;\n // No dangerous configurations detected. But definitely\n // has a change in regions at the corner. We know this\n // because one of the previous checks looked for a single\n // region for all wrap cells.\n return hasMultiRegions;\n };\n /**\n * Checks the cell to see if it should be reassigned to a new region.\n *\n * @param grid\n * @param referenceCell A cell on one side of an obstacle region contour's\n * outer corner. It is expected that the all cells that wrap the\n * corner are in the same region.\n * @param borderDirection The direction of the obstacle region border.\n * @param cornerDirection The direction of the outer corner from the\n * reference cell.\n * @return The region the cell should be a member of. May be the\n * region the cell is currently a member of.\n */\n ObstacleRegionBordersCleaner.prototype.selectedRegionID = function (grid, referenceCell, borderDirection, cornerDirection) {\n // Initial example state:\n //\n // a - Known region.\n // x - Null region.\n // u - Unknown, not checked yet.\n //\n // u u u\n // u a x\n // u a a\n // The only possible alternate region id is from\n // the cell that is opposite the border. So check it first.\n var regionID = grid.getNeighbor(referenceCell, (borderDirection + 2) & 0x3)\n .regionID;\n if (regionID === referenceCell.regionID ||\n regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // The region away from the border is either a obstacle region\n // or the same region. So we keep the current region.\n //\n // u u u u u u\n // a a x or x a x <-- Potentially bad, but stuck with it.\n // u a a u a a\n return referenceCell.regionID;\n // Candidate region for re-assignment.\n var potentialRegion = regionID;\n // Next we check the region opposite from the corner direction.\n // If it is the current region, then we definitely can't\n // change the region id without risk of splitting the region.\n regionID = grid.getNeighbor(referenceCell, (cornerDirection + 2) & 0x3)\n .regionID;\n if (regionID === referenceCell.regionID ||\n regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // The region opposite from the corner direction is\n // either a obstacle region or the same region. So we\n // keep the current region.\n //\n // u a u u x u\n // b a x or b a x\n // u a a u a a\n return referenceCell.regionID;\n // We have checked the early exit special cases. Now a generalized\n // brute count is performed.\n //\n // Priority is given to the potential region. Here is why:\n // (Highly unlikely worst case scenario)\n //\n // c c c c c c\n // b a x -> b b x Select b even though b count == a count.\n // b a a b a a\n // Neighbors in potential region.\n // We know this will have a minimum value of 1.\n var potentialCount = 0;\n // Neighbors in the cell's current region.\n // We know this will have a minimum value of 2.\n var currentCount = 0;\n // Maximum edge case:\n //\n // b b b\n // b a x\n // b a a\n //\n // The maximum edge case for region A can't exist. It\n // is filtered out during one of the earlier special cases\n // handlers.\n //\n // Other cases may exist if more regions are involved.\n // Such cases will tend to favor the current region.\n for (var direction = 0; direction < 8; direction++) {\n var regionID_1 = grid.getNeighbor(referenceCell, direction).regionID;\n if (regionID_1 === referenceCell.regionID)\n currentCount++;\n else if (regionID_1 === potentialRegion)\n potentialCount++;\n }\n return potentialCount < currentCount\n ? referenceCell.regionID\n : potentialRegion;\n };\n /**\n * Returns the direction of the first neighbor in a non-obstacle region.\n * @param grid\n * @param cell The cell to check.\n * @return The direction of the first neighbor in a non-obstacle region, or\n * -1 if all neighbors are in the obstacle region.\n */\n ObstacleRegionBordersCleaner.prototype.getNonNullBorderDirection = function (grid, cell) {\n // Search axis-neighbors.\n for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {\n var delta = RasterizationGrid.neighbor4Deltas[direction];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.regionID !== RasterizationCell.OBSTACLE_REGION_ID)\n // The neighbor is a obstacle region.\n return direction;\n }\n // All neighbors are in a non-obstacle region.\n return -1;\n };\n return ObstacleRegionBordersCleaner;\n}());\n\n// This implementation is strongly inspired from a Java one\n// by Stephen A. Pratt:\n// http://www.critterai.org/projects/nmgen_study/\n//\n// Most of the comments were written by him and were adapted to fit this implementation.\n// This implementation differs a bit from the original:\n// - it's only 2D instead of 3D\n// - it has less features (see TODO) and might have lesser performance\n// - it uses objects for points instead of pointer-like in arrays of numbers\n// - the rasterization comes from other sources because of the 2d focus\n// - partialFloodRegion was rewritten to fix an issue\n// - filterNonObstacleVertices was added\n//\n// The Java implementation was also inspired from Recast that can be found here:\n// https://github.com/recastnavigation/recastnavigation\nvar NavMeshGenerator = /** @class */ (function () {\n function NavMeshGenerator(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, isometricRatio) {\n if (isometricRatio === void 0) { isometricRatio = 1; }\n this.grid = new RasterizationGrid(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, \n // make cells square in the world\n rasterizationCellSize / isometricRatio);\n this.isometricRatio = isometricRatio;\n this.obstacleRasterizer = new ObstacleRasterizer();\n this.regionGenerator = new RegionGenerator();\n this.contourBuilder = new ContourBuilder();\n this.convexPolygonGenerator = new ConvexPolygonGenerator();\n this.gridCoordinateConverter = new GridCoordinateConverter();\n }\n NavMeshGenerator.prototype.buildNavMesh = function (obstacles, obstacleCellPadding) {\n var _this = this;\n this.grid.clear();\n this.obstacleRasterizer.rasterizeObstacles(this.grid, obstacles);\n this.regionGenerator.generateDistanceField(this.grid);\n this.regionGenerator.generateRegions(this.grid, obstacleCellPadding);\n // It's probably not a good idea to expose the vectorization threshold.\n // As stated in the parameter documentation, the value 1 gives good\n // results in any situations.\n var threshold = 1;\n var contours = this.contourBuilder.buildContours(this.grid, threshold);\n var meshField = this.convexPolygonGenerator.splitToConvexPolygons(contours, 16);\n var scaledMeshField = this.gridCoordinateConverter.convertFromGridBasis(this.grid, meshField);\n if (this.isometricRatio != 1) {\n // Rescale the mesh to have the same unit length on the 2 axis for the pathfinding.\n scaledMeshField.forEach(function (polygon) {\n return polygon.forEach(function (point) {\n point.y *= _this.isometricRatio;\n });\n });\n }\n return scaledMeshField;\n };\n return NavMeshGenerator;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * PathfindingObstaclesManager manages the common objects shared by objects having a\n * pathfinding behavior: In particular, the obstacles behaviors are required to declare\n * themselves (see `PathfindingObstaclesManager.addObstacle`) to the manager of their associated scene\n * (see `gdjs.NavMeshPathfindingRuntimeBehavior.obstaclesManagers`).\n */\nvar NavMeshPathfindingObstaclesManager = /** @class */ (function () {\n function NavMeshPathfindingObstaclesManager(instanceContainer, configuration) {\n /**\n * The navigation meshes by moving object size\n * (rounded on _cellSize)\n */\n this._navMeshes = new Map();\n /**\n * Used while NavMeshes update is disabled to remember to do the update\n * when it's enable back.\n */\n this._navMeshesAreUpToDate = true;\n /**\n * This allows to continue finding paths with the old NavMeshes while\n * moving obstacles.\n */\n this._navMeshesUpdateIsEnabled = true;\n var viewpoint = configuration._getViewpoint();\n if (viewpoint === 'Isometry 2:1 (26.565°)') {\n configuration._setIsometricRatio(2);\n }\n else if (viewpoint === 'True Isometry (30°)') {\n configuration._setIsometricRatio(Math.sqrt(3));\n }\n else {\n configuration._setIsometricRatio(1);\n }\n if (configuration._getCellSize() <= 0) {\n configuration._setCellSize(10);\n }\n if (configuration._getAreaLeftBound() === 0 &&\n configuration._getAreaTopBound() === 0 &&\n configuration._getAreaRightBound() === 0 &&\n configuration._getAreaBottomBound() === 0) {\n var game = instanceContainer.getGame();\n configuration._setAreaLeftBound(0);\n configuration._setAreaTopBound(0);\n configuration._setAreaRightBound(game.getGameResolutionWidth());\n configuration._setAreaBottomBound(game.getGameResolutionHeight());\n }\n this.configuration = configuration;\n this._obstacles = new Set();\n this._polygonIterableAdapter = new PolygonIterableAdapter();\n this._navMeshGenerator = new NavMeshGenerator(configuration._getAreaLeftBound(), configuration._getAreaTopBound(), configuration._getAreaRightBound(), configuration._getAreaBottomBound(), configuration._getCellSize(), \n // make cells square in the world\n configuration._getIsometricRatio());\n }\n /**\n * Get the obstacles manager of a scene.\n */\n NavMeshPathfindingObstaclesManager.getManager = function (instanceContainer) {\n // @ts-ignore\n return instanceContainer.navMeshPathfindingObstaclesManager;\n };\n NavMeshPathfindingObstaclesManager.getManagerOrCreate = function (instanceContainer, configuration) {\n // @ts-ignore\n if (!instanceContainer.navMeshPathfindingObstaclesManager) {\n // Create the shared manager if necessary.\n // @ts-ignore\n instanceContainer.navMeshPathfindingObstaclesManager = new NavMeshPathfindingObstaclesManager(instanceContainer, configuration);\n }\n // @ts-ignore\n return instanceContainer.navMeshPathfindingObstaclesManager;\n };\n NavMeshPathfindingObstaclesManager.prototype.setNavMeshesUpdateEnabled = function (navMeshesUpdateIsEnabled) {\n this._navMeshesUpdateIsEnabled = navMeshesUpdateIsEnabled;\n if (navMeshesUpdateIsEnabled && !this._navMeshesAreUpToDate) {\n this._navMeshes.clear();\n this._navMeshesAreUpToDate = true;\n }\n };\n /**\n * Add a obstacle to the list of existing obstacles.\n */\n NavMeshPathfindingObstaclesManager.prototype.addObstacle = function (pathfindingObstacleBehavior) {\n this._obstacles.add(pathfindingObstacleBehavior.behavior.owner);\n this.invalidateNavMesh();\n };\n /**\n * Remove a obstacle from the list of existing obstacles. Be sure that the obstacle was\n * added before.\n */\n NavMeshPathfindingObstaclesManager.prototype.removeObstacle = function (pathfindingObstacleBehavior) {\n this._obstacles.delete(pathfindingObstacleBehavior.behavior.owner);\n this.invalidateNavMesh();\n };\n NavMeshPathfindingObstaclesManager.prototype.invalidateNavMesh = function () {\n if (this._navMeshesUpdateIsEnabled) {\n this._navMeshes.clear();\n this._navMeshesAreUpToDate = true;\n }\n else {\n this._navMeshesAreUpToDate = false;\n }\n };\n NavMeshPathfindingObstaclesManager.prototype.getNavMesh = function (obstacleCellPadding) {\n var navMesh = this._navMeshes.get(obstacleCellPadding);\n if (!navMesh) {\n var navMeshPolygons = this._navMeshGenerator.buildNavMesh(this._getVerticesIterable(this._obstacles), obstacleCellPadding);\n navMesh = new NavMesh(navMeshPolygons);\n this._navMeshes.set(obstacleCellPadding, navMesh);\n }\n return navMesh;\n };\n NavMeshPathfindingObstaclesManager.prototype._getVerticesIterable = function (objects) {\n this._polygonIterableAdapter.set(objects);\n return this._polygonIterableAdapter;\n };\n return NavMeshPathfindingObstaclesManager;\n}());\n/**\n * Iterable that adapts `RuntimeObject` to `Iterable<{x: float y: float}>`.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar PolygonIterableAdapter = /** @class */ (function () {\n function PolygonIterableAdapter() {\n this.objects = [];\n this.objectsItr = this.objects[Symbol.iterator]();\n this.polygonsItr = [][Symbol.iterator]();\n this.pointIterableAdapter = new PointIterableAdapter();\n this.result = {\n value: this.pointIterableAdapter,\n done: false,\n };\n }\n PolygonIterableAdapter.prototype.set = function (objects) {\n this.objects = objects;\n };\n PolygonIterableAdapter.prototype[Symbol.iterator] = function () {\n this.objectsItr = this.objects[Symbol.iterator]();\n this.polygonsItr = [][Symbol.iterator]();\n return this;\n };\n PolygonIterableAdapter.prototype.next = function () {\n var polygonNext = this.polygonsItr.next();\n while (polygonNext.done) {\n var objectNext = this.objectsItr.next();\n if (objectNext.done) {\n // IteratorReturnResult require a defined value\n // even though the spec state otherwise.\n // So, this class can't be typed as an iterable.\n this.result.value = undefined;\n this.result.done = true;\n return this.result;\n }\n this.polygonsItr = objectNext.value.getHitBoxes().values();\n polygonNext = this.polygonsItr.next();\n }\n this.pointIterableAdapter.set(polygonNext.value.vertices);\n this.result.value = this.pointIterableAdapter;\n this.result.done = false;\n return this.result;\n };\n return PolygonIterableAdapter;\n}());\n/**\n * Iterable that adapts coordinates from `[int, int]` to `{x: int, y: int}`.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar PointIterableAdapter = /** @class */ (function () {\n function PointIterableAdapter() {\n this.vertices = [];\n this.verticesItr = this.vertices[Symbol.iterator]();\n this.result = {\n value: { x: 0, y: 0 },\n done: false,\n };\n }\n PointIterableAdapter.prototype.set = function (vertices) {\n this.vertices = vertices;\n };\n PointIterableAdapter.prototype[Symbol.iterator] = function () {\n this.verticesItr = this.vertices[Symbol.iterator]();\n return this;\n };\n PointIterableAdapter.prototype.next = function () {\n var next = this.verticesItr.next();\n if (next.done) {\n return next;\n }\n this.result.value.x = next.value[0];\n this.result.value.y = next.value[1];\n return this.result;\n };\n return PointIterableAdapter;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar NavMeshRenderer = /** @class */ (function () {\n function NavMeshRenderer() {\n /** Used to draw traces for debugging */\n this._lastUsedObstacleCellPadding = null;\n }\n NavMeshRenderer.prototype.setLastUsedObstacleCellPadding = function (lastUsedObstacleCellPadding) {\n this._lastUsedObstacleCellPadding = lastUsedObstacleCellPadding;\n };\n NavMeshRenderer.prototype.render = function (instanceContainer, shapePainter) {\n if (this._lastUsedObstacleCellPadding === null) {\n return;\n }\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n // TODO find a way to rebuild drawing only when necessary.\n // Draw the navigation mesh on a shape painter object for debugging purpose\n var navMesh = manager.getNavMesh(this._lastUsedObstacleCellPadding);\n for (var _i = 0, _a = navMesh.getPolygons(); _i < _a.length; _i++) {\n var navPoly = _a[_i];\n var polygon = navPoly.getPoints();\n if (polygon.length === 0)\n continue;\n for (var index = 1; index < polygon.length; index++) {\n // It helps to spot vertices with 180° between edges.\n shapePainter.drawCircle(polygon[index].x, polygon[index].y / isometricRatio, 3);\n }\n }\n for (var _b = 0, _c = navMesh.getPolygons(); _b < _c.length; _b++) {\n var navPoly = _c[_b];\n var polygon = navPoly.getPoints();\n if (polygon.length === 0)\n continue;\n shapePainter.beginFillPath(polygon[0].x, polygon[0].y / isometricRatio);\n for (var index = 1; index < polygon.length; index++) {\n shapePainter.drawPathLineTo(polygon[index].x, polygon[index].y / isometricRatio);\n }\n shapePainter.closePath();\n shapePainter.endFillPath();\n }\n };\n return NavMeshRenderer;\n}());\n\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar PathFollower = /** @class */ (function () {\n function PathFollower(configuration) {\n // Attributes used for traveling on the path:\n this._path = [];\n this._speed = 0;\n this._distanceOnSegment = 0;\n this._totalSegmentDistance = 0;\n this._currentSegment = 0;\n this._movementAngle = 0;\n this.configuration = configuration;\n }\n PathFollower.prototype.setSpeed = function (speed) {\n this._speed = speed;\n };\n PathFollower.prototype.getSpeed = function () {\n return this._speed;\n };\n PathFollower.prototype.getMovementAngle = function () {\n return this._movementAngle;\n };\n PathFollower.prototype.movementAngleIsAround = function (degreeAngle, tolerance) {\n return (Math.abs(gdjs.evtTools.common.angleDifference(this._movementAngle, degreeAngle)) <= tolerance);\n };\n PathFollower.prototype.getNodeX = function (index) {\n if (index < this._path.length) {\n return this._path[index][0];\n }\n return 0;\n };\n PathFollower.prototype.getNodeY = function (index) {\n if (index < this._path.length) {\n return this._path[index][1];\n }\n return 0;\n };\n PathFollower.prototype.getNextNodeIndex = function () {\n return Math.min(this._currentSegment + 1, this._path.length - 1);\n };\n PathFollower.prototype.getNodeCount = function () {\n return this._path.length;\n };\n PathFollower.prototype.getNextNodeX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);\n return this._path[nextIndex][0];\n };\n PathFollower.prototype.getNextNodeY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);\n return this._path[nextIndex][1];\n };\n PathFollower.prototype.getPreviousNodeX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var previousIndex = Math.min(this._currentSegment, this._path.length - 1);\n return this._path[previousIndex][0];\n };\n PathFollower.prototype.getPreviousNodeY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var previousIndex = Math.min(this._currentSegment, this._path.length - 1);\n return this._path[previousIndex][1];\n };\n PathFollower.prototype.getDestinationX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n return this._path[this._path.length - 1][0];\n };\n PathFollower.prototype.getDestinationY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n return (this._path[this._path.length - 1][1]);\n };\n /**\n * Return true if the object reached its destination.\n */\n PathFollower.prototype.destinationReached = function () {\n return this._currentSegment >= this._path.length - 1;\n };\n /**\n * Compute and move on the path to the specified destination.\n */\n PathFollower.prototype.setPath = function (path) {\n this._path = path;\n this._enterSegment(0);\n };\n PathFollower.prototype._enterSegment = function (segmentNumber) {\n if (this._path.length === 0) {\n return;\n }\n this._currentSegment = segmentNumber;\n if (this._currentSegment < this._path.length - 1) {\n var pathX = this._path[this._currentSegment + 1][0] -\n this._path[this._currentSegment][0];\n var pathY = this._path[this._currentSegment + 1][1] -\n this._path[this._currentSegment][1];\n this._totalSegmentDistance = Math.sqrt(pathX * pathX + pathY * pathY);\n this._distanceOnSegment = 0;\n this._movementAngle =\n (gdjs.toDegrees(Math.atan2(pathY, pathX)) + 360) % 360;\n }\n else {\n this._speed = 0;\n }\n };\n PathFollower.prototype.isMoving = function () {\n return !(this._path.length === 0 || this.destinationReached());\n };\n PathFollower.prototype.step = function (timeDelta) {\n if (this._path.length === 0 || this.destinationReached()) {\n return;\n }\n // Update the speed of the object\n var previousSpeed = this._speed;\n var maxSpeed = this.configuration._getMaxSpeed();\n if (this._speed !== maxSpeed) {\n this._speed += this.configuration._getAcceleration() * timeDelta;\n if (this._speed > maxSpeed) {\n this._speed = maxSpeed;\n }\n }\n // Update the time on the segment and change segment if needed\n // Use a Verlet integration to be frame rate independent.\n this._distanceOnSegment +=\n ((this._speed + previousSpeed) / 2) * timeDelta;\n var remainingDistanceOnSegment = this._totalSegmentDistance - this._distanceOnSegment;\n if (remainingDistanceOnSegment <= 0 &&\n this._currentSegment < this._path.length) {\n this._enterSegment(this._currentSegment + 1);\n this._distanceOnSegment = -remainingDistanceOnSegment;\n }\n };\n PathFollower.prototype.getX = function () {\n return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][0], this._path[this._currentSegment + 1][0], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][0];\n };\n PathFollower.prototype.getY = function () {\n return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][1], this._path[this._currentSegment + 1][1], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][1];\n };\n return PathFollower;\n}());\n\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar NavMeshPathfindingBehavior = /** @class */ (function () {\n function NavMeshPathfindingBehavior(behavior) {\n // Attributes used for traveling on the path:\n this._pathFound = false;\n this.behavior = behavior;\n this.pathFollower = new PathFollower(behavior);\n this.navMeshRenderer = new NavMeshRenderer();\n }\n /**\n * Return true if the latest call to moveTo succeeded.\n */\n NavMeshPathfindingBehavior.prototype.pathFound = function () {\n return this._pathFound;\n };\n /**\n * Compute and move on the path to the specified destination.\n */\n NavMeshPathfindingBehavior.prototype.moveTo = function (instanceContainer, x, y) {\n var owner = this.behavior.owner;\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n this._pathFound = true;\n this.pathFollower.setPath([[owner.getX(), owner.getY()], [x, y]]);\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n var cellSize = manager.configuration._getCellSize();\n var collisionShape = this.behavior._getCollisionShape();\n var extraBorder = this.behavior._getExtraBorder();\n var radiusSqMax = 0;\n if (collisionShape !== 'Dot at center') {\n var centerX = owner.getCenterXInScene();\n var centerY = owner.getCenterYInScene();\n for (var _i = 0, _a = owner.getHitBoxes(); _i < _a.length; _i++) {\n var hitBox = _a[_i];\n for (var _b = 0, _c = hitBox.vertices; _b < _c.length; _b++) {\n var vertex = _c[_b];\n var deltaX = vertex[0] - centerX;\n // to have the same unit on x and y\n var deltaY = (vertex[1] - centerY) * isometricRatio;\n var radiusSq = deltaX * deltaX + deltaY * deltaY;\n radiusSqMax = Math.max(radiusSq, radiusSqMax);\n }\n }\n }\n // Round to avoid to flicker between 2 NavMesh\n // because of trigonometry rounding errors.\n // Round the padding on cellSize to avoid almost identical NavMesh\n var obstacleCellPadding = Math.max(0, Math.round((Math.sqrt(radiusSqMax) + extraBorder) / cellSize));\n this.navMeshRenderer.setLastUsedObstacleCellPadding(obstacleCellPadding);\n var navMesh = manager.getNavMesh(obstacleCellPadding);\n // TODO avoid the path allocation\n var path = navMesh.findPath({\n x: owner.getX(),\n y: owner.getY() * isometricRatio,\n }, { x: x, y: y * isometricRatio }) || [];\n this._pathFound = path.length > 0;\n this.pathFollower.setPath(path.map(function (_a) {\n var x = _a.x, y = _a.y;\n return [x, y];\n }));\n };\n NavMeshPathfindingBehavior.prototype.doStepPreEvents = function (instanceContainer) {\n if (this.pathFollower.destinationReached()) {\n return;\n }\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n var owner = this.behavior.owner;\n var angleOffset = this.behavior._getAngleOffset();\n var angularMaxSpeed = this.behavior._getMaxSpeed();\n var rotateObject = this.behavior._getRotateObject();\n var timeDelta = owner.getElapsedTime(instanceContainer) / 1000;\n this.pathFollower.step(timeDelta);\n // Position object on the segment and update its angle\n var movementAngle = this.pathFollower.getMovementAngle();\n if (rotateObject &&\n owner.getAngle() !== movementAngle + angleOffset) {\n owner.rotateTowardAngle(movementAngle + angleOffset, angularMaxSpeed, instanceContainer);\n }\n owner.setX(this.pathFollower.getX());\n // In case of isometry, convert coords back in screen.\n owner.setY(this.pathFollower.getY() / isometricRatio);\n };\n return NavMeshPathfindingBehavior;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * NavMeshPathfindingObstacleRuntimeBehavior represents a behavior allowing objects to be\n * considered as a obstacle by objects having Pathfinding Behavior.\n */\nvar NavMeshPathfindingObstacleBehavior = /** @class */ (function () {\n function NavMeshPathfindingObstacleBehavior(instanceContainer, behavior) {\n this._oldX = 0;\n this._oldY = 0;\n this._oldWidth = 0;\n this._oldHeight = 0;\n this._registeredInManager = false;\n this.behavior = behavior;\n this._manager = NavMeshPathfindingObstaclesManager.getManagerOrCreate(instanceContainer, \n // @ts-ignore\n behavior._sharedData);\n //Note that we can't use getX(), getWidth()... of owner here:\n //The owner is not yet fully constructed.\n }\n NavMeshPathfindingObstacleBehavior.prototype.onDestroy = function () {\n if (this._manager && this._registeredInManager) {\n this._manager.removeObstacle(this);\n }\n };\n NavMeshPathfindingObstacleBehavior.prototype.doStepPreEvents = function (instanceContainer) {\n var owner = this.behavior.owner;\n //Make sure the obstacle is or is not in the obstacles manager.\n if (!this.behavior.activated() && this._registeredInManager) {\n this._manager.removeObstacle(this);\n this._registeredInManager = false;\n }\n else {\n if (this.behavior.activated() && !this._registeredInManager) {\n this._manager.addObstacle(this);\n this._registeredInManager = true;\n }\n }\n //Track changes in size or position\n if (this._oldX !== owner.getX() ||\n this._oldY !== owner.getY() ||\n this._oldWidth !== owner.getWidth() ||\n this._oldHeight !== owner.getHeight()) {\n if (this._registeredInManager) {\n this._manager.removeObstacle(this);\n this._manager.addObstacle(this);\n }\n this._oldX = owner.getX();\n this._oldY = owner.getY();\n this._oldWidth = owner.getWidth();\n this._oldHeight = owner.getHeight();\n }\n };\n NavMeshPathfindingObstacleBehavior.prototype.doStepPostEvents = function (instanceContainer) { };\n NavMeshPathfindingObstacleBehavior.prototype.onActivate = function () {\n if (this._registeredInManager) {\n return;\n }\n this._manager.addObstacle(this);\n this._registeredInManager = true;\n };\n NavMeshPathfindingObstacleBehavior.prototype.onDeActivate = function () {\n if (!this._registeredInManager) {\n return;\n }\n this._manager.removeObstacle(this);\n this._registeredInManager = false;\n };\n return NavMeshPathfindingObstacleBehavior;\n}());\n\ngdjs.__NavMeshPathfinding = gdjs.__NavMeshPathfinding || {};\ngdjs.__NavMeshPathfinding.NavMeshPathfindingBehavior = NavMeshPathfindingBehavior;\ngdjs.__NavMeshPathfinding.NavMeshPathfindingObstacleBehavior = NavMeshPathfindingObstacleBehavior;\n", + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ] + } + ], + "parameters": [], + "objectGroups": [] + } + ], + "eventsBasedBehaviors": [ + { + "description": "Move the object to a target in straight lines while avoiding all objects that are flagged as obstacles.", + "fullName": "Navigation mesh pathfinding (experimental)", + "name": "NavMeshPathfindingBehavior", + "objectType": "", + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Initiate and attach properties as objects variables", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::DefineJavaScript" + }, + "parameters": [ + "", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\r\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r\nconst behavior = object.getBehavior(behaviorName);\r\nbehavior.__NavMeshPathfinding = behavior.__NavMeshPathfinding || {};\r\nbehavior.__NavMeshPathfinding.pathfinding = new gdjs.__NavMeshPathfinding.NavMeshPathfindingBehavior(behavior);\r\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPreEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\r\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r\nconst behavior = object.getBehavior(behaviorName);\r\n\r\nbehavior.__NavMeshPathfinding.pathfinding.doStepPreEvents(runtimeScene);\r\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Move the object to a position.", + "fullName": "Move to a position", + "functionType": "Action", + "group": "Movement on the path (navigation mesh)", + "name": "SetDestination", + "sentence": "Move _PARAM0_ to _PARAM2_;_PARAM3_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst destinationX = eventsFunctionContext.getArgument(\"DestinationX\");\nconst destinationY = eventsFunctionContext.getArgument(\"DestinationY\");\n\nbehavior.__NavMeshPathfinding.pathfinding.moveTo(runtimeScene, destinationX, destinationY);\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + }, + { + "description": "Destination X position", + "name": "DestinationX", + "type": "expression" + }, + { + "description": "Destination Y position", + "name": "DestinationY", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the object is moving on a path.", + "fullName": "Is moving", + "functionType": "Condition", + "group": "Movement on the path (navigation mesh)", + "name": "IsMoving", + "sentence": "_PARAM0_ is moving on a path", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.isMoving();\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if a path has been found.", + "fullName": "Path found", + "functionType": "Condition", + "group": "Movement on the path (navigation mesh)", + "name": "PathFound", + "sentence": "A path has been found for _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFound();\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the destination was reached.", + "fullName": "Destination reached", + "functionType": "Condition", + "group": "Movement on the path (navigation mesh)", + "name": "DestinationReached", + "sentence": "_PARAM0_ reached its destination", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.destinationReached();\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Get the number of waypoints on the path.", + "fullName": "Waypoint count", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "NodeCount", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNodeCount();\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return a waypoint X position.", + "fullName": "Waypoint X position", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "NodeX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst nodeIndex = eventsFunctionContext.getArgument(\"NodeIndex\");\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNodeX(nodeIndex);\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + }, + { + "description": "Node index (start at 0)", + "name": "NodeIndex", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Return a waypoint Y position.", + "fullName": "Waypoint Y position", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "NodeY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst nodeIndex = eventsFunctionContext.getArgument(\"NodeIndex\");\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNodeY(nodeIndex);\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + }, + { + "description": "Node index (start at 0)", + "name": "NodeIndex", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Return the index of the next waypoint to reach.", + "fullName": "Index of the next waypoint", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "NextNodeIndex", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNextNodeIndex();\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return the next waypoint X position.", + "fullName": "Next waypoint X position", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "NextNodeX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::NodeX(Object.Behavior::NextNodeIndex())" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return the next waypoint Y position.", + "fullName": "Next waypoint Y position", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "NextNodeY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::NodeY(Object.Behavior::NextNodeIndex())" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return the previous waypoint X position.", + "fullName": "Previous waypoint X position", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "PreviousNodeX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::NodeX(Object.Behavior::NextNodeIndex() - 1)" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return the previous waypoint Y position.", + "fullName": "Previous waypoint Y position", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "PreviousNodeY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::NodeY(Object.Behavior::NextNodeIndex() - 1)" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return the destination X position.", + "fullName": "Destination X position", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "DestinationX", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::NodeX(Object.Behavior::NodeCount() - 1)" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return the destination Y position.", + "fullName": "Destination Y position", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "DestinationY", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::NodeY(Object.Behavior::NodeCount() - 1)" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return the angle of movement of an object on its path.", + "fullName": "Angle of movement on its path", + "functionType": "Expression", + "group": "Movement on the path (navigation mesh)", + "name": "MovementAngle", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst angle = eventsFunctionContext.getArgument(\"Angle\");\nconst tolerance = eventsFunctionContext.getArgument(\"Tolerance\");\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getMovementAngle(angle, tolerance);\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Compare the angle of movement of an object on its path.", + "fullName": "Angle of movement on its path", + "functionType": "Condition", + "group": "Movement on the path (navigation mesh)", + "name": "MovementAngleIsAround", + "sentence": "Angle of movement of _PARAM0_ is _PARAM2_ (tolerance: _PARAM3_ degrees)", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BuiltinCommonInstructions::CompareNumbers" + }, + "parameters": [ + "AngleDifference(GetArgumentAsNumber(\"Angle\"), Object.Behavior::MovementAngle())", + "<", + "GetArgumentAsNumber(\"Tolerance\")" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + }, + { + "description": "Angle (in degrees)", + "name": "Angle", + "type": "expression" + }, + { + "description": "Tolerance (in degrees)", + "name": "Tolerance", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "the number of waypoints on the path.", + "fullName": "Speed on the path", + "functionType": "ExpressionAndCondition", + "group": "Movement on the path (navigation mesh)", + "name": "Speed", + "sentence": "the speed on the path", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getSpeed();\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "Draw navigation mesh", + "functionType": "Action", + "group": "Debug (navigation mesh)", + "name": "DrawNavMesh", + "sentence": "Draw the navigation mesh used for _PARAM0_ on _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst shapePainters = eventsFunctionContext.getObjects(\"ShapePainter\");\n\nfor (const shapePainter of shapePainters) {\n behavior.__NavMeshPathfinding.pathfinding.navMeshRenderer.render(runtimeScene, shapePainter);\n}\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + }, + { + "description": "Shape painter", + "name": "ShapePainter", + "supplementaryInformation": "PrimitiveDrawing::Drawer", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "the acceleration of the object.", + "fullName": "Acceleration", + "functionType": "ExpressionAndCondition", + "group": "Pathfinding configuration (navigation mesh)", + "name": "Acceleration", + "sentence": "the acceleration", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyAcceleration()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "Acceleration", + "name": "SetAcceleration", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetPropertyAcceleration" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the max. speed of the object.", + "fullName": "Max. speed", + "functionType": "ExpressionAndCondition", + "group": "Pathfinding configuration (navigation mesh)", + "name": "MaxSpeed", + "sentence": "the max. speed", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyMaxSpeed()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "MaxSpeed", + "name": "SetMaxSpeed", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetPropertyMaxSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the rotate speed of the object.", + "fullName": "Rotate speed", + "functionType": "ExpressionAndCondition", + "group": "Pathfinding configuration (navigation mesh)", + "name": "AngularMaxSpeed", + "sentence": "the rotate speed", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyAngularMaxSpeed()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "AngularMaxSpeed", + "name": "SetAngularMaxSpeed", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetPropertyAngularMaxSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the angle offset of the object.", + "fullName": "Angle offset", + "functionType": "ExpressionAndCondition", + "group": "Pathfinding configuration (navigation mesh)", + "name": "AngleOffset", + "sentence": "the angle offset", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyAngleOffset()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "AngleOffset", + "name": "SetAngleOffset", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetPropertyAngleOffset" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the extra border size of the object.", + "fullName": "Extra border size", + "functionType": "ExpressionAndCondition", + "group": "Pathfinding configuration (navigation mesh)", + "name": "ExtraBorder", + "sentence": "the extra border size", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyExtraBorder()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "ExtraBorder", + "name": "SetExtraBorder", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetPropertyExtraBorder" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the collision shape of the object.", + "fullName": "Collision shape", + "functionType": "ExpressionAndCondition", + "group": "Pathfinding configuration (navigation mesh)", + "name": "CollisionShape", + "sentence": "the collision shape", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnString" + }, + "parameters": [ + "Object.Behavior::PropertyCollisionShape()" + ] + } + ] + } + ], + "expressionType": { + "supplementaryInformation": "[\"Bounding disk\",\"Dot at center\"]", + "type": "stringWithSelector" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "CollisionShape", + "name": "SetCollisionShape", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetPropertyCollisionShape" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsString(\"Value\")" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the object should rotate when following its path.", + "fullName": "Rotate object", + "functionType": "Condition", + "group": "Pathfinding configuration (navigation mesh)", + "name": "RotateObject", + "sentence": "_PARAM0_ rotate when following its path", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::PropertyRotateObject" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "getterName": "RotateObject", + "group": "Pathfinding configuration (navigation mesh)", + "name": "SetRotateObject", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"Value\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetPropertyRotateObject" + }, + "parameters": [ + "Object", + "Behavior", + "yes" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"Value\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetPropertyRotateObject" + }, + "parameters": [ + "Object", + "Behavior", + "no" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + }, + { + "description": "Rotate object", + "name": "Value", + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "propertyDescriptors": [ + { + "value": "400", + "type": "Number", + "label": "Acceleration", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "Acceleration" + }, + { + "value": "200", + "type": "Number", + "label": "Max. speed", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "MaxSpeed" + }, + { + "value": "180", + "type": "Number", + "label": "Rotate speed", + "description": "", + "group": "Rotation", + "extraInformation": [], + "hidden": false, + "name": "AngularMaxSpeed" + }, + { + "value": "", + "type": "Boolean", + "label": "Rotate object", + "description": "", + "group": "Rotation", + "extraInformation": [], + "hidden": false, + "name": "RotateObject" + }, + { + "value": "0", + "type": "Number", + "label": "Angle offset", + "description": "", + "group": "Rotation", + "extraInformation": [], + "hidden": false, + "name": "AngleOffset" + }, + { + "value": "Bounding disk", + "type": "Choice", + "label": "Collision shape", + "description": "", + "group": "Collision", + "extraInformation": [ + "Bounding disk", + "Dot at center" + ], + "hidden": false, + "name": "CollisionShape" + }, + { + "value": "0", + "type": "Number", + "label": "Extra border size", + "description": "", + "group": "Collision", + "extraInformation": [], + "hidden": false, + "name": "ExtraBorder" + } + ], + "sharedPropertyDescriptors": [] + }, + { + "description": "Flag the object as being an obstacle for pathfinding.", + "fullName": "Obstacle for pathfinding (navigation mesh, experimental)", + "name": "NavMeshPathfindingObstacleBehavior", + "objectType": "", + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Initiate and attach properties as objects variables", + "comment2": "" }, { "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "inverted": false, - "value": "CollisionPoint" - }, - "parameters": [ - "Floor", - "Mindy.PointX(\"footcollis\")", - "Mindy.PointY(\"footcollis\")" - ], - "subInstructions": [] - }, - { - "type": { - "inverted": false, - "value": "BuiltinCommonInstructions::Once" - }, - "parameters": [], - "subInstructions": [] - } - ], + "conditions": [], "actions": [ { "type": { - "inverted": false, - "value": "StopSoundCanal" + "value": "NavMeshPathfinding::DefineJavaScript" }, "parameters": [ "", - "1" - ], - "subInstructions": [] + "" + ] } - ], - "events": [] + ] + }, + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\r\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r\nconst behavior = object.getBehavior(behaviorName);\r\nbehavior.__NavMeshPathfinding = behavior.__NavMeshPathfinding || {};\r\nbehavior.__NavMeshPathfinding.obstacle =\r\n new gdjs.__NavMeshPathfinding.NavMeshPathfindingObstacleBehavior(\r\n runtimeScene, behavior);\r\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ + ], + "parameters": [ { - "type": { - "inverted": true, - "value": "TopDownMovementBehavior::IsMoving" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ], - "subInstructions": [] + "description": "Object", + "name": "Object", + "type": "object" }, { - "type": { - "inverted": false, - "value": "BuiltinCommonInstructions::Once" - }, - "parameters": [], - "subInstructions": [] + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" } ], - "actions": [ + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPreEvents", + "sentence": "", + "events": [ { - "type": { - "inverted": false, - "value": "StopSoundCanal" - }, - "parameters": [ - "", - "1" - ], - "subInstructions": [] + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\r\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r\nconst behavior = object.getBehavior(behaviorName);\r\n\r\nbehavior.__NavMeshPathfinding.obstacle.doStepPreEvents(runtimeScene);", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" } ], - "events": [] + "objectGroups": [] } ], - "parameters": [] - } - ], - "layers": [ - { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "followBaseLayerCamera": false, - "isLightingLayer": false, - "name": "", - "visibility": true, - "cameras": [ + "propertyDescriptors": [], + "sharedPropertyDescriptors": [ { - "defaultSize": true, - "defaultViewport": true, - "height": 0, - "viewportBottom": 1, - "viewportLeft": 0, - "viewportRight": 1, - "viewportTop": 0, - "width": 0 + "value": "Top-Down", + "type": "Choice", + "label": "Viewpoint", + "description": "", + "group": "", + "extraInformation": [ + "Top-Down", + "Isometry 2:1 (26.565°)", + "True Isometry (30°)" + ], + "hidden": false, + "name": "Viewpoint" + }, + { + "value": "10", + "type": "Number", + "label": "Cell size", + "description": "Cell size for obstacle collision mask rasterization.", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "CellSize" + }, + { + "value": "", + "type": "Number", + "label": "Area left bound", + "description": "The left bound of the area where objects can go.", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "AreaLeftBound" + }, + { + "value": "", + "type": "Number", + "label": "Area top bound", + "description": "The top bound of the area where objects can go.", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "AreaTopBound" + }, + { + "value": "", + "type": "Number", + "label": "Area right bound", + "description": "The right bound of the area where objects can go (default to the game resolution).", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "AreaRightBound" + }, + { + "value": "", + "type": "Number", + "label": "Area bottom bound", + "description": "The bottom bound of the area where objects can go (default to the game resolution).", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "AreaBottomBound" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "IsometricRatio" } - ], - "effects": [] - }, - { - "ambientLightColorB": 167801455, - "ambientLightColorG": 6025424, - "ambientLightColorR": 7792048, - "followBaseLayerCamera": false, - "isLightingLayer": false, - "name": "Level1", - "visibility": true, - "cameras": [], - "effects": [] - }, - { - "ambientLightColorB": 167801455, - "ambientLightColorG": 6025424, - "ambientLightColorR": 7792048, - "followBaseLayerCamera": false, - "isLightingLayer": false, - "name": "Level2", - "visibility": true, - "cameras": [], - "effects": [] - }, - { - "ambientLightColorB": 200, - "ambientLightColorG": 200, - "ambientLightColorR": 200, - "followBaseLayerCamera": false, - "isLightingLayer": false, - "name": "UI", - "visibility": true, - "cameras": [], - "effects": [] - }, - { - "ambientLightColorB": 9845048, - "ambientLightColorG": 6025424, - "ambientLightColorR": 10009488, - "followBaseLayerCamera": false, - "isLightingLayer": false, - "name": "Obstacle", - "visibility": true, - "cameras": [], - "effects": [] + ] } ], - "behaviorsSharedData": [ - { - "name": "NavMeshPathfindingObstacleBehavior", - "type": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", - "areaTopBound": -150, - "cellSize": 20, - "viewpoint": "Isometry 2:1 (26.565°)", - "areaBottomBound": 1700, - "areaLeftBound": -250, - "areaRightBound": 1900 - }, - { - "name": "TopDownMovement", - "type": "TopDownMovementBehavior::TopDownMovementBehavior" - }, - { - "name": "YSort", - "type": "YSort::YSort" - } - ] - } - ], - "externalEvents": [ - { - "associatedLayout": "Level 1", - "lastChangeTimeStamp": 0, - "name": "Base_Event", - "events": [] - } - ], - "eventsFunctionsExtensions": [ + "eventsBasedObjects": [] + }, { "author": "Gustavo Marciano", "category": "", - "description": "Set the depth (Z-order) of the instance to the value of its Y position in the scene, creating an illusion of depth. The origin point of the object is used to determine the Z-order.\n\nUseful for isometric games, 2D games with a \"Top-Down\" view, RPG...", "extensionNamespace": "", "fullName": "YSort", "helpPath": "", @@ -26738,6 +28310,7 @@ "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/sort-ascending.svg", "shortDescription": "Create an illusion of depth by setting the Z-order based on the Y position of the object. Useful for isometric games, 2D games with a \"Top-Down\" view, RPG...", "version": "0.0.1", + "description": "Set the depth (Z-order) of the instance to the value of its Y position in the scene, creating an illusion of depth. The origin point of the object is used to determine the Z-order.\n\nUseful for isometric games, 2D games with a \"Top-Down\" view, RPG...", "tags": [ "z-order", "y-sort", @@ -26757,12 +28330,9 @@ "objectType": "", "eventsFunctions": [ { - "description": "", "fullName": "", "functionType": "Action", - "group": "", "name": "doStepPostEvents", - "private": false, "sentence": "", "events": [ { @@ -26771,38 +28341,26 @@ "actions": [ { "type": { - "inverted": false, "value": "ChangePlan" }, "parameters": [ "Object", "=", "Object.Y()" - ], - "subInstructions": [] + ] } - ], - "events": [] + ] } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "Object", - "longDescription": "", "name": "Object", - "optional": false, - "supplementaryInformation": "", "type": "object" }, { - "codeOnly": false, - "defaultValue": "", "description": "Behavior", - "longDescription": "", "name": "Behavior", - "optional": false, "supplementaryInformation": "YSort::YSort", "type": "behavior" } @@ -26810,22 +28368,28 @@ "objectGroups": [] } ], - "propertyDescriptors": [] + "propertyDescriptors": [], + "sharedPropertyDescriptors": [] } - ] + ], + "eventsBasedObjects": [] }, { "author": "Bouh", - "category": "", - "description": "Add support for gamepads (or other controllers) to your game, giving access to information such as button presses, axis positions, axis force, trigger pressure, deadzone for each gamepad, etc...\n\nUp to 4 gamepads can be connected: for each condition or expression, you'll have to enter the index of the gamepad to read. This is 1, 2,3 or 4.", + "category": "Input", "extensionNamespace": "", "fullName": "Gamepads (controllers)", - "helpPath": "", - "iconUrl": "", + "helpPath": "/all-features/gamepad", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWdhbWVwYWQtdmFyaWFudC1vdXRsaW5lIiB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHBhdGggZD0iTTYsOUg4VjExSDEwVjEzSDhWMTVINlYxM0g0VjExSDZWOU0xOC41LDlBMS41LDEuNSAwIDAsMSAyMCwxMC41QTEuNSwxLjUgMCAwLDEgMTguNSwxMkExLjUsMS41IDAgMCwxIDE3LDEwLjVBMS41LDEuNSAwIDAsMSAxOC41LDlNMTUuNSwxMkExLjUsMS41IDAgMCwxIDE3LDEzLjVBMS41LDEuNSAwIDAsMSAxNS41LDE1QTEuNSwxLjUgMCAwLDEgMTQsMTMuNUExLjUsMS41IDAgMCwxIDE1LjUsMTJNMTcsNUE3LDcgMCAwLDEgMjQsMTJBNyw3IDAgMCwxIDE3LDE5QzE1LjA0LDE5IDEzLjI3LDE4LjIgMTIsMTYuOUMxMC43MywxOC4yIDguOTYsMTkgNywxOUE3LDcgMCAwLDEgMCwxMkE3LDcgMCAwLDEgNyw1SDE3TTcsN0E1LDUgMCAwLDAgMiwxMkE1LDUgMCAwLDAgNywxN0M4LjY0LDE3IDEwLjA5LDE2LjIxIDExLDE1SDEzQzEzLjkxLDE2LjIxIDE1LjM2LDE3IDE3LDE3QTUsNSAwIDAsMCAyMiwxMkE1LDUgMCAwLDAgMTcsN0g3WiIgLz48L3N2Zz4=", "name": "Gamepads", - "previewIconUrl": "", + "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/gamepad-variant-outline.svg", "shortDescription": "Add support for gamepads (or other controllers) to your game, giving access to information such as button presses, axis positions, trigger pressure, etc...", - "version": "0.0.6", + "version": "0.3.0", + "description": "Add support for gamepads (or other controllers) to your game, giving access to information such as button presses, axis positions, axis force, trigger pressure, deadzone for each gamepad, etc...\n\nUp to 4 gamepads can be connected. For each condition or expression, you'll have to enter the index of the gamepad to read. This is 1, 2, 3 or 4.", + "origin": { + "identifier": "Gamepads", + "name": "gdevelop-extension-store" + }, "tags": [ "controllers", "gamepads", @@ -26834,46 +28398,40 @@ "xbox", "ps4" ], - "authorIds": [], + "authorIds": [ + "2OwwM8ToR9dx9RJ2sAKTcrLmCB92" + ], "dependencies": [], "eventsFunctions": [ { "description": "Get the value of the pressure on a gamepad trigger.", "fullName": "Pressure on a gamepad trigger", "functionType": "Expression", - "group": "", "name": "TriggerPressure", - "private": false, "sentence": "Player _PARAM1_ push axis _PARAM2_ to _PARAM3_", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst trigger = eventsFunctionContext.getArgument(\"trigger\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in expression: \"Pressure on a gamepad trigger\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (trigger != \"LT\" && trigger != \"RT\" && trigger != \"L2\" && trigger != \"R2\") {\r\n console.error('Parameter trigger is not valid in expression: \"Pressure on a gamepad trigger\"');\r\n return;\r\n}\r\n\r\nvar gamepad = gamepads[playerId];\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\n\r\nswitch (trigger) {\r\n case 'LT':\r\n case 'L2':\r\n eventsFunctionContext.returnValue = gamepad.buttons[6].value;\r\n break;\r\n\r\n case 'RT':\r\n case 'R2':\r\n eventsFunctionContext.returnValue = gamepad.buttons[7].value;\r\n break;\r\n\r\n default:\r\n eventsFunctionContext.returnValue = -1;\r\n break;\r\n}", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst trigger = eventsFunctionContext.getArgument(\"trigger\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in expression: \"Pressure on a gamepad trigger\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (trigger != \"LT\" && trigger != \"RT\" && trigger != \"L2\" && trigger != \"R2\") {\r\n console.error('Parameter trigger is not valid in expression: \"Pressure on a gamepad trigger\"');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nswitch (trigger) {\r\n case 'LT':\r\n case 'L2':\r\n eventsFunctionContext.returnValue = gamepad.buttons[6].value;\r\n break;\r\n\r\n case 'RT':\r\n case 'R2':\r\n eventsFunctionContext.returnValue = gamepad.buttons[7].value;\r\n break;\r\n\r\n default:\r\n eventsFunctionContext.returnValue = -1;\r\n break;\r\n}", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", - "description": "Trigger: \"LT\", \"RT\", \"L2\", \"R2\"", - "longDescription": "", + "description": "Trigger button", "name": "trigger", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"LT\",\"RT\",\"L2\",\"R2\"]", + "type": "stringWithSelector" } ], "objectGroups": [] @@ -26882,39 +28440,31 @@ "description": "Get the force value of a gamepad stick.\n0 is stick on default position, 1 is at the maximum.", "fullName": "Value of a stick force", "functionType": "Expression", - "group": "", "name": "StickForce", - "private": false, "sentence": "Player _PARAM1_ push axis _PARAM2_ to _PARAM3_", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\n\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier is not valid in expression: \"Value of a stick force\"');\r\n return;\r\n}\r\n\r\nif (stick !== \"LEFT\" && stick !== \"RIGHT\") {\r\n console.error('Parameter stick is not valid in expression: \"Value of a stick force\"');\r\n return;\r\n}\r\n\r\nvar gamepad = gamepads[playerId];\r\nif (gamepad == null) return;\r\n\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n eventsFunctionContext.returnValue = gdjs.evtTools.common.clamp(Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId)) + Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId)), 0, 1);\r\n break;\r\n\r\n case 'RIGHT':\r\n eventsFunctionContext.returnValue = gdjs.evtTools.common.clamp(Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId)) + Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId)), 0, 1);\r\n break;\r\n\r\n default:\r\n eventsFunctionContext.returnValue = -1;\r\n break;\r\n}", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\n\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier is not valid in expression: \"Value of a stick force\"');\r\n return;\r\n}\r\n\r\nif (stick !== \"LEFT\" && stick !== \"RIGHT\") {\r\n console.error('Parameter stick is not valid in expression: \"Value of a stick force\"');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n eventsFunctionContext.returnValue = gdjs.evtTools.common.clamp(Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId)) + Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId)), 0, 1);\r\n break;\r\n\r\n case 'RIGHT':\r\n eventsFunctionContext.returnValue = gdjs.evtTools.common.clamp(Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId)) + Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId)), 0, 1);\r\n break;\r\n\r\n default:\r\n eventsFunctionContext.returnValue = -1;\r\n break;\r\n}", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", - "description": "Stick: \"LEFT\" or \"RIGHT\"", - "longDescription": "", + "description": "Stick: \"Left\" or \"Right\"", "name": "stick", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"Left\",\"Right\"]", + "type": "stringWithSelector" } ], "objectGroups": [] @@ -26923,39 +28473,31 @@ "description": "Get the rotation value of a gamepad stick.\nIf the deadzone value is high, the angle value is rounded to main axes, left, left, up, down.\nAn zero deadzone value give a total freedom on the angle value.", "fullName": "Value of a stick rotation", "functionType": "Expression", - "group": "", "name": "StickRotationValue", - "private": false, "sentence": "Player _PARAM1_ push axis _PARAM2_ to _PARAM3_", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\n\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier is not valid in expression: \"Value of a stick rotation\"');\r\n return;\r\n}\r\nif (stick !== \"LEFT\" && stick !== \"RIGHT\") {\r\n console.error('Parameter stick is not valid in expression: \"Value of a stick rotation\"');\r\n return;\r\n}\r\nvar gamepad = gamepads[playerId];\r\nif (gamepad == null) return;\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n eventsFunctionContext.returnValue = gdjs._extensionController.axisToAngle(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId), gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId));\r\n break;\r\n\r\n case 'RIGHT':\r\n eventsFunctionContext.returnValue = gdjs._extensionController.axisToAngle(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId), gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId));\r\n break;\r\n\r\n default:\r\n eventsFunctionContext.returnValue = -1;\r\n break;\r\n}", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\n\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier is not valid in expression: \"Value of a stick rotation\"');\r\n return;\r\n}\r\nif (stick !== \"LEFT\" && stick !== \"RIGHT\") {\r\n console.error('Parameter stick is not valid in expression: \"Value of a stick rotation\"');\r\n return;\r\n}\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n eventsFunctionContext.returnValue = gdjs._extensionController.axisToAngle(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId), gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId));\r\n break;\r\n\r\n case 'RIGHT':\r\n eventsFunctionContext.returnValue = gdjs._extensionController.axisToAngle(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId), gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId));\r\n break;\r\n\r\n default:\r\n eventsFunctionContext.returnValue = -1;\r\n break;\r\n}", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", - "description": "Stick: \"LEFT\" or \"RIGHT\"", - "longDescription": "", + "description": "Stick: \"Left\" or \"Right\"", "name": "stick", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"Left\",\"Right\"]", + "type": "stringWithSelector" } ], "objectGroups": [] @@ -26964,101 +28506,76 @@ "description": "Get the value of axis of a gamepad stick.", "fullName": "Value of a gamepad axis", "functionType": "Expression", - "group": "", "name": "AxisValue", - "private": false, "sentence": "Player _PARAM1_ push axis _PARAM2_ to _PARAM3_", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\nconst direction = eventsFunctionContext.getArgument(\"direction\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier is not valid in expression: \"Value of a gamepad axis\"');\r\n return;\r\n}\r\nif (stick != \"LEFT\" && stick != \"RIGHT\") {\r\n console.error('Parameter stick is not valid in expression: \"Value of a gamepad axis\"');\r\n return;\r\n}\r\nif (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\" && direction != \"HORIZONTAL\" && direction != \"VERTICAL\" ) {\r\n console.error('Parameter direction is not valid in expression: \"Value of a gamepad axis\"');\r\n return;\r\n}\r\nvar gamepad = gamepads[playerId];\r\nif (gamepad == null) return;\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r\n }\r\n break;\r\n\r\n case \"HORIZONTAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r\n break;\r\n\r\n case \"VERTICAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r\n break;\r\n\r\n default:\r\n break;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r\n }\r\n break;\r\n\r\n case \"HORIZONTAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r\n break;\r\n\r\n case \"VERTICAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r\n break;\r\n\r\n default:\r\n break;\r\n }\r\n break;\r\n\r\n default:\r\n break;\r\n}", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\nconst direction = eventsFunctionContext.getArgument(\"direction\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier is not valid in expression: \"Value of a gamepad axis\"');\r\n return;\r\n}\r\nif (stick != \"LEFT\" && stick != \"RIGHT\") {\r\n console.error('Parameter stick is not valid in expression: \"Value of a gamepad axis\"');\r\n return;\r\n}\r\nif (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\" && direction != \"HORIZONTAL\" && direction != \"VERTICAL\") {\r\n console.error('Parameter direction is not valid in expression: \"Value of a gamepad axis\"');\r\n return;\r\n}\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nlet parameterError = false;\r\nswitch (stick) {\r\n case 'LEFT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r\n }\r\n break;\r\n\r\n case \"HORIZONTAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r\n break;\r\n\r\n case \"VERTICAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r\n break;\r\n\r\n default:\r\n break;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r\n }\r\n break;\r\n\r\n case \"HORIZONTAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r\n break;\r\n\r\n case \"VERTICAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r\n break;\r\n\r\n default:\r\n break;\r\n }\r\n break;\r\n\r\n default:\r\n break;\r\n}\r\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", - "description": "Stick: \"LEFT\" or \"RIGHT\"", - "longDescription": "", + "description": "Stick: \"Left\" or \"Right\"", "name": "stick", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"Left\",\"Right\"]", + "type": "stringWithSelector" }, { - "codeOnly": false, - "defaultValue": "", - "description": "Direction: \"UP\", \"DOWN\", \"LEFT\", \"RIGHT\", \"HORIZONTAL\" or \"VERTICAL\"", - "longDescription": "", + "description": "Direction", "name": "direction", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"Up\",\"Down\",\"Left\",\"Right\",\"Horizontal\",\"Vertical\"]", + "type": "stringWithSelector" } ], "objectGroups": [] }, { - "description": "Test if a button is released on a gamepad. Buttons can be:\n* Xbox: \"A\", \"B\", \"X\", \"Y\", \"LB\", \"RB\", \"LT\", \"RT\", \"BACK\", \"START\",\n* PS4: \"CROSS\", \"SQUARE\", \"CIRCLE\", \"TRIANGLE\", \"L1\", \"L2\", \"R1\", \"R2\", \"SHARE\", \"OPTIONS\", \"PS_BUTTON\", \"CLICK_TOUCHPAD\",\n* Other: \"UP\", \"DOWN\", \"LEFT\", \"RIGHT\", \"CLICK_STICK_LEFT\", \"CLICK_STICK_RIGHT\"", + "description": "Test if a button is released on a gamepad. Buttons can be:\n* Xbox: \"A\", \"B\", \"X\", \"Y\", \"LB\", \"RB\", \"LT\", \"RT\", \"BACK\", \"START\",\n* PS4: \"CROSS\", \"SQUARE\", \"CIRCLE\", \"TRIANGLE\", \"L1\", \"L2\", \"R1\", \"R2\", \"SHARE\", \"OPTIONS\", \"PS_BUTTON\", \"CLICK_TOUCHPAD\",\n* Other: \"UP\", \"DOWN\", \"LEFT\", \"RIGHT\", \"CLICK_STICK_LEFT\", \"CLICK_STICK_RIGHT\".", "fullName": "Gamepad button released", "functionType": "Condition", - "group": "", "name": "C_Button_released", - "private": false, "sentence": "Button _PARAM2_ of gamepad _PARAM1_ is released", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst button = eventsFunctionContext.getArgument(\"button\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Gamepad button released\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (button === \"\") {\r\n console.error('Parameter button is not valid in condition: \"Gamepad button released\"');\r\n return;\r\n}\r\n\r\nvar gamepad = gamepads[playerId];\r\nif (gamepad == null) return;\r\n\r\nvar buttonId;\r\n\r\nswitch (button) {\r\n case 'A':\r\n case 'CROSS':\r\n buttonId = 0;\r\n break;\r\n case 'B':\r\n case 'CIRCLE':\r\n buttonId = 1;\r\n break;\r\n case 'X':\r\n case 'SQUARE':\r\n buttonId = 2;\r\n break;\r\n case 'Y':\r\n case 'TRIANGLE':\r\n buttonId = 3;\r\n break;\r\n case 'LB':\r\n case 'L1':\r\n buttonId = 4;\r\n break;\r\n case 'RB':\r\n case 'R1':\r\n buttonId = 5;\r\n break;\r\n case 'LT':\r\n case 'L2':\r\n buttonId = 6;\r\n break;\r\n case 'RT':\r\n case 'R2':\r\n buttonId = 7;\r\n break;\r\n\r\n case 'UP':\r\n buttonId = 12;\r\n break;\r\n case 'DOWN':\r\n buttonId = 13;\r\n break;\r\n case 'LEFT':\r\n buttonId = 14;\r\n break;\r\n case 'RIGHT':\r\n buttonId = 15;\r\n break;\r\n\r\n case 'BACK':\r\n case 'SHARE':\r\n buttonId = 8;\r\n break;\r\n case 'START':\r\n case 'OPTIONS':\r\n buttonId = 9;\r\n break;\r\n\r\n case 'CLICK_STICK_LEFT':\r\n buttonId = 10;\r\n break;\r\n case 'CLICK_STICK_RIGHT':\r\n buttonId = 11;\r\n break;\r\n\r\n //PS4\r\n case 'PS_BUTTON':\r\n buttonId = 16;\r\n break;\r\n case 'CLICK_TOUCHPAD':\r\n buttonId = 17;\r\n break;\r\n\r\n default:\r\n break;\r\n}\r\n\r\nif (buttonId === undefined) {\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n//Define default value on pressed button or use previous value\r\ngdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] || { pressed: false };\r\n\r\n//Get state of button at previous frame\r\nvar previousStateButton = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed;\r\n\r\n//When previousStateButton is true and actual button state is not pressed\r\n//Player have release the button\r\nif (previousStateButton === true && gamepad.buttons[buttonId].pressed === false) {\r\n // Save the last button used for the player \r\n gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\n gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = true;\r\n eventsFunctionContext.returnValue = true;\r\n\r\n} else {\r\n gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = false;\r\n eventsFunctionContext.returnValue = false;\r\n}\r\n", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst button = eventsFunctionContext.getArgument(\"button\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Gamepad button released\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (button === \"\") {\r\n console.error('Parameter button is not valid in condition: \"Gamepad button released\"');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nlet buttonId;\r\n\r\nswitch (button) {\r\n case 'A':\r\n case 'CROSS':\r\n buttonId = 0;\r\n break;\r\n case 'B':\r\n case 'CIRCLE':\r\n buttonId = 1;\r\n break;\r\n case 'X':\r\n case 'SQUARE':\r\n buttonId = 2;\r\n break;\r\n case 'Y':\r\n case 'TRIANGLE':\r\n buttonId = 3;\r\n break;\r\n case 'LB':\r\n case 'L1':\r\n buttonId = 4;\r\n break;\r\n case 'RB':\r\n case 'R1':\r\n buttonId = 5;\r\n break;\r\n case 'LT':\r\n case 'L2':\r\n buttonId = 6;\r\n break;\r\n case 'RT':\r\n case 'R2':\r\n buttonId = 7;\r\n break;\r\n\r\n case 'UP':\r\n buttonId = 12;\r\n break;\r\n case 'DOWN':\r\n buttonId = 13;\r\n break;\r\n case 'LEFT':\r\n buttonId = 14;\r\n break;\r\n case 'RIGHT':\r\n buttonId = 15;\r\n break;\r\n\r\n case 'BACK':\r\n case 'SHARE':\r\n buttonId = 8;\r\n break;\r\n case 'START':\r\n case 'OPTIONS':\r\n buttonId = 9;\r\n break;\r\n\r\n case 'CLICK_STICK_LEFT':\r\n buttonId = 10;\r\n break;\r\n case 'CLICK_STICK_RIGHT':\r\n buttonId = 11;\r\n break;\r\n\r\n //PS4\r\n case 'PS_BUTTON':\r\n buttonId = 16;\r\n break;\r\n case 'CLICK_TOUCHPAD':\r\n buttonId = 17;\r\n break;\r\n\r\n default:\r\n console.error('The button: ' + button + ' in condition: \"Gamepad button released\" is not valid.');\r\n break;\r\n}\r\n\r\nif (buttonId === undefined) {\r\n console.error('There is no buttons valid in condition: \"Gamepad button released\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\nif (gamepad.buttons == null || gamepad.buttons[buttonId] == null) {\r\n console.error('Buttons on the gamepad are not accessible in condition: \"Gamepad button released\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n//Define default value on pressed button or use previous value\r\ngdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] || { pressed: false };\r\n\r\n//Get state of button at previous frame\r\nconst previousStateButton = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed;\r\n\r\n//When previousStateButton is true and actual button state is not pressed\r\n//Player have release the button\r\nif (previousStateButton === true && gamepad.buttons[buttonId].pressed === false) {\r\n // Save the last button used for the player \r\n gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\n gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = true;\r\n eventsFunctionContext.returnValue = true;\r\n\r\n} else {\r\n gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = false;\r\n eventsFunctionContext.returnValue = false;\r\n}\r\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", - "description": "The name of the button.", - "longDescription": "", + "description": "Name of the button", "name": "button", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"A\",\"Cross\",\"B\",\"Circle\",\"X\",\"Square\",\"Y\",\"Triangle\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"Up\",\"Down\",\"Left\",\"Right\",\"Back\",\"Share\",\"Start\",\"Options\",\"Click_Stick_Left\",\"Click_Stick_Right\",\"PS_Button\",\"Click_Touchpad\"]", + "type": "stringWithSelector" } ], "objectGroups": [] }, { - "description": "Return the index of the last pressed button of a gamepad", + "description": "Return the index of the last pressed button of a gamepad.", "fullName": "Last pressed button (id)", "functionType": "Expression", - "group": "", "name": "LastButtonID", - "private": false, "sentence": "", "events": [ { @@ -27066,18 +28583,16 @@ "inlineCode": "//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\n//Player id is not valid\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in expression: \"Last pressed button (id)\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\n\r\n//Return the last button used by the player\r\neventsFunctionContext.returnValue = gdjs._extensionController.players[playerId].lastButtonUsed;", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" } ], @@ -27087,111 +28602,85 @@ "description": "Check if any button is pressed on a gamepad.", "fullName": "Any gamepad button pressed", "functionType": "Condition", - "group": "", "name": "C_Any_Button_pressed", - "private": false, "sentence": "Any button of gamepad _PARAM1_ is pressed", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Any gamepad button pressed\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nvar gamepad = gamepads[playerId];\r\nif (gamepad == null) return;\r\n\r\nvar buttonId;\r\nfor (var i = 0; i < gamepad.buttons.length; i++) { //For each buttons\r\n if (gamepad.buttons[i].pressed) { //One of them is pressed\r\n buttonId = i; //Save the button pressed\r\n break;\r\n }\r\n}\r\n\r\nif (buttonId === undefined) {\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n//When a button is pressed, save the button in lastButtonUsed for each players\r\nif (gamepad.buttons[buttonId].pressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\neventsFunctionContext.returnValue = gamepad.buttons[buttonId].pressed;\r\n\r\n\r\n", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Any gamepad button pressed\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nlet buttonId;\r\nfor (let i = 0; i < gamepad.buttons.length; i++) { //For each buttons\r\n if (gamepad.buttons[i].pressed) { //One of them is pressed\r\n buttonId = i; //Save the button pressed\r\n break;\r\n }\r\n}\r\n\r\nif (buttonId === undefined) {\r\n // No buttons are pressed.\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\nif (gamepad.buttons == null || gamepad.buttons[buttonId] == null) {\r\n console.error('Buttons on the gamepad are not accessible in condition: \"Any gamepad button pressed\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n//When a button is pressed, save the button in lastButtonUsed for each players\r\nif (gamepad.buttons[buttonId].pressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\neventsFunctionContext.returnValue = gamepad.buttons[buttonId].pressed;\r\n\r\n\r\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" } ], "objectGroups": [] }, { - "description": "Return the last button pressed. \nButtons for Xbox and PS4 can be:\n* Xbox: \"A\", \"B\", \"X\", \"Y\", \"LB\", \"RB\", \"LT\", \"RT\", \"BACK\", \"START\",\n* PS4: \"CROSS\", \"SQUARE\", \"CIRCLE\", \"TRIANGLE\", \"L1\", \"L2\", \"R1\", \"R2\", \"SHARE\", \"OPTIONS\", \"PS_BUTTON\", \"CLICK_TOUCHPAD\",\n* Both: \"UP\", \"DOWN\", \"LEFT\", \"RIGHT\", \"CLICK_STICK_LEFT\", \"CLICK_STICK_RIGHT\"", + "description": "Return the last button pressed. \nButtons for Xbox and PS4 can be:\n* Xbox: \"A\", \"B\", \"X\", \"Y\", \"LB\", \"RB\", \"LT\", \"RT\", \"BACK\", \"START\",\n* PS4: \"CROSS\", \"SQUARE\", \"CIRCLE\", \"TRIANGLE\", \"L1\", \"L2\", \"R1\", \"R2\", \"SHARE\", \"OPTIONS\", \"PS_BUTTON\", \"CLICK_TOUCHPAD\",\n* Both: \"UP\", \"DOWN\", \"LEFT\", \"RIGHT\", \"CLICK_STICK_LEFT\", \"CLICK_STICK_RIGHT\".", "fullName": "Last pressed button (string)", "functionType": "StringExpression", - "group": "", "name": "LastButtonString", - "private": false, "sentence": "Button _PARAM2_ of gamepad _PARAM1_ is pressed", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst controllerType = eventsFunctionContext.getArgument(\"controller_type\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in expression: \"Last pressed button (string)\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (controllerType === \"\") {\r\n console.error('Parameter controller type is not valid in expression: \"Last pressed button (string)\"');\r\n return;\r\n}\r\n\r\nvar gamepad = gamepads[playerId];\r\n\r\nif (gamepad !== null) { //Gamepad exist\r\n //Get last btn id\r\n const lastButtonUsedID = gdjs._extensionController.players[playerId].lastButtonUsed;\r\n\r\n //Return last button as string \r\n eventsFunctionContext.returnValue = gdjs._extensionController.getInputString(controllerType, lastButtonUsedID);\r\n\r\n} else { //Gamepad dosen't exist\r\n eventsFunctionContext.returnValue = \"Gamepad not connected\";\r\n}", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst controllerType = eventsFunctionContext.getArgument(\"controller_type\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in string expression: \"Last pressed button (LastButtonString)\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (controllerType === \"\") {\r\n console.error('Parameter controller type is not valid in string expression: \"Last pressed button (LastButtonString)\"');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\nif (gamepad !== null) { //Gamepad exist\r\n //Get last btn id\r\n const lastButtonUsedID = gdjs._extensionController.players[playerId].lastButtonUsed;\r\n\r\n //Return last button as string \r\n eventsFunctionContext.returnValue = gdjs._extensionController.getInputString(controllerType, lastButtonUsedID);\r\n\r\n} else { //Gamepad dosen't exist\r\n console.error('Your controller is not supported or the gamepad wasn\\'t detected in string expression: \"Last pressed button (LastButtonString)\"');\r\n eventsFunctionContext.returnValue = \"Gamepad not connected\";\r\n}", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "string" + }, "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", - "description": "Controller type \"XBOX\" or \"PS4\". This defines the type of button names to return as a string. \"Mapping not exist\" is returned if invalid type is used.", - "longDescription": "", + "description": "Controller type", "name": "controller_type", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"Xbox\",\"PS4\"]", + "type": "stringWithSelector" } ], "objectGroups": [] }, { - "description": "Check if a button is pressed on a gamepad. \nButtons can be:\n* Xbox: \"A\", \"B\", \"X\", \"Y\", \"LB\", \"RB\", \"LT\", \"RT\", \"BACK\", \"START\",\n* PS4: \"CROSS\", \"SQUARE\", \"CIRCLE\", \"TRIANGLE\", \"L1\", \"L2\", \"R1\", \"R2\", \"SHARE\", \"OPTIONS\", \"PS_BUTTON\", \"CLICK_TOUCHPAD\",\n* Other: \"UP\", \"DOWN\", \"LEFT\", \"RIGHT\", \"CLICK_STICK_LEFT\", \"CLICK_STICK_RIGHT\"", + "description": "Check if a button is pressed on a gamepad. \nButtons can be:\n* Xbox: \"A\", \"B\", \"X\", \"Y\", \"LB\", \"RB\", \"LT\", \"RT\", \"BACK\", \"START\",\n* PS4: \"CROSS\", \"SQUARE\", \"CIRCLE\", \"TRIANGLE\", \"L1\", \"L2\", \"R1\", \"R2\", \"SHARE\", \"OPTIONS\", \"PS_BUTTON\", \"CLICK_TOUCHPAD\",\n* Other: \"UP\", \"DOWN\", \"LEFT\", \"RIGHT\", \"CLICK_STICK_LEFT\", \"CLICK_STICK_RIGHT\".", "fullName": "Gamepad button pressed", "functionType": "Condition", - "group": "", "name": "C_Button_pressed", - "private": false, "sentence": "Button _PARAM2_ of gamepad _PARAM1_ is pressed", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst button = eventsFunctionContext.getArgument(\"button\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Gamepad button pressed\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (button === \"\") {\r\n console.error('Parameter button is not valid in condition: \"Gamepad button pressed\"');\r\n return;\r\n}\r\n\r\nvar gamepad = gamepads[playerId];\r\nif (gamepad == null) return;\r\n\r\nvar buttonId;\r\n\r\nswitch (button) {\r\n case 'A':\r\n case 'CROSS':\r\n buttonId = 0;\r\n break;\r\n case 'B':\r\n case 'CIRCLE':\r\n buttonId = 1;\r\n break;\r\n case 'X':\r\n case 'SQUARE':\r\n buttonId = 2;\r\n break;\r\n case 'Y':\r\n case 'TRIANGLE':\r\n buttonId = 3;\r\n break;\r\n case 'LB':\r\n case 'L1':\r\n buttonId = 4;\r\n break;\r\n case 'RB':\r\n case 'R1':\r\n buttonId = 5;\r\n break;\r\n case 'LT':\r\n case 'L2':\r\n buttonId = 6;\r\n break;\r\n case 'RT':\r\n case 'R2':\r\n buttonId = 7;\r\n break;\r\n\r\n case 'UP':\r\n buttonId = 12;\r\n break;\r\n case 'DOWN':\r\n buttonId = 13;\r\n break;\r\n case 'LEFT':\r\n buttonId = 14;\r\n break;\r\n case 'RIGHT':\r\n buttonId = 15;\r\n break;\r\n\r\n case 'BACK':\r\n case 'SHARE':\r\n buttonId = 8;\r\n break;\r\n case 'START':\r\n case 'OPTIONS':\r\n buttonId = 9;\r\n break;\r\n\r\n case 'CLICK_STICK_LEFT':\r\n buttonId = 10;\r\n break;\r\n case 'CLICK_STICK_RIGHT':\r\n buttonId = 11;\r\n break;\r\n\r\n //PS4\r\n case 'PS_BUTTON':\r\n buttonId = 16;\r\n break;\r\n case 'CLICK_TOUCHPAD':\r\n buttonId = 17;\r\n break;\r\n\r\n default:\r\n break;\r\n}\r\n\r\nif (buttonId === undefined) {\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n//When a button is pressed, save the button in lastButtonUsed for each players\r\nif (gamepad.buttons[buttonId].pressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\neventsFunctionContext.returnValue = gamepad.buttons[buttonId].pressed;", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst button = eventsFunctionContext.getArgument(\"button\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Gamepad button pressed\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (button === \"\") {\r\n console.error('Parameter button is not valid in condition: \"Gamepad button pressed\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nlet buttonId;\r\n\r\nswitch (button) {\r\n case 'A':\r\n case 'CROSS':\r\n buttonId = 0;\r\n break;\r\n case 'B':\r\n case 'CIRCLE':\r\n buttonId = 1;\r\n break;\r\n case 'X':\r\n case 'SQUARE':\r\n buttonId = 2;\r\n break;\r\n case 'Y':\r\n case 'TRIANGLE':\r\n buttonId = 3;\r\n break;\r\n case 'LB':\r\n case 'L1':\r\n buttonId = 4;\r\n break;\r\n case 'RB':\r\n case 'R1':\r\n buttonId = 5;\r\n break;\r\n case 'LT':\r\n case 'L2':\r\n buttonId = 6;\r\n break;\r\n case 'RT':\r\n case 'R2':\r\n buttonId = 7;\r\n break;\r\n\r\n case 'UP':\r\n buttonId = 12;\r\n break;\r\n case 'DOWN':\r\n buttonId = 13;\r\n break;\r\n case 'LEFT':\r\n buttonId = 14;\r\n break;\r\n case 'RIGHT':\r\n buttonId = 15;\r\n break;\r\n\r\n case 'BACK':\r\n case 'SHARE':\r\n buttonId = 8;\r\n break;\r\n case 'START':\r\n case 'OPTIONS':\r\n buttonId = 9;\r\n break;\r\n\r\n case 'CLICK_STICK_LEFT':\r\n buttonId = 10;\r\n break;\r\n case 'CLICK_STICK_RIGHT':\r\n buttonId = 11;\r\n break;\r\n\r\n //PS4\r\n case 'PS_BUTTON':\r\n buttonId = 16;\r\n break;\r\n case 'CLICK_TOUCHPAD':\r\n buttonId = 17;\r\n break;\r\n\r\n default:\r\n console.error('The button: ' + button + ' in condition: \"Gamepad button pressed\" is not valid.');\r\n eventsFunctionContext.returnValue = false;\r\n break;\r\n}\r\n\r\n\r\n\r\nif (buttonId === undefined) {\r\n console.error('There is no buttons valid in condition: \"Gamepad button pressed\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\nif (gamepad.buttons == null || gamepad.buttons[buttonId] == null) {\r\n console.error('Buttons on the gamepad are not accessible in condition: \"Gamepad button pressed\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n//When a button is pressed, save the button in lastButtonUsed for each players\r\nif (gamepad.buttons[buttonId].pressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\neventsFunctionContext.returnValue = gamepad.buttons[buttonId].pressed;\r\n\r\n\r\n\r\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", - "description": "The name of the button.", - "longDescription": "", + "description": "Name of the button", "name": "button", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"A\",\"Cross\",\"B\",\"Circle\",\"X\",\"Square\",\"Y\",\"Triangle\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"Up\",\"Down\",\"Left\",\"Right\",\"BAck\",\"Share\",\"Start\",\"Options\",\"Click_Stick_Left\",\"Click_Stick_Right\",\"PS_Button\",\"Click_Touchpad\"]", + "type": "stringWithSelector" } ], "objectGroups": [] @@ -27200,34 +28689,29 @@ "description": "Return the value of the deadzone applied to a gamepad sticks, between 0 and 1.", "fullName": "Gamepad deadzone for sticks", "functionType": "Expression", - "group": "", "name": "Deadzone", - "private": false, "sentence": "", "events": [ { "type": "BuiltinCommonInstructions::Standard", "conditions": [], - "actions": [], - "events": [] + "actions": [] }, { "type": "BuiltinCommonInstructions::JsCode", "inlineCode": "//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in expression: \"Gamepad deadzone for sticks\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\n///Return the deadzone value for a given player\r\neventsFunctionContext.returnValue = gdjs._extensionController.players[playerId].deadzone;", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" } ], @@ -27237,38 +28721,26 @@ "description": "Set the deadzone for sticks of the gamepad. The deadzone is an area for which movement on sticks won't be taken into account (instead, the stick will be considered as not moved). Deadzone is between 0 and 1, and is by default 0.2.", "fullName": "Set gamepad deadzone for sticks", "functionType": "Action", - "group": "", "name": "A_Set_deadzone", - "private": false, "sentence": "Set deadzone for sticks on gamepad: _PARAM1_ to _PARAM2_", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nvar newDeadzone = eventsFunctionContext.getArgument(\"deadzone\");\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in action: \"Set gamepad deadzone for sticks\", is not valid, must be between 0 and 4.');\r\n return;\r\n}\r\n\r\n// clamp the newDeadzone in range [0, 1].\r\n// https://github.com/4ian/GDevelop-extensions/pull/33#issuecomment-618224857\r\ngdjs._extensionController.players[playerId].deadzone = gdjs.evtTools.common.clamp(newDeadzone, 0, 1);\r\n", + "inlineCode": "//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst newDeadzone = eventsFunctionContext.getArgument(\"deadzone\");\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in action: \"Set gamepad deadzone for sticks\", is not valid, must be between 0 and 4.');\r\n return;\r\n}\r\n\r\n// clamp the newDeadzone in range [0, 1].\r\n// https://github.com/4ian/GDevelop-extensions/pull/33#issuecomment-618224857\r\ngdjs._extensionController.players[playerId].deadzone = gdjs.evtTools.common.clamp(newDeadzone, 0, 1);\r\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", "description": "Deadzone for sticks, 0.2 by default (0 to 1)", - "longDescription": "", "name": "deadzone", - "optional": false, - "supplementaryInformation": "", "type": "expression" } ], @@ -27278,105 +28750,86 @@ "description": "Check if a stick of a gamepad is pushed in a given direction.", "fullName": "Gamepad stick pushed (axis)", "functionType": "Condition", - "group": "", "name": "C_Axis_pushed", - "private": false, "sentence": "_PARAM2_ stick of gamepad _PARAM1_ is pushed in direction _PARAM3_", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\nconst direction = eventsFunctionContext.getArgument(\"direction\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Gamepad stick pushed (axis)\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (stick != \"LEFT\" && stick != \"RIGHT\") {\r\n console.error('Parameter stick in action: \"Gamepad stick pushed (axis)\", is not valid, must be LEFT or RIGHT');\r\n return;\r\n}\r\nif (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\") {\r\n console.error('Parameter deadzone in action: \"Gamepad stick pushed (axis)\", is not valid, must be UP, DOWN, LEFT or RIGHT');\r\n return;\r\n}\r\n\r\nvar gamepad = gamepads[playerId];\r\nif (gamepad == null) return;\r\n\r\n//Define in onFirstSceneLoaded function\r\nconst getNormalizedAxisValue = gdjs._extensionController.getNormalizedAxisValue;\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (getNormalizedAxisValue(gamepad.axes[0], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (getNormalizedAxisValue(gamepad.axes[0], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (getNormalizedAxisValue(gamepad.axes[1], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n default:\r\n break;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (getNormalizedAxisValue(gamepad.axes[2], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (getNormalizedAxisValue(gamepad.axes[2], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (getNormalizedAxisValue(gamepad.axes[3], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n default:\r\n break;\r\n }\r\n break;\r\n\r\n default:\r\n break;\r\n}\r\n\r\neventsFunctionContext.returnValue = false;\r\n", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\nconst direction = eventsFunctionContext.getArgument(\"direction\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Gamepad stick pushed (axis)\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (stick != \"LEFT\" && stick != \"RIGHT\") {\r\n console.error('Parameter stick in condition: \"Gamepad stick pushed (axis)\", is not valid, must be LEFT or RIGHT');\r\n return;\r\n}\r\nif (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\" && direction != \"ANY\") {\r\n console.error('Parameter deadzone in condition: \"Gamepad stick pushed (axis)\", is not valid, must be UP, DOWN, LEFT or RIGHT');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) {\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n\r\n//Define in onFirstSceneLoaded function\r\nconst getNormalizedAxisValue = gdjs._extensionController.getNormalizedAxisValue;\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (getNormalizedAxisValue(gamepad.axes[0], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (getNormalizedAxisValue(gamepad.axes[0], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (getNormalizedAxisValue(gamepad.axes[1], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'ANY':\r\n if ( getNormalizedAxisValue(gamepad.axes[0], playerId) < 0\r\n || getNormalizedAxisValue(gamepad.axes[0], playerId) > 0\r\n || getNormalizedAxisValue(gamepad.axes[1], playerId) < 0 \r\n || getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n default:\r\n console.error('The value Direction on stick Left on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r\n eventsFunctionContext.returnValue = false;\r\n break;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (getNormalizedAxisValue(gamepad.axes[2], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (getNormalizedAxisValue(gamepad.axes[2], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (getNormalizedAxisValue(gamepad.axes[3], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'ANY':\r\n if ( getNormalizedAxisValue(gamepad.axes[2], playerId) < 0\r\n || getNormalizedAxisValue(gamepad.axes[2], playerId) > 0\r\n || getNormalizedAxisValue(gamepad.axes[3], playerId) < 0 \r\n || getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n default:\r\n console.error('The value Direction on stick Right on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r\n eventsFunctionContext.returnValue = false;\r\n break;\r\n }\r\n break;\r\n\r\n default:\r\n console.error('The value Stick on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r\n eventsFunctionContext.returnValue = false;\r\n break;\r\n}\r\n\r\neventsFunctionContext.returnValue = false;\r\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", - "description": "Stick: \"LEFT\" or \"RIGHT\"", - "longDescription": "", + "description": "Stick: \"Left\" or \"Right\"", "name": "stick", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"Left\",\"Right\"]", + "type": "stringWithSelector" }, { - "codeOnly": false, - "defaultValue": "", - "description": "Direction: \"UP\", \"DOWN\", \"LEFT\" or \"RIGHT\"", - "longDescription": "", + "description": "Direction", "name": "direction", - "optional": false, - "supplementaryInformation": "", - "type": "string" + "supplementaryInformation": "[\"Up\",\"Down\",\"Left\",\"Right\",\"Any\"]", + "type": "stringWithSelector" } ], "objectGroups": [] }, { - "description": "Return the number of connected gamepads", + "description": "Return the number of connected gamepads.", "fullName": "Connected gamepads number", "functionType": "Expression", - "group": "", "name": "ConnectedGamepadsCount", - "private": false, "sentence": "", "events": [ { "type": "BuiltinCommonInstructions::Standard", "conditions": [], - "actions": [], - "events": [] + "actions": [] }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n// Gamepads can be disconnected and become null, so we have to filter them.\r\neventsFunctionContext.returnValue = Object.keys(gamepads).filter(key => !!gamepads[key]).length;\r\n", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n// Gamepads can be disconnected and become null, so we have to filter them.\r\neventsFunctionContext.returnValue = Object.keys(gamepads).filter(key => !!gamepads[key]).length;\r\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "expression" + }, "parameters": [], "objectGroups": [] }, { - "description": "Return a string containing informations about the specified gamepad", + "description": "Return a string containing informations about the specified gamepad.", "fullName": "Gamepad type", "functionType": "StringExpression", - "group": "", "name": "GamepadType", - "private": false, "sentence": "Player _PARAM1_ use _PARAM2_ controller", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameter\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in expression: \"Gamepad type\", is not valid number, must be between 0 and 4');\n return;\n}\n\neventsFunctionContext.returnValue = (gamepads[playerId] && gamepads[playerId].id) ? gamepads[playerId].id : \"No information for player \" + (playerId + 1)\n", + "inlineCode": "/** @type {Gamepad[]} */\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameter\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in string expression: \"Gamepad type\", is not valid number, must be between 0 and 4');\n return;\n}\n\nconst gamepad = gamepads[playerId];\n\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\nif (gamepad == null) return;\n\neventsFunctionContext.returnValue = (gamepad && gamepad.id) ? gamepad.id : \"No information for player \" + (playerId + 1)\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], + "expressionType": { + "type": "string" + }, "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" } ], @@ -27386,38 +28839,26 @@ "description": "Check if the specified gamepad has the specified information in its description. Useful to know if the gamepad is a Xbox or PS4 controller.", "fullName": "Gamepad type", "functionType": "Condition", - "group": "", "name": "C_Controller_type", - "private": false, "sentence": "Gamepad _PARAM1_ is a _PARAM2_ controller", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameters\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\nconst controllerType = eventsFunctionContext.getArgument(\"controller_type\").toUpperCase();\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in condition: \"Gamepad type\", is not valid number, must be between 0 and 4.');\n return;\n}\nif (controllerType === \"\") {\n console.error('Parameter type in condition: \"Gamepad type\", is not a string.');\n return;\n}\n\nconst gamepad = gamepads[playerId];\neventsFunctionContext.returnValue = gamepad ? gamepad.id.toUpperCase().indexOf(controllerType) !== -1 : false;\n", + "inlineCode": "/** @type {Gamepad[]} */\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameters\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\nconst controllerType = eventsFunctionContext.getArgument(\"controller_type\").toUpperCase();\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in condition: \"Gamepad type\", is not valid number, must be between 0 and 4.');\n return;\n}\nif (controllerType === \"\") {\n console.error('Parameter type in condition: \"Gamepad type\", is not a string.');\n return;\n}\n\nconst gamepad = gamepads[playerId];\n\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\nif (gamepad == null) return;\n\n\nif (controllerType == \"XBOX\") {\n eventsFunctionContext.returnValue = gdjs._extensionController.isXbox(gamepad);\n} else {\n eventsFunctionContext.returnValue = gamepad ? gamepad.id.toUpperCase().indexOf(controllerType) !== -1 : false;\n}", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", - "description": "Type: \"Xbox\", \"PS4\", \"STEAM\" or \"PS3\" (among other)", - "longDescription": "", + "description": "Type: \"Xbox\", \"PS4\", \"Steam\" or \"PS3\" (among other)", "name": "controller_type", - "optional": false, - "supplementaryInformation": "", "type": "string" } ], @@ -27427,28 +28868,21 @@ "description": "Check if a gamepad is connected.", "fullName": "Gamepad connected", "functionType": "Condition", - "group": "", "name": "C_Controller_X_is_connected", - "private": false, "sentence": "Gamepad _PARAM1_ is plugged and connected", "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameter\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in condition: \"Gamepad connected\", is not valid number, must be between 0 and 4.');\n return;\n}\n\n// If gamepad was disconnected it will be null (so this will return false)\n// If gamepad was never connected it will be undefined (so this will return false)\neventsFunctionContext.returnValue = !!gamepads[playerId];", + "inlineCode": "/** @type {Gamepad[]} */\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameter\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in condition: \"Gamepad connected\", is not valid number, must be between 0 and 4.');\n return;\n}\n\n// If gamepad was disconnected it will be null (so this will return false)\n// If gamepad was never connected it will be undefined (so this will return false)\neventsFunctionContext.returnValue = !!gamepads[playerId];", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" } ], @@ -27458,96 +28892,75 @@ "description": "Generate a vibration on the specified controller. Might only work if the game is running in a recent web browser.", "fullName": "Gamepad vibration", "functionType": "Action", - "group": "", "name": "A_Vibrate_controller", - "private": false, "sentence": "Make gamepad _PARAM1_ vibrate for _PARAM2_ seconds", "events": [ { "type": "BuiltinCommonInstructions::Standard", "conditions": [], - "actions": [], - "events": [] + "actions": [] }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\n//Vibration work only on game in browser.\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameters\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\nconst duration = eventsFunctionContext.getArgument(\"duration\") || 1;\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in condition: \"Gamepad connected\", is not valid number, must be between 0 and 4.');\n return;\n}\n\nvar gamepad = gamepads[playerId];\nif (gamepad && gamepad.vibrationActuator) {\n gamepad.vibrationActuator.playEffect(\"dual-rumble\", {\n startDelay: 0,\n duration: duration * 1000,\n weakMagnitude: 1.0,\n strongMagnitude: 1.0\n });\n}", + "inlineCode": "/** @type {Gamepad[]} */\n//Vibration work only on game in browser.\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameters\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\nconst duration = eventsFunctionContext.getArgument(\"duration\") || 1;\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in action: \"Gamepad connected\", is not valid number, must be between 0 and 4.');\n return;\n}\n\nconst gamepad = gamepads[playerId];\n\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\nif (gamepad == null) return;\n\nif (gamepad && gamepad.vibrationActuator) {\n gamepad.vibrationActuator.playEffect(\"dual-rumble\", {\n startDelay: 0,\n duration: duration * 1000,\n weakMagnitude: 1.0,\n strongMagnitude: 1.0\n });\n}", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" }, { - "codeOnly": false, - "defaultValue": "", "description": "Time of the vibration, in seconds (optional, default value is 1)", - "longDescription": "", "name": "duration", - "optional": false, - "supplementaryInformation": "", "type": "expression" } ], "objectGroups": [] }, { - "description": "", "fullName": "", "functionType": "Action", - "group": "", "name": "onFirstSceneLoaded", - "private": false, "sentence": "", "events": [ { "type": "BuiltinCommonInstructions::Standard", "conditions": [], - "actions": [], - "events": [] + "actions": [] }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "//Define an new private object javascript for the gamepad extension\r\ngdjs._extensionController = {\r\n players: {\r\n 0: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n 1: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n 2: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n 3: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n },\r\n controllerButtonNames: { //Map associating controller button ids to button names\r\n \"XBOX\": {\r\n 0: \"A\",\r\n 1: \"B\",\r\n 2: \"X\",\r\n 3: \"Y\",\r\n 4: \"LB\",\r\n 5: \"RB\",\r\n 6: \"LT\",\r\n 7: \"RT\",\r\n 8: \"BACK\",\r\n 9: \"START\",\r\n 10: \"CLICK_STICK_LEFT\",\r\n 11: \"CLICK_STICK_RIGHT\",\r\n 12: \"UP\",\r\n 13: \"DOWN\",\r\n 14: \"LEFT\",\r\n 15: \"RIGHT\",\r\n 16: \"NONE\",\r\n 17: \"NONE\"\r\n },\r\n \"PS4\": {\r\n 0: \"CROSS\",\r\n 1: \"CIRCLE\",\r\n 2: \"SQUARE\",\r\n 3: \"TRIANGLE\",\r\n 4: \"L1\",\r\n 5: \"R1\",\r\n 6: \"L2\",\r\n 7: \"R2\",\r\n 8: \"SHARE\",\r\n 9: \"OPTIONS\",\r\n 10: \"CLICK_STICK_LEFT\",\r\n 11: \"CLICK_STICK_RIGHT\",\r\n 12: \"UP\",\r\n 13: \"DOWN\",\r\n 14: \"LEFT\",\r\n 15: \"RIGHT\",\r\n 16: \"PS_BUTTON\",\r\n 17: \"CLICK_TOUCHPAD\"\r\n }\r\n }\r\n};\r\n\r\ngdjs._extensionController.getInputString = function (type, buttonId) {\r\n const controllerButtonNames = gdjs._extensionController.controllerButtonNames;\r\n if (controllerButtonNames[type] !== undefined) {\r\n return controllerButtonNames[type][buttonId];\r\n }\r\n\r\n return \"UNKNOWN_BUTTON\";\r\n}\r\n\r\ngdjs._extensionController.axisToAngle = function (deltaX, deltaY) {\r\n var rad = Math.atan2(deltaY, deltaX);\r\n var deg = rad * (180 / Math.PI);\r\n return deg;\r\n}\r\n\r\n//Returns the new value taking into account the dead zone for the player_ID given\r\ngdjs._extensionController.getNormalizedAxisValue = function (v, player_ID) {\r\n // gdjs._extensionController = gdjs._extensionController || { deadzone: 0.2 };\r\n\r\n // Anything smaller than this is assumed to be 0,0\r\n var DEADZONE = gdjs._extensionController.players[player_ID].deadzone;\r\n\r\n if (Math.abs(v) < DEADZONE) {\r\n // In the dead zone, set to 0\r\n v = 0;\r\n\r\n if (v == null) {\r\n return 0;\r\n } else {\r\n return v;\r\n }\r\n\r\n } else {\r\n // We're outside the dead zone, but we'd like to smooth\r\n // this value out so it still runs nicely between 0..1.\r\n // That is, we don't want it to jump suddenly from 0 to\r\n // DEADZONE.\r\n\r\n // Remap v from\r\n // DEADZONE..1 to 0..(1-DEADZONE)\r\n // or from\r\n // -1..-DEADZONE to -(1-DEADZONE)..0\r\n\r\n v = v - Math.sign(v) * DEADZONE;\r\n\r\n // Remap v from\r\n // 0..(1-DEADZONE) to 0..1\r\n // or from\r\n // -(1-DEADZONE)..0 to -1..0\r\n\r\n return v / (1 - DEADZONE);\r\n }\r\n};", + "inlineCode": "//Define an new private object javascript for the gamepad extension\r\ngdjs._extensionController = {\r\n players: {\r\n 0: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n 1: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n 2: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n 3: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n },\r\n controllerButtonNames: { //Map associating controller button ids to button names\r\n \"XBOX\": {\r\n 0: \"A\",\r\n 1: \"B\",\r\n 2: \"X\",\r\n 3: \"Y\",\r\n 4: \"LB\",\r\n 5: \"RB\",\r\n 6: \"LT\",\r\n 7: \"RT\",\r\n 8: \"BACK\",\r\n 9: \"START\",\r\n 10: \"CLICK_STICK_LEFT\",\r\n 11: \"CLICK_STICK_RIGHT\",\r\n 12: \"UP\",\r\n 13: \"DOWN\",\r\n 14: \"LEFT\",\r\n 15: \"RIGHT\",\r\n 16: \"NONE\",\r\n 17: \"NONE\"\r\n },\r\n \"PS4\": {\r\n 0: \"CROSS\",\r\n 1: \"CIRCLE\",\r\n 2: \"SQUARE\",\r\n 3: \"TRIANGLE\",\r\n 4: \"L1\",\r\n 5: \"R1\",\r\n 6: \"L2\",\r\n 7: \"R2\",\r\n 8: \"SHARE\",\r\n 9: \"OPTIONS\",\r\n 10: \"CLICK_STICK_LEFT\",\r\n 11: \"CLICK_STICK_RIGHT\",\r\n 12: \"UP\",\r\n 13: \"DOWN\",\r\n 14: \"LEFT\",\r\n 15: \"RIGHT\",\r\n 16: \"PS_BUTTON\",\r\n 17: \"CLICK_TOUCHPAD\"\r\n }\r\n }\r\n};\r\n\r\ngdjs._extensionController.getInputString = function (type, buttonId) {\r\n const controllerButtonNames = gdjs._extensionController.controllerButtonNames;\r\n if (controllerButtonNames[type] !== undefined) {\r\n return controllerButtonNames[type][buttonId];\r\n }\r\n\r\n return \"UNKNOWN_BUTTON\";\r\n}\r\n\r\ngdjs._extensionController.axisToAngle = function (deltaX, deltaY) {\r\n const rad = Math.atan2(deltaY, deltaX);\r\n const deg = rad * (180 / Math.PI);\r\n return deg;\r\n}\r\n\r\ngdjs._extensionController.isXbox = function (gamepad) {\r\n return (gamepad ? (\r\n gamepad.id.toUpperCase().indexOf(\"XBOX\") !== -1\r\n // \"XINPUT\" cannot be used to check if it is a xbox controller is just a generic\r\n // name reported in Firefox corresponding to the driver being used by the controller\r\n // https://gamefaqs.gamespot.com/boards/916373-pc/73341312?page=1\r\n ) : false);\r\n}\r\n\r\n//Returns the new value taking into account the dead zone for the player_ID given\r\ngdjs._extensionController.getNormalizedAxisValue = function (v, player_ID) {\r\n // gdjs._extensionController = gdjs._extensionController || { deadzone: 0.2 };\r\n\r\n // Anything smaller than this is assumed to be 0,0\r\n const DEADZONE = gdjs._extensionController.players[player_ID].deadzone;\r\n\r\n if (Math.abs(v) < DEADZONE) {\r\n // In the dead zone, set to 0\r\n v = 0;\r\n\r\n if (v == null) {\r\n return 0;\r\n } else {\r\n return v;\r\n }\r\n\r\n } else {\r\n // We're outside the dead zone, but we'd like to smooth\r\n // this value out so it still runs nicely between 0..1.\r\n // That is, we don't want it to jump suddenly from 0 to\r\n // DEADZONE.\r\n\r\n // Remap v from\r\n // DEADZONE..1 to 0..(1-DEADZONE)\r\n // or from\r\n // -1..-DEADZONE to -(1-DEADZONE)..0\r\n\r\n v = v - Math.sign(v) * DEADZONE;\r\n\r\n // Remap v from\r\n // 0..(1-DEADZONE) to 0..1\r\n // or from\r\n // -(1-DEADZONE)..0 to -1..0\r\n\r\n return v / (1 - DEADZONE);\r\n }\r\n};", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], "objectGroups": [] }, { - "description": "", "fullName": "", "functionType": "Action", - "group": "", "name": "onScenePostEvents", - "private": false, "sentence": "", "events": [ { "type": "BuiltinCommonInstructions::Standard", "conditions": [], - "actions": [], - "events": [] + "actions": [] }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "//Each time a player press a button i save the last button pressed for the next frame\n/** @type {Gamepad[]} */\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameter\nlet countPlayers = Object.keys(gdjs._extensionController.players).length;\n\n//Repeat for each players\nfor (var i = 0; i < countPlayers; i++) {\n var gamepad = gamepads[i]; // Get the gamepad of the player\n if (gamepad == null) return;\n\n for (var b = 0; b < Object.keys(gamepad.buttons).length; b++) { //For each buttons\n if (gamepad.buttons[b].pressed) { //One of them is pressed\n gdjs._extensionController.players[i].lastButtonUsed = b; //Save the button pressed\n\n //Save the state of the button for the next frame.\n gdjs._extensionController.players[i].previousFrameStateButtons[b] = { pressed: true };\n }\n }\n}\n\n\n", + "inlineCode": "//Each time a player press a button i save the last button pressed for the next frame\n/** @type {Gamepad[]} */\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameter\nlet countPlayers = Object.keys(gdjs._extensionController.players).length;\n\n//Repeat for each players\nfor (let i = 0; i < countPlayers; i++) {\n let gamepad = gamepads[i]; // Get the gamepad of the player\n\n //we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\n if (gamepad == null) {\n return;\n }\n\n for (let b = 0; b < Object.keys(gamepad.buttons).length; b++) { //For each buttons\n if (gamepad.buttons[b].pressed) { //One of them is pressed\n gdjs._extensionController.players[i].lastButtonUsed = b; //Save the button pressed\n\n //Save the state of the button for the next frame.\n gdjs._extensionController.players[i].previousFrameStateButtons[b] = { pressed: true };\n }\n }\n}\n\n\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [], @@ -27557,41 +28970,34 @@ "description": "Check if any button is released on a gamepad.", "fullName": "Any gamepad button released", "functionType": "Condition", - "group": "", "name": "C_any_button_released", - "private": false, "sentence": "Any button of gamepad _PARAM1_ is released", "events": [ { "type": "BuiltinCommonInstructions::Standard", "conditions": [], - "actions": [], - "events": [] + "actions": [] }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nvar gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n\tconsole.error('Parameter gamepad identifier in condition: \"Any gamepad button released\", is not valid number, must be between 0 and 4.');\r\n\treturn;\r\n}\r\n\r\nvar gamepad = gamepads[playerId];\r\nif (gamepad == null) return;\r\n\r\n\r\nfor (var buttonId = 0; buttonId < gamepad.buttons.length; buttonId++) { //For each buttons on current frame.\r\n\r\n\r\n\tif (buttonId === undefined) {\r\n\t\teventsFunctionContext.returnValue = false;\r\n\t\treturn;\r\n\t}\r\n\r\n\t//Get previous value or define value by default for the current button\r\n\tgdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] || { pressed: false };\r\n\r\n\t//Get state of the button at previous frame\r\n\tvar previousStateButtonIsPressed = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed;\r\n\r\n\t//Get the state of the button on the current frame.\r\n\tvar currentFrameStateButtonIsPressed = gamepad.buttons[buttonId].pressed;\r\n\r\n\t//When previousStateButtonIsPressed is true and actual button state is not pressed\r\n\t//Player have release the button\r\n\tif (previousStateButtonIsPressed === true && currentFrameStateButtonIsPressed === false) {\r\n\t\tgdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = false;\r\n\t\teventsFunctionContext.returnValue = true;\r\n\t\t//break;\r\n\t\treturn;\r\n\t} else {\r\n\t\teventsFunctionContext.returnValue = false;\r\n\t}\r\n\r\n\tif (currentFrameStateButtonIsPressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\n}\r\n", + "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n\tconsole.error('Parameter gamepad identifier in condition: \"Any gamepad button released\", is not valid number, must be between 0 and 4.');\r\n\treturn;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nfor (let buttonId = 0; buttonId < gamepad.buttons.length; buttonId++) { //For each buttons on current frame.\r\n\r\n\tif (buttonId === undefined) {\r\n\t\teventsFunctionContext.returnValue = false;\r\n\t\treturn;\r\n\t}\r\n\r\n\t//Get previous value or define value by default for the current button\r\n\tgdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] || { pressed: false };\r\n\r\n\t//Get state of the button at previous frame\r\n\tconst previousStateButtonIsPressed = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed;\r\n\r\n\t//Get the state of the button on the current frame.\r\n\tconst currentFrameStateButtonIsPressed = gamepad.buttons[buttonId].pressed;\r\n\r\n\t//When previousStateButtonIsPressed is true and actual button state is not pressed\r\n\t//Player have release the button\r\n\tif (previousStateButtonIsPressed === true && currentFrameStateButtonIsPressed === false) {\r\n\t\tgdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = false;\r\n\t\teventsFunctionContext.returnValue = true;\r\n\t\t//break;\r\n\t\treturn;\r\n\t} else {\r\n\t\teventsFunctionContext.returnValue = false;\r\n\t}\r\n\r\n\tif (currentFrameStateButtonIsPressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\n}\r\n", "parameterObjects": "", "useStrict": true, - "eventsSheetExpanded": false + "eventsSheetExpanded": true } ], "parameters": [ { - "codeOnly": false, - "defaultValue": "", "description": "The gamepad identifier: 1, 2, 3 or 4", - "longDescription": "", "name": "player_ID", - "optional": false, - "supplementaryInformation": "", "type": "expression" } ], "objectGroups": [] } ], - "eventsBasedBehaviors": [] + "eventsBasedBehaviors": [], + "eventsBasedObjects": [] } ], "externalLayouts": [], From 4690efa9392420c1a06ff3ba9858fb3dade608c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sat, 26 Nov 2022 00:13:04 +0100 Subject: [PATCH 02/11] Add function for the grid --- examples/isometric-game/isometric-game.json | 491 +++++++++++++++++++- 1 file changed, 489 insertions(+), 2 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index b84c10ee4..bd96c447b 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -4547,7 +4547,7 @@ "gridColor": 0, "gridAlpha": 0.5, "snap": true, - "zoomFactor": 0.3833333129882819, + "zoomFactor": 0.4099999755859377, "windowMask": false }, "objectsGroups": [ @@ -26295,6 +26295,7 @@ "colorG": 176, "colorR": 74, "creationTime": 0, + "disabled": true, "name": "Sound", "source": "", "type": "BuiltinCommonInstructions::Group", @@ -26638,7 +26639,7 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nfunction __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\n/**\n * Stripped down version of Phaser's Vector2 with just the functionality needed for navmeshes.\n *\n * @export\n * @class Vector2\n */\nvar Vector2 = /** @class */ (function () {\n function Vector2(x, y) {\n if (x === void 0) { x = 0; }\n if (y === void 0) { y = 0; }\n this.x = x;\n this.y = y;\n }\n Vector2.prototype.equals = function (v) {\n return this.x === v.x && this.y === v.y;\n };\n Vector2.prototype.angle = function (v) {\n return Math.atan2(v.y - this.y, v.x - this.x);\n };\n Vector2.prototype.distance = function (v) {\n var dx = v.x - this.x;\n var dy = v.y - this.y;\n return Math.sqrt(dx * dx + dy * dy);\n };\n Vector2.prototype.add = function (v) {\n this.x += v.x;\n this.y += v.y;\n };\n Vector2.prototype.subtract = function (v) {\n this.x -= v.x;\n this.y -= v.y;\n };\n Vector2.prototype.clone = function () {\n return new Vector2(this.x, this.y);\n };\n return Vector2;\n}());\n\nvar GridNode = /** @class */ (function () {\n function GridNode(weight) {\n this.h = 0;\n this.g = 0;\n this.f = 0;\n this.closed = false;\n this.visited = false;\n this.parent = null;\n this.weight = weight;\n }\n GridNode.prototype.isWall = function () {\n return this.weight === 0;\n };\n GridNode.prototype.clean = function () {\n this.f = 0;\n this.g = 0;\n this.h = 0;\n this.visited = false;\n this.closed = false;\n this.parent = null;\n };\n return GridNode;\n}());\n\n/**\n * A class that represents a navigable polygon with a navmesh. It is built on top of a\n * {@link Polygon}. It implements the properties and fields that javascript-astar needs - weight,\n * toString, isWall and getCost. See GPS test from astar repo for structure:\n * https://github.com/bgrins/javascript-astar/blob/master/test/tests.js\n */\nvar NavPoly = /** @class */ (function (_super) {\n __extends(NavPoly, _super);\n /**\n * Creates an instance of NavPoly.\n */\n function NavPoly(id, polygon) {\n var _this = _super.call(this, 1) || this;\n _this.id = id;\n _this.polygon = polygon;\n _this.edges = polygon.edges;\n _this.neighbors = [];\n _this.portals = [];\n _this.centroid = _this.calculateCentroid();\n _this.boundingRadius = _this.calculateRadius();\n return _this;\n }\n /**\n * Returns an array of points that form the polygon.\n */\n NavPoly.prototype.getPoints = function () {\n return this.polygon.points;\n };\n /**\n * Check if the given point-like object is within the polygon.\n */\n NavPoly.prototype.contains = function (point) {\n // Phaser's polygon check doesn't handle when a point is on one of the edges of the line. Note:\n // check numerical stability here. It would also be good to optimize this for different shapes.\n return this.polygon.contains(point.x, point.y) || this.isPointOnEdge(point);\n };\n /**\n * Only rectangles are supported, so this calculation works, but this is not actually the centroid\n * calculation for a polygon. This is just the average of the vertices - proper centroid of a\n * polygon factors in the area.\n */\n NavPoly.prototype.calculateCentroid = function () {\n var centroid = new Vector2(0, 0);\n var length = this.polygon.points.length;\n this.polygon.points.forEach(function (p) { return centroid.add(p); });\n centroid.x /= length;\n centroid.y /= length;\n return centroid;\n };\n /**\n * Calculate the radius of a circle that circumscribes the polygon.\n */\n NavPoly.prototype.calculateRadius = function () {\n var boundingRadius = 0;\n for (var _i = 0, _a = this.polygon.points; _i < _a.length; _i++) {\n var point = _a[_i];\n var d = this.centroid.distance(point);\n if (d > boundingRadius)\n boundingRadius = d;\n }\n return boundingRadius;\n };\n /**\n * Check if the given point-like object is on one of the edges of the polygon.\n */\n NavPoly.prototype.isPointOnEdge = function (_a) {\n var x = _a.x, y = _a.y;\n for (var _i = 0, _b = this.edges; _i < _b.length; _i++) {\n var edge = _b[_i];\n if (edge.pointOnSegment(x, y))\n return true;\n }\n return false;\n };\n NavPoly.prototype.destroy = function () {\n this.neighbors = [];\n this.portals = [];\n };\n // === jsastar methods ===\n NavPoly.prototype.toString = function () {\n return \"NavPoly(id: \" + this.id + \" at: \" + this.centroid + \")\";\n };\n NavPoly.prototype.isWall = function () {\n return false;\n };\n NavPoly.prototype.centroidDistance = function (navPolygon) {\n return this.centroid.distance(navPolygon.centroid);\n };\n NavPoly.prototype.getCost = function (navPolygon) {\n //TODO the cost method should not be in the Node\n return this.centroidDistance(navPolygon);\n };\n return NavPoly;\n}(GridNode));\n\n/**\n * A graph memory structure\n */\nvar Graph = /** @class */ (function () {\n /**\n * A graph memory structure\n * @param {Array} gridIn 2D array of input weights\n * @param {Object} [options]\n * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed\n */\n function Graph(nodes, options) {\n this.dirtyNodes = [];\n options = options || {};\n this.nodes = nodes;\n this.diagonal = !!options.diagonal;\n this.init();\n }\n Graph.prototype.init = function () {\n this.dirtyNodes = [];\n for (var i = 0; i < this.nodes.length; i++) {\n this.nodes[i].clean();\n }\n };\n Graph.prototype.cleanDirty = function () {\n for (var i = 0; i < this.dirtyNodes.length; i++) {\n this.dirtyNodes[i].clean();\n }\n this.dirtyNodes = [];\n };\n Graph.prototype.markDirty = function (node) {\n this.dirtyNodes.push(node);\n };\n return Graph;\n}());\n\n/**\n * Graph for javascript-astar. It implements the functionality for astar. See GPS test from astar\n * repo for structure: https://github.com/bgrins/javascript-astar/blob/master/test/tests.js\n *\n * @class NavGraph\n * @private\n */\nvar NavGraph = /** @class */ (function (_super) {\n __extends(NavGraph, _super);\n function NavGraph(navPolygons) {\n var _this = _super.call(this, navPolygons) || this;\n _this.nodes = navPolygons;\n _this.init();\n return _this;\n }\n NavGraph.prototype.neighbors = function (navPolygon) {\n return navPolygon.neighbors;\n };\n NavGraph.prototype.navHeuristic = function (navPolygon1, navPolygon2) {\n return navPolygon1.centroidDistance(navPolygon2);\n };\n NavGraph.prototype.destroy = function () {\n this.cleanDirty();\n this.nodes = [];\n };\n return NavGraph;\n}(Graph));\n\n/**\n * Calculate the distance squared between two points. This is an optimization to a square root when\n * you just need to compare relative distances without needing to know the specific distance.\n * @param a\n * @param b\n */\nfunction distanceSquared(a, b) {\n var dx = b.x - a.x;\n var dy = b.y - a.y;\n return dx * dx + dy * dy;\n}\n/**\n * Project a point onto a line segment.\n * JS Source: http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment\n * @param point\n * @param line\n */\nfunction projectPointToEdge(point, line) {\n var a = line.start;\n var b = line.end;\n // Consider the parametric equation for the edge's line, p = a + t (b - a). We want to find\n // where our point lies on the line by solving for t:\n // t = [(p-a) . (b-a)] / |b-a|^2\n var l2 = distanceSquared(a, b);\n var t = ((point.x - a.x) * (b.x - a.x) + (point.y - a.y) * (b.y - a.y)) / l2;\n // We clamp t from [0,1] to handle points outside the segment vw.\n t = clamp(t, 0, 1);\n // Project onto the segment\n var p = new Vector2(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y));\n return p;\n}\n/**\n * Twice the area of the triangle formed by a, b and c.\n */\nfunction triarea2(a, b, c) {\n var ax = b.x - a.x;\n var ay = b.y - a.y;\n var bx = c.x - a.x;\n var by = c.y - a.y;\n return bx * ay - ax * by;\n}\n/**\n * Clamp the given value between min and max.\n */\nfunction clamp(value, min, max) {\n if (value < min)\n value = min;\n if (value > max)\n value = max;\n return value;\n}\n/**\n * Check if two values are within a small margin of one another.\n */\nfunction almostEqual(value1, value2, errorMargin) {\n if (errorMargin === void 0) { errorMargin = 0.0001; }\n if (Math.abs(value1 - value2) <= errorMargin)\n return true;\n else\n return false;\n}\n/**\n * Find the smallest angle difference between two angles\n * https://gist.github.com/Aaronduino/4068b058f8dbc34b4d3a9eedc8b2cbe0\n */\nfunction angleDifference(x, y) {\n var a = x - y;\n var i = a + Math.PI;\n var j = Math.PI * 2;\n a = i - Math.floor(i / j) * j; // (a+180) % 360; this ensures the correct sign\n a -= Math.PI;\n return a;\n}\n/**\n * Check if two lines are collinear (within a small error margin).\n */\nfunction areCollinear(line1, line2, errorMargin) {\n if (errorMargin === void 0) { errorMargin = 0.0001; }\n // Figure out if the two lines are equal by looking at the area of the triangle formed\n // by their points\n var area1 = triarea2(line1.start, line1.end, line2.start);\n var area2 = triarea2(line1.start, line1.end, line2.end);\n if (almostEqual(area1, 0, errorMargin) && almostEqual(area2, 0, errorMargin)) {\n return true;\n }\n else\n return false;\n}\n\n// Mostly sourced from PatrolJS at the moment. TODO: come back and reimplement this as an incomplete\n/**\n * @private\n */\nvar Channel = /** @class */ (function () {\n function Channel() {\n this.portals = [];\n this.path = [];\n }\n Channel.prototype.push = function (p1, p2) {\n if (p2 === undefined)\n p2 = p1;\n this.portals.push({\n left: p1,\n right: p2,\n });\n };\n Channel.prototype.stringPull = function () {\n var portals = this.portals;\n var pts = [];\n // Init scan state\n var apexIndex = 0;\n var leftIndex = 0;\n var rightIndex = 0;\n var portalApex = portals[0].left;\n var portalLeft = portals[0].left;\n var portalRight = portals[0].right;\n // Add start point.\n pts.push(portalApex);\n for (var i = 1; i < portals.length; i++) {\n // Find the next portal vertices\n var left = portals[i].left;\n var right = portals[i].right;\n // Update right vertex.\n if (triarea2(portalApex, portalRight, right) <= 0.0) {\n if (portalApex.equals(portalRight) || triarea2(portalApex, portalLeft, right) > 0.0) {\n // Tighten the funnel.\n portalRight = right;\n rightIndex = i;\n }\n else {\n // Right vertex just crossed over the left vertex, so the left vertex should\n // now be part of the path.\n pts.push(portalLeft);\n // Restart scan from portal left point.\n // Make current left the new apex.\n portalApex = portalLeft;\n apexIndex = leftIndex;\n // Reset portal\n portalLeft = portalApex;\n portalRight = portalApex;\n leftIndex = apexIndex;\n rightIndex = apexIndex;\n // Restart scan\n i = apexIndex;\n continue;\n }\n }\n // Update left vertex.\n if (triarea2(portalApex, portalLeft, left) >= 0.0) {\n if (portalApex.equals(portalLeft) || triarea2(portalApex, portalRight, left) < 0.0) {\n // Tighten the funnel.\n portalLeft = left;\n leftIndex = i;\n }\n else {\n // Left vertex just crossed over the right vertex, so the right vertex should\n // now be part of the path\n pts.push(portalRight);\n // Restart scan from portal right point.\n // Make current right the new apex.\n portalApex = portalRight;\n apexIndex = rightIndex;\n // Reset portal\n portalLeft = portalApex;\n portalRight = portalApex;\n leftIndex = apexIndex;\n rightIndex = apexIndex;\n // Restart scan\n i = apexIndex;\n continue;\n }\n }\n }\n if (pts.length === 0 || !pts[pts.length - 1].equals(portals[portals.length - 1].left)) {\n // Append last point to path.\n pts.push(portals[portals.length - 1].left);\n }\n this.path = pts;\n return pts;\n };\n return Channel;\n}());\n\n/**\n * Stripped down version of Phaser's Line with just the functionality needed for navmeshes.\n *\n * @export\n * @class Line\n */\nvar Line = /** @class */ (function () {\n function Line(x1, y1, x2, y2) {\n this.start = new Vector2(x1, y1);\n this.end = new Vector2(x2, y2);\n this.left = Math.min(x1, x2);\n this.right = Math.max(x1, x2);\n this.top = Math.min(y1, y2);\n this.bottom = Math.max(y1, y2);\n }\n Line.prototype.pointOnSegment = function (x, y) {\n return (x >= this.left &&\n x <= this.right &&\n y >= this.top &&\n y <= this.bottom &&\n this.pointOnLine(x, y));\n };\n Line.prototype.pointOnLine = function (x, y) {\n // Compare slope of line start -> xy to line start -> line end\n return (x - this.left) * (this.bottom - this.top) === (this.right - this.left) * (y - this.top);\n };\n return Line;\n}());\n\n/**\n * Stripped down version of Phaser's Polygon with just the functionality needed for navmeshes.\n *\n * @export\n * @class Polygon\n */\nvar Polygon = /** @class */ (function () {\n function Polygon(points, closed) {\n if (closed === void 0) { closed = true; }\n this.isClosed = closed;\n this.points = points;\n this.edges = [];\n for (var i = 1; i < points.length; i++) {\n var p1 = points[i - 1];\n var p2 = points[i];\n this.edges.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n if (this.isClosed) {\n var first = points[0];\n var last = points[points.length - 1];\n this.edges.push(new Line(first.x, first.y, last.x, last.y));\n }\n }\n Polygon.prototype.contains = function (x, y) {\n var inside = false;\n for (var i = -1, j = this.points.length - 1; ++i < this.points.length; j = i) {\n var ix = this.points[i].x;\n var iy = this.points[i].y;\n var jx = this.points[j].x;\n var jy = this.points[j].y;\n if (((iy <= y && y < jy) || (jy <= y && y < iy)) &&\n x < ((jx - ix) * (y - iy)) / (jy - iy) + ix) {\n inside = !inside;\n }\n }\n return inside;\n };\n return Polygon;\n}());\n\nvar BinaryHeap = /** @class */ (function () {\n function BinaryHeap(scoreFunction) {\n this.content = new Array();\n this.scoreFunction = scoreFunction;\n }\n BinaryHeap.prototype.push = function (element) {\n // Add the new element to the end of the array.\n this.content.push(element);\n // Allow it to sink down.\n this.sinkDown(this.content.length - 1);\n };\n BinaryHeap.prototype.pop = function () {\n // Store the first element so we can return it later.\n var result = this.content[0];\n // Get the element at the end of the array.\n var end = this.content.pop();\n if (!end)\n return;\n // If there are any elements left, put the end element at the\n // start, and let it bubble up.\n if (this.content.length > 0) {\n this.content[0] = end;\n this.bubbleUp(0);\n }\n return result;\n };\n BinaryHeap.prototype.remove = function (node) {\n var i = this.content.indexOf(node);\n // When it is found, the process seen in 'pop' is repeated\n // to fill up the hole.\n var end = this.content.pop();\n if (!end)\n return;\n if (i !== this.content.length - 1) {\n this.content[i] = end;\n if (this.scoreFunction(end) < this.scoreFunction(node)) {\n this.sinkDown(i);\n }\n else {\n this.bubbleUp(i);\n }\n }\n };\n BinaryHeap.prototype.size = function () {\n return this.content.length;\n };\n BinaryHeap.prototype.rescoreElement = function (node) {\n this.sinkDown(this.content.indexOf(node));\n };\n BinaryHeap.prototype.sinkDown = function (n) {\n // Fetch the element that has to be sunk.\n var element = this.content[n];\n // When at 0, an element can not sink any further.\n while (n > 0) {\n // Compute the parent element's index, and fetch it.\n var parentN = ((n + 1) >> 1) - 1;\n var parent = this.content[parentN];\n // Swap the elements if the parent is greater.\n if (this.scoreFunction(element) < this.scoreFunction(parent)) {\n this.content[parentN] = element;\n this.content[n] = parent;\n // Update 'n' to continue at the new position.\n n = parentN;\n }\n // Found a parent that is less, no need to sink any further.\n else {\n break;\n }\n }\n };\n BinaryHeap.prototype.bubbleUp = function (n) {\n // Look up the target element and its score.\n var length = this.content.length;\n var element = this.content[n];\n var elemScore = this.scoreFunction(element);\n while (true) {\n // Compute the indices of the child elements.\n var child2N = (n + 1) << 1;\n var child1N = child2N - 1;\n // This is used to store the new position of the element, if any.\n var swap = null;\n var child1Score = 0;\n // If the first child exists (is inside the array)...\n if (child1N < length) {\n // Look it up and compute its score.\n var child1 = this.content[child1N];\n child1Score = this.scoreFunction(child1);\n // If the score is less than our element's, we need to swap.\n if (child1Score < elemScore) {\n swap = child1N;\n }\n }\n // Do the same checks for the other child.\n if (child2N < length) {\n var child2 = this.content[child2N];\n var child2Score = this.scoreFunction(child2);\n if (child2Score < (swap === null ? elemScore : child1Score)) {\n swap = child2N;\n }\n }\n // If the element needs to be moved, swap it, and continue.\n if (swap !== null) {\n this.content[n] = this.content[swap];\n this.content[swap] = element;\n n = swap;\n }\n // Otherwise, we are done.\n else {\n break;\n }\n }\n };\n return BinaryHeap;\n}());\n\n// The following implementation of the A* algorithm is from:\nvar AStar = /** @class */ (function () {\n function AStar() {\n }\n /**\n * Perform an A* Search on a graph given a start and end node.\n * @param {Graph} graph\n * @param {GridNode} start\n * @param {GridNode} end\n * @param {Object} [options]\n * @param {bool} [options.closest] Specifies whether to return the\n path to the closest node if the target is unreachable.\n * @param {Function} [options.heuristic] Heuristic function (see\n * astar.heuristics).\n */\n AStar.prototype.search = function (graph, start, end, \n // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html\n heuristic, closest) {\n if (closest === void 0) { closest = false; }\n graph.cleanDirty();\n var openHeap = this.getHeap();\n var closestNode = start; // set the start node to be the closest if required\n start.h = heuristic(start, end);\n graph.markDirty(start);\n openHeap.push(start);\n while (openHeap.size() > 0) {\n // Grab the lowest f(x) to process next. Heap keeps this sorted for us.\n var currentNode = openHeap.pop();\n // never happen\n if (!currentNode)\n return [];\n // End case -- result has been found, return the traced path.\n if (currentNode === end) {\n return this.pathTo(currentNode);\n }\n // Normal case -- move currentNode from open to closed, process each of its neighbors.\n currentNode.closed = true;\n // Find all neighbors for the current node.\n var neighbors = graph.neighbors(currentNode);\n for (var i = 0, il = neighbors.length; i < il; ++i) {\n var neighbor = neighbors[i];\n if (neighbor.closed || neighbor.isWall()) {\n // Not a valid node to process, skip to next neighbor.\n continue;\n }\n // The g score is the shortest distance from start to current node.\n // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.\n var gScore = currentNode.g + neighbor.getCost(currentNode);\n var beenVisited = neighbor.visited;\n if (!beenVisited || gScore < neighbor.g) {\n // Found an optimal (so far) path to this node. Take score for node to see how good it is.\n neighbor.visited = true;\n neighbor.parent = currentNode;\n neighbor.h = neighbor.h || heuristic(neighbor, end);\n neighbor.g = gScore;\n neighbor.f = neighbor.g + neighbor.h;\n graph.markDirty(neighbor);\n if (closest) {\n // If the neighbor is closer than the current closestNode or if it's equally close but has\n // a cheaper path than the current closest node then it becomes the closest node\n if (neighbor.h < closestNode.h ||\n (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) {\n closestNode = neighbor;\n }\n }\n if (!beenVisited) {\n // Pushing to heap will put it in proper place based on the 'f' value.\n openHeap.push(neighbor);\n }\n else {\n // Already seen the node, but since it has been rescored we need to reorder it in the heap\n openHeap.rescoreElement(neighbor);\n }\n }\n }\n }\n if (closest) {\n return this.pathTo(closestNode);\n }\n // No result was found - empty array signifies failure to find path.\n return [];\n };\n AStar.prototype.pathTo = function (node) {\n var curr = node;\n var path = new Array();\n while (curr.parent) {\n path.unshift(curr);\n curr = curr.parent;\n }\n return path;\n };\n AStar.prototype.getHeap = function () {\n return new BinaryHeap(function (node) {\n return node.f;\n });\n };\n return AStar;\n}());\n\n/**\n * The `NavMesh` class is the workhorse that represents a navigation mesh built from a series of\n * polygons. Once built, the mesh can be asked for a path from one point to another point. Some\n * internal terminology usage:\n * - neighbor: a polygon that shares part of an edge with another polygon\n * - portal: when two neighbor's have edges that overlap, the portal is the overlapping line segment\n * - channel: the path of polygons from starting point to end point\n * - pull the string: run the funnel algorithm on the channel so that the path hugs the edges of the\n * channel. Equivalent to having a string snaking through a hallway and then pulling it taut.\n */\nvar NavMesh = /** @class */ (function () {\n /**\n * @param meshPolygonPoints Array where each element is an array of point-like objects that\n * defines a polygon.\n * @param meshShrinkAmount The amount (in pixels) that the navmesh has been shrunk around\n * obstacles (a.k.a the amount obstacles have been expanded).\n */\n function NavMesh(meshPolygonPoints, meshShrinkAmount) {\n if (meshShrinkAmount === void 0) { meshShrinkAmount = 0; }\n this.meshShrinkAmount = meshShrinkAmount;\n // Convert the PolyPoints[] into NavPoly instances.\n this.navPolygons = meshPolygonPoints.map(function (polyPoints, i) { return new NavPoly(i, new Polygon(polyPoints)); });\n this.calculateNeighbors();\n // Astar graph of connections between polygons\n this.graph = new NavGraph(this.navPolygons);\n }\n /**\n * Get the NavPolys that are in this navmesh.\n */\n NavMesh.prototype.getPolygons = function () {\n return this.navPolygons;\n };\n /**\n * Cleanup method to remove references.\n */\n NavMesh.prototype.destroy = function () {\n this.graph.destroy();\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var poly = _a[_i];\n poly.destroy();\n }\n this.navPolygons = [];\n };\n /**\n * Find if the given point is within any of the polygons in the mesh.\n * @param point\n */\n NavMesh.prototype.isPointInMesh = function (point) {\n return this.navPolygons.some(function (navPoly) { return navPoly.contains(point); });\n };\n /**\n * Find the closest point in the mesh to the given point. If the point is already in the mesh,\n * this will give you that point. If the point is outside of the mesh, this will attempt to\n * project this point into the mesh (up to the given maxAllowableDist). This returns an object\n * with:\n * - distance - from the given point to the mesh\n * - polygon - the one the point is closest to, or null\n * - point - the point inside the mesh, or null\n * @param point\n * @param maxAllowableDist\n */\n NavMesh.prototype.findClosestMeshPoint = function (point, maxAllowableDist) {\n if (maxAllowableDist === void 0) { maxAllowableDist = Number.POSITIVE_INFINITY; }\n var minDistance = maxAllowableDist;\n var closestPoly = null;\n var pointOnClosestPoly = null;\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var navPoly = _a[_i];\n // If we are inside a poly, we've got the closest.\n if (navPoly.contains(point)) {\n minDistance = 0;\n closestPoly = navPoly;\n pointOnClosestPoly = point;\n break;\n }\n // Is the poly close enough to warrant a more accurate check? Point is definitely outside of\n // the polygon. Distance - Radius is the smallest possible distance to an edge of the poly.\n // This will underestimate distance, but that's perfectly fine.\n var r = navPoly.boundingRadius;\n var d = navPoly.centroid.distance(point);\n if (d - r < minDistance) {\n var result = this.projectPointToPolygon(point, navPoly);\n if (result.distance < minDistance) {\n minDistance = result.distance;\n closestPoly = navPoly;\n pointOnClosestPoly = result.point;\n }\n }\n }\n return { distance: minDistance, polygon: closestPoly, point: pointOnClosestPoly };\n };\n /**\n * Find a path from the start point to the end point using this nav mesh.\n * @param startPoint A point-like object in the form {x, y}\n * @param endPoint A point-like object in the form {x, y}\n * @returns An array of points if a path is found, or null if no path\n */\n NavMesh.prototype.findPath = function (startPoint, endPoint) {\n var startPoly = null;\n var endPoly = null;\n var startDistance = Number.MAX_VALUE;\n var endDistance = Number.MAX_VALUE;\n var d, r;\n var startVector = new Vector2(startPoint.x, startPoint.y);\n var endVector = new Vector2(endPoint.x, endPoint.y);\n // Find the closest poly for the starting and ending point\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var navPoly = _a[_i];\n r = navPoly.boundingRadius;\n // Start\n d = navPoly.centroid.distance(startVector);\n if (d <= startDistance && d <= r && navPoly.contains(startVector)) {\n startPoly = navPoly;\n startDistance = d;\n }\n // End\n d = navPoly.centroid.distance(endVector);\n if (d <= endDistance && d <= r && navPoly.contains(endVector)) {\n endPoly = navPoly;\n endDistance = d;\n }\n }\n // If the end point wasn't inside a polygon, run a more liberal check that allows a point\n // to be within meshShrinkAmount radius of a polygon\n if (!endPoly && this.meshShrinkAmount > 0) {\n for (var _b = 0, _c = this.navPolygons; _b < _c.length; _b++) {\n var navPoly = _c[_b];\n r = navPoly.boundingRadius + this.meshShrinkAmount;\n d = navPoly.centroid.distance(endVector);\n if (d <= r) {\n var distance = this.projectPointToPolygon(endVector, navPoly).distance;\n if (distance <= this.meshShrinkAmount && distance < endDistance) {\n endPoly = navPoly;\n endDistance = distance;\n }\n }\n }\n }\n // No matching polygons locations for the end, so no path found\n // because start point is valid normally, check end point first\n if (!endPoly)\n return null;\n // Same check as above, but for the start point\n if (!startPoly && this.meshShrinkAmount > 0) {\n for (var _d = 0, _e = this.navPolygons; _d < _e.length; _d++) {\n var navPoly = _e[_d];\n // Check if point is within bounding circle to avoid extra projection calculations\n r = navPoly.boundingRadius + this.meshShrinkAmount;\n d = navPoly.centroid.distance(startVector);\n if (d <= r) {\n // Check if projected point is within range of a polygon and is closer than the\n // previous point\n var distance = this.projectPointToPolygon(startVector, navPoly).distance;\n if (distance <= this.meshShrinkAmount && distance < startDistance) {\n startPoly = navPoly;\n startDistance = distance;\n }\n }\n }\n }\n // No matching polygons locations for the start, so no path found\n if (!startPoly)\n return null;\n // If the start and end polygons are the same, return a direct path\n if (startPoly === endPoly)\n return [startVector, endVector];\n // Search!\n var astarPath = new AStar().search(this.graph, startPoly, endPoly, this.graph.navHeuristic);\n // While the start and end polygons may be valid, no path between them\n if (astarPath.length === 0)\n return null;\n // jsastar drops the first point from the path, but the funnel algorithm needs it\n astarPath.unshift(startPoly);\n // We have a path, so now time for the funnel algorithm\n var channel = new Channel();\n channel.push(startVector);\n for (var i = 0; i < astarPath.length - 1; i++) {\n var navPolygon = astarPath[i];\n var nextNavPolygon = astarPath[i + 1];\n // Find the portal\n var portal = null;\n for (var i_1 = 0; i_1 < navPolygon.neighbors.length; i_1++) {\n if (navPolygon.neighbors[i_1].id === nextNavPolygon.id) {\n portal = navPolygon.portals[i_1];\n }\n }\n if (!portal)\n throw new Error(\"Path was supposed to be found, but portal is missing!\");\n // Push the portal vertices into the channel\n channel.push(portal.start, portal.end);\n }\n channel.push(endVector);\n // Pull a string along the channel to run the funnel\n channel.stringPull();\n // Clone path, excluding duplicates\n var lastPoint = null;\n var phaserPath = new Array();\n for (var _f = 0, _g = channel.path; _f < _g.length; _f++) {\n var p = _g[_f];\n var newPoint = p.clone();\n if (!lastPoint || !newPoint.equals(lastPoint))\n phaserPath.push(newPoint);\n lastPoint = newPoint;\n }\n return phaserPath;\n };\n NavMesh.prototype.calculateNeighbors = function () {\n // Fill out the neighbor information for each navpoly\n for (var i = 0; i < this.navPolygons.length; i++) {\n var navPoly = this.navPolygons[i];\n for (var j = i + 1; j < this.navPolygons.length; j++) {\n var otherNavPoly = this.navPolygons[j];\n // Check if the other navpoly is within range to touch\n var d = navPoly.centroid.distance(otherNavPoly.centroid);\n if (d > navPoly.boundingRadius + otherNavPoly.boundingRadius)\n continue;\n // The are in range, so check each edge pairing\n for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {\n var edge = _a[_i];\n for (var _b = 0, _c = otherNavPoly.edges; _b < _c.length; _b++) {\n var otherEdge = _c[_b];\n // If edges aren't collinear, not an option for connecting navpolys\n if (!areCollinear(edge, otherEdge))\n continue;\n // If they are collinear, check if they overlap\n var overlap = this.getSegmentOverlap(edge, otherEdge);\n if (!overlap)\n continue;\n // Connections are symmetric!\n navPoly.neighbors.push(otherNavPoly);\n otherNavPoly.neighbors.push(navPoly);\n // Calculate the portal between the two polygons - this needs to be in\n // counter-clockwise order, relative to each polygon\n var p1 = overlap[0], p2 = overlap[1];\n var edgeStartAngle = navPoly.centroid.angle(edge.start);\n var a1 = navPoly.centroid.angle(overlap[0]);\n var a2 = navPoly.centroid.angle(overlap[1]);\n var d1 = angleDifference(edgeStartAngle, a1);\n var d2 = angleDifference(edgeStartAngle, a2);\n if (d1 < d2) {\n navPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n else {\n navPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));\n }\n edgeStartAngle = otherNavPoly.centroid.angle(otherEdge.start);\n a1 = otherNavPoly.centroid.angle(overlap[0]);\n a2 = otherNavPoly.centroid.angle(overlap[1]);\n d1 = angleDifference(edgeStartAngle, a1);\n d2 = angleDifference(edgeStartAngle, a2);\n if (d1 < d2) {\n otherNavPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n else {\n otherNavPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));\n }\n // Two convex polygons shouldn't be connected more than once! (Unless\n // there are unnecessary vertices...)\n }\n }\n }\n }\n };\n // Check two collinear line segments to see if they overlap by sorting the points.\n // Algorithm source: http://stackoverflow.com/a/17152247\n NavMesh.prototype.getSegmentOverlap = function (line1, line2) {\n var points = [\n { line: line1, point: line1.start },\n { line: line1, point: line1.end },\n { line: line2, point: line2.start },\n { line: line2, point: line2.end },\n ];\n points.sort(function (a, b) {\n if (a.point.x < b.point.x)\n return -1;\n else if (a.point.x > b.point.x)\n return 1;\n else {\n if (a.point.y < b.point.y)\n return -1;\n else if (a.point.y > b.point.y)\n return 1;\n else\n return 0;\n }\n });\n // If the first two points in the array come from the same line, no overlap\n var noOverlap = points[0].line === points[1].line;\n // If the two middle points in the array are the same coordinates, then there is a\n // single point of overlap.\n var singlePointOverlap = points[1].point.equals(points[2].point);\n if (noOverlap || singlePointOverlap)\n return null;\n else\n return [points[1].point, points[2].point];\n };\n /**\n * Project a point onto a polygon in the shortest distance possible.\n *\n * @param {Phaser.Point} point The point to project\n * @param {NavPoly} navPoly The navigation polygon to test against\n * @returns {{point: Phaser.Point, distance: number}}\n */\n NavMesh.prototype.projectPointToPolygon = function (point, navPoly) {\n var closestProjection = null;\n var closestDistance = Number.MAX_VALUE;\n for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {\n var edge = _a[_i];\n var projectedPoint = projectPointToEdge(point, edge);\n var d = point.distance(projectedPoint);\n if (closestProjection === null || d < closestDistance) {\n closestDistance = d;\n closestProjection = projectedPoint;\n }\n }\n return { point: closestProjection, distance: closestDistance };\n };\n return NavMesh;\n}());\n\n/**\n * This implementation is strongly inspired from CritterAI class \"Geometry\".\n */\nvar Geometry = /** @class */ (function () {\n function Geometry() {\n }\n /**\n * Returns TRUE if line segment AB intersects with line segment CD in any\n * manner. Either collinear or at a single point.\n * @param ax The x-value for point (ax, ay) in line segment AB.\n * @param ay The y-value for point (ax, ay) in line segment AB.\n * @param bx The x-value for point (bx, by) in line segment AB.\n * @param by The y-value for point (bx, by) in line segment AB.\n * @param cx The x-value for point (cx, cy) in line segment CD.\n * @param cy The y-value for point (cx, cy) in line segment CD.\n * @param dx The x-value for point (dx, dy) in line segment CD.\n * @param dy The y-value for point (dx, dy) in line segment CD.\n * @return TRUE if line segment AB intersects with line segment CD in any\n * manner.\n */\n Geometry.segmentsIntersect = function (ax, ay, bx, by, cx, cy, dx, dy) {\n // This is modified 2D line-line intersection/segment-segment\n // intersection test.\n var deltaABx = bx - ax;\n var deltaABy = by - ay;\n var deltaCAx = ax - cx;\n var deltaCAy = ay - cy;\n var deltaCDx = dx - cx;\n var deltaCDy = dy - cy;\n var numerator = deltaCAy * deltaCDx - deltaCAx * deltaCDy;\n var denominator = deltaABx * deltaCDy - deltaABy * deltaCDx;\n // Perform early exit tests.\n if (denominator === 0 && numerator !== 0) {\n // If numerator is zero, then the lines are colinear.\n // Since it isn't, then the lines must be parallel.\n return false;\n }\n // Lines intersect. But do the segments intersect?\n // Forcing float division on both of these via casting of the\n // denominator.\n var factorAB = numerator / denominator;\n var factorCD = (deltaCAy * deltaABx - deltaCAx * deltaABy) / denominator;\n // Determine the type of intersection\n if (factorAB >= 0.0 &&\n factorAB <= 1.0 &&\n factorCD >= 0.0 &&\n factorCD <= 1.0) {\n return true; // The two segments intersect.\n }\n // The lines intersect, but segments to not.\n return false;\n };\n /**\n * Returns the distance squared from the point to the line segment.\n *\n * Behavior is undefined if the the closest distance is outside the\n * line segment.\n *\n * @param px The x-value of point (px, py).\n * @param py The y-value of point (px, py)\n * @param ax The x-value of the line segment's vertex A.\n * @param ay The y-value of the line segment's vertex A.\n * @param bx The x-value of the line segment's vertex B.\n * @param by The y-value of the line segment's vertex B.\n * @return The distance squared from the point (px, py) to line segment AB.\n */\n Geometry.getPointSegmentDistanceSq = function (px, py, ax, ay, bx, by) {\n // Reference: http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/\n //\n // The goal of the algorithm is to find the point on line segment AB\n // that is closest to P and then calculate the distance between P\n // and that point.\n var deltaABx = bx - ax;\n var deltaABy = by - ay;\n var deltaAPx = px - ax;\n var deltaAPy = py - ay;\n var segmentABLengthSq = deltaABx * deltaABx + deltaABy * deltaABy;\n if (segmentABLengthSq === 0) {\n // AB is not a line segment. So just return\n // distanceSq from P to A\n return deltaAPx * deltaAPx + deltaAPy * deltaAPy;\n }\n var u = (deltaAPx * deltaABx + deltaAPy * deltaABy) / segmentABLengthSq;\n if (u < 0) {\n // Closest point on line AB is outside outside segment AB and\n // closer to A. So return distanceSq from P to A.\n return deltaAPx * deltaAPx + deltaAPy * deltaAPy;\n }\n else if (u > 1) {\n // Closest point on line AB is outside segment AB and closer to B.\n // So return distanceSq from P to B.\n return (px - bx) * (px - bx) + (py - by) * (py - by);\n }\n // Closest point on lineAB is inside segment AB. So find the exact\n // point on AB and calculate the distanceSq from it to P.\n // The calculation in parenthesis is the location of the point on\n // the line segment.\n var deltaX = ax + u * deltaABx - px;\n var deltaY = ay + u * deltaABy - py;\n return deltaX * deltaX + deltaY * deltaY;\n };\n return Geometry;\n}());\n\n/**\n * A cell that holds data needed by the 1st steps of the NavMesh generation.\n */\nvar RasterizationCell = /** @class */ (function () {\n function RasterizationCell(x, y) {\n /**\n * 0 means there is an obstacle in the cell.\n * See {@link RegionGenerator}\n */\n this.distanceToObstacle = Number.MAX_VALUE;\n this.regionID = RasterizationCell.NULL_REGION_ID;\n this.distanceToRegionCore = 0;\n /**\n * If a cell is connected to one or more external regions then the\n * flag will be a 4 bit value where connections are recorded as\n * follows:\n * - bit1 = neighbor0\n * - bit2 = neighbor1\n * - bit3 = neighbor2\n * - bit4 = neighbor3\n * With the meaning of the bits as follows:\n * - 0 = neighbor in same region.\n * - 1 = neighbor not in same region (neighbor may be the obstacle\n * region or a real region).\n *\n * See {@link ContourBuilder}\n */\n this.contourFlags = 0;\n this.x = x;\n this.y = y;\n this.clear();\n }\n RasterizationCell.prototype.clear = function () {\n this.distanceToObstacle = Number.MAX_VALUE;\n this.regionID = RasterizationCell.NULL_REGION_ID;\n this.distanceToRegionCore = 0;\n this.contourFlags = 0;\n };\n /** A cell that has not been assigned to any region yet */\n RasterizationCell.NULL_REGION_ID = 0;\n /**\n * A cell that contains an obstacle.\n *\n * The value is the same as NULL_REGION_ID because the cells that are\n * not assigned to any region at the end of the flooding algorithm are\n * the obstacle cells.\n */\n RasterizationCell.OBSTACLE_REGION_ID = 0;\n return RasterizationCell;\n}());\n\nvar RasterizationGrid = /** @class */ (function () {\n function RasterizationGrid(left, top, right, bottom, cellWidth, cellHeight) {\n this.regionCount = 0;\n this.cellWidth = cellWidth;\n this.cellHeight = cellHeight;\n this.originX = left - cellWidth;\n this.originY = top - cellHeight;\n var dimX = 2 + Math.ceil((right - left) / cellWidth);\n var dimY = 2 + Math.ceil((bottom - top) / cellHeight);\n this.cells = [];\n for (var y = 0; y < dimY; y++) {\n this.cells[y] = [];\n for (var x = 0; x < dimX; x++) {\n this.cells[y][x] = new RasterizationCell(x, y);\n }\n }\n }\n RasterizationGrid.prototype.clear = function () {\n for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {\n var row = _a[_i];\n for (var _b = 0, row_1 = row; _b < row_1.length; _b++) {\n var cell = row_1[_b];\n cell.clear();\n }\n }\n this.regionCount = 0;\n };\n /**\n *\n * @param position the position on the scene\n * @param gridPosition the position on the grid\n * @returns the position on the grid\n */\n RasterizationGrid.prototype.convertToGridBasis = function (position, gridPosition) {\n gridPosition.x = (position.x - this.originX) / this.cellWidth;\n gridPosition.y = (position.y - this.originY) / this.cellHeight;\n return gridPosition;\n };\n /**\n *\n * @param gridPosition the position on the grid\n * @param position the position on the scene\n * @returns the position on the scene\n */\n RasterizationGrid.prototype.convertFromGridBasis = function (gridPosition, position) {\n position.x = gridPosition.x * this.cellWidth + this.originX;\n position.y = gridPosition.y * this.cellHeight + this.originY;\n return position;\n };\n RasterizationGrid.prototype.get = function (x, y) {\n return this.cells[y][x];\n };\n RasterizationGrid.prototype.getNeighbor = function (cell, direction) {\n var delta = RasterizationGrid.neighbor8Deltas[direction];\n return this.cells[cell.y + delta.y][cell.x + delta.x];\n };\n RasterizationGrid.prototype.dimY = function () {\n return this.cells.length;\n };\n RasterizationGrid.prototype.dimX = function () {\n var firstColumn = this.cells[0];\n return firstColumn ? firstColumn.length : 0;\n };\n RasterizationGrid.prototype.obstacleDistanceMax = function () {\n var max = 0;\n for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {\n var cellRow = _a[_i];\n for (var _b = 0, cellRow_1 = cellRow; _b < cellRow_1.length; _b++) {\n var cell = cellRow_1[_b];\n if (cell.distanceToObstacle > max) {\n max = cell.distanceToObstacle;\n }\n }\n }\n return max;\n };\n RasterizationGrid.neighbor4Deltas = [\n { x: -1, y: 0 },\n { x: 0, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: -1 },\n ];\n RasterizationGrid.neighbor8Deltas = [\n { x: -1, y: 0 },\n { x: 0, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: -1 },\n { x: 1, y: 1 },\n { x: -1, y: 1 },\n { x: -1, y: -1 },\n { x: 1, y: -1 },\n ];\n return RasterizationGrid;\n}());\n\n/**\n * Builds a set of contours from the region information contained in\n * {@link RasterizationCell}. It does this by locating and \"walking\" the edges.\n *\n * This implementation is strongly inspired from CritterAI class \"ContourSetBuilder\".\n * http://www.critterai.org/projects/nmgen_study/contourgen.html\n */\nvar ContourBuilder = /** @class */ (function () {\n function ContourBuilder() {\n // These are working lists whose content changes with each iteration\n // of the up coming loop. They represent the detailed and simple\n // contour vertices.\n // Initial sizing is arbitrary.\n this.workingRawVertices = new Array(256);\n this.workingSimplifiedVertices = new Array(64);\n }\n /**\n * Generates a contour set from the provided {@link RasterizationGrid}\n *\n * The provided field is expected to contain region information.\n * Behavior is undefined if the provided field is malformed or incomplete.\n *\n * This operation overwrites the flag fields for all cells in the\n * provided field. So the flags must be saved and restored if they are\n * important.\n *\n * @param grid A fully generated field.\n * @param threshold The maximum distance (in cells) the edge of the contour\n * may deviate from the source geometry when the rastered obstacles are\n * vectorized.\n *\n * Setting it to:\n * - 1 ensure that an aliased edge won't be split to more edges.\n * - more that 1 will reduce the number of edges but the obstacles edges\n * will be followed with less accuracy.\n * - less that 1 might be more accurate but it may try to follow the\n * aliasing and be a lot less accurate.\n *\n * Values under 1 can be useful in specific cases:\n * - when edges are horizontal or vertical, there is no aliasing so value\n * near 0 can do better results.\n * - when edges are 45° multiples, aliased vertex won't be farther than\n * sqrt(2)/2 so values over 0.71 should give good results but not\n * necessarily better than 1.\n *\n * @return The contours generated from the field.\n */\n ContourBuilder.prototype.buildContours = function (grid, threshold) {\n var contours = new Array(grid.regionCount);\n contours.length = 0;\n var contoursByRegion = new Array(grid.regionCount);\n var discardedContours = 0;\n // Set the flags on all cells in non-obstacle regions to indicate which\n // edges are connected to external regions.\n //\n // Reference: Neighbor search and nomenclature.\n // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch\n //\n // If a cell has no connections to external regions or is\n // completely surrounded by other regions (a single cell island),\n // its flag will be zero.\n //\n // If a cell is connected to one or more external regions then the\n // flag will be a 4 bit value where connections are recorded as\n // follows:\n // bit1 = neighbor0\n // bit2 = neighbor1\n // bit3 = neighbor2\n // bit4 = neighbor3\n // With the meaning of the bits as follows:\n // 0 = neighbor in same region.\n // 1 = neighbor not in same region (neighbor may be the obstacle\n // region or a real region).\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n // Note: This algorithm first sets the flag bits such that\n // 1 = \"neighbor is in the same region\". At the end it inverts\n // the bits so flags are as expected.\n // Default to \"not connected to any external region\".\n cell.contourFlags = 0;\n if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // Don't care about cells in the obstacle region.\n continue;\n for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {\n var delta = RasterizationGrid.neighbor4Deltas[direction];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (cell.regionID === neighbor.regionID) {\n // Neighbor is in same region as this cell.\n // Set the bit for this neighbor to 1 (Will be inverted later).\n cell.contourFlags |= 1 << direction;\n }\n }\n // Invert the bits so a bit value of 1 indicates neighbor NOT in\n // same region.\n cell.contourFlags ^= 0xf;\n if (cell.contourFlags === 0xf) {\n // This is an island cell (All neighbors are from other regions)\n // Get rid of flags.\n cell.contourFlags = 0;\n console.warn(\"Discarded contour: Island cell. Can't form a contour. Region: \" +\n cell.regionID);\n discardedContours++;\n }\n }\n }\n // Loop through all cells looking for cells on the edge of a region.\n //\n // At this point, only cells with flags != 0 are edge cells that\n // are part of a region contour.\n //\n // The process of building a contour will clear the flags on all cells\n // that make up the contour to ensure they are only processed once.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID ||\n cell.contourFlags === 0) {\n // cell is either: Part of the obstacle region, does not\n // represent an edge cell, or was already processed during\n // an earlier iteration.\n continue;\n }\n this.workingRawVertices.length = 0;\n this.workingSimplifiedVertices.length = 0;\n // The cell is part of an unprocessed region's contour.\n // Locate a direction of the cell's edge which points toward\n // another region (there is at least one).\n var startDirection = 0;\n while ((cell.contourFlags & (1 << startDirection)) === 0) {\n startDirection++;\n }\n // We now have a cell that is part of a contour and a direction\n // that points to a different region (obstacle or real).\n // Build the contour.\n this.buildRawContours(grid, cell, startDirection, this.workingRawVertices);\n // Perform post processing on the contour in order to\n // create the final, simplified contour.\n this.generateSimplifiedContour(cell.regionID, this.workingRawVertices, this.workingSimplifiedVertices, threshold);\n // The CritterAI implementation filters polygons with less than\n // 3 vertices, but they are needed to filter vertices in the middle\n // (not on an obstacle region border).\n var contour = Array.from(this.workingSimplifiedVertices);\n contours.push(contour);\n contoursByRegion[cell.regionID] = contour;\n }\n }\n if (contours.length + discardedContours !== grid.regionCount - 1) {\n // The only valid state is one contour per region.\n //\n // The only time this should occur is if an invalid contour\n // was formed or if a region resulted in multiple\n // contours (bad region data).\n //\n // IMPORTANT: While a mismatch may not be a fatal error,\n // it should be addressed since it can result in odd,\n // hard to spot anomalies later in the pipeline.\n //\n // A known cause is if a region fully encompasses another\n // region. In such a case, two contours will be formed.\n // The normal outer contour and an inner contour.\n // The CleanNullRegionBorders algorithm protects\n // against internal encompassed obstacle regions.\n console.error(\"Contour generation failed: Detected contours does\" +\n \" not match the number of regions. Regions: \" +\n (grid.regionCount - 1) +\n \", Detected contours: \" +\n (contours.length + discardedContours) +\n \" (Actual: \" +\n contours.length +\n \", Discarded: \" +\n discardedContours +\n \")\");\n // The CritterAI implementation has more detailed logs.\n // They can be interesting for debugging.\n }\n this.filterNonObstacleVertices(contours, contoursByRegion);\n return contours;\n };\n /**\n * Search vertices that are not shared with the obstacle region and\n * remove them.\n *\n * Some contours will have no vertex left.\n *\n * @param contours\n * @param contoursByRegion Some regions may have been discarded\n * so contours index can't be used.\n */\n ContourBuilder.prototype.filterNonObstacleVertices = function (contours, contoursByRegion) {\n // This was not part of the CritterAI implementation.\n // The removed vertex is merged on the nearest of the edges other extremity\n // that is on an obstacle border.\n var commonVertexContours = new Array(5);\n var commonVertexIndexes = new Array(5);\n // Each pass only filter vertex that have an edge other extremity on an obstacle.\n // Vertex depth (in number of edges to reach an obstacle) is reduces by\n // at least one by each pass.\n var movedAnyVertex = false;\n do {\n movedAnyVertex = false;\n for (var _i = 0, contours_1 = contours; _i < contours_1.length; _i++) {\n var contour = contours_1[_i];\n for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {\n var vertex = contour[vertexIndex];\n var nextVertex = contour[(vertexIndex + 1) % contour.length];\n if (vertex.region !== RasterizationCell.OBSTACLE_REGION_ID &&\n nextVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n // This is a vertex in the middle. It must be removed.\n // Search the contours around the vertex.\n //\n // Typically a contour point to its neighbor and it form a cycle.\n //\n // \\ C /\n // \\ /\n // A | B\n // |\n //\n // C -> B -> A -> C\n //\n // There can be more than 3 contours even if it's rare.\n commonVertexContours.length = 0;\n commonVertexIndexes.length = 0;\n commonVertexContours.push(contour);\n commonVertexIndexes.push(vertexIndex);\n var errorFound = false;\n var commonVertex = vertex;\n do {\n var neighborContour = contoursByRegion[commonVertex.region];\n if (!neighborContour) {\n errorFound = true;\n if (commonVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n console.warn(\"contour already discarded: \" + commonVertex.region);\n }\n break;\n }\n var foundVertex = false;\n for (var neighborVertexIndex = 0; neighborVertexIndex < neighborContour.length; neighborVertexIndex++) {\n var neighborVertex = neighborContour[neighborVertexIndex];\n if (neighborVertex.x === commonVertex.x &&\n neighborVertex.y === commonVertex.y) {\n commonVertexContours.push(neighborContour);\n commonVertexIndexes.push(neighborVertexIndex);\n commonVertex = neighborVertex;\n foundVertex = true;\n break;\n }\n }\n if (!foundVertex) {\n errorFound = true;\n console.error(\"Can't find a common vertex with a neighbor contour. There is probably a superposition.\");\n break;\n }\n } while (commonVertex !== vertex);\n if (errorFound) {\n continue;\n }\n if (commonVertexContours.length < 3) {\n console.error(\"The vertex is shared by only \" + commonVertexContours.length + \" regions.\");\n }\n var shorterEdgeContourIndex = -1;\n var edgeLengthMin = Number.MAX_VALUE;\n for (var index = 0; index < commonVertexContours.length; index++) {\n var vertexContour = commonVertexContours[index];\n var vertexIndex_1 = commonVertexIndexes[index];\n var previousVertex = vertexContour[(vertexIndex_1 - 1 + vertexContour.length) %\n vertexContour.length];\n if (previousVertex.region === RasterizationCell.OBSTACLE_REGION_ID) {\n var deltaX = previousVertex.x - vertex.x;\n var deltaY = previousVertex.y - vertex.y;\n var lengthSq = deltaX * deltaX + deltaY * deltaY;\n if (lengthSq < edgeLengthMin) {\n edgeLengthMin = lengthSq;\n shorterEdgeContourIndex = index;\n }\n }\n }\n if (shorterEdgeContourIndex === -1) {\n // A vertex has no neighbor on an obstacle.\n // It will be solved in next iterations.\n continue;\n }\n // Merge the vertex on the other extremity of the smallest of the 3 edges.\n //\n // \\ C /\n // \\ /\n // A | B\n // |\n //\n // - the shortest edge is between A and B\n // - the Y will become a V\n // - vertices are store clockwise\n // - there can be more than one C (it's rare)\n // This is B\n var shorterEdgeContour = commonVertexContours[shorterEdgeContourIndex];\n var shorterEdgeVertexIndex = commonVertexIndexes[shorterEdgeContourIndex];\n var shorterEdgeExtremityVertex = shorterEdgeContour[(shorterEdgeVertexIndex - 1 + shorterEdgeContour.length) %\n shorterEdgeContour.length];\n // This is A\n var shorterEdgeOtherContourIndex = (shorterEdgeContourIndex + 1) % commonVertexContours.length;\n var shorterEdgeOtherContour = commonVertexContours[shorterEdgeOtherContourIndex];\n var shorterEdgeOtherVertexIndex = commonVertexIndexes[shorterEdgeOtherContourIndex];\n for (var index = 0; index < commonVertexContours.length; index++) {\n if (index === shorterEdgeContourIndex ||\n index === shorterEdgeOtherContourIndex) {\n continue;\n }\n // These are C\n var commonVertexContour = commonVertexContours[index];\n var commonVertexIndex = commonVertexIndexes[index];\n // Move the vertex to an obstacle border\n var movedVertex = commonVertexContour[commonVertexIndex];\n movedVertex.x = shorterEdgeExtremityVertex.x;\n movedVertex.y = shorterEdgeExtremityVertex.y;\n movedVertex.region = RasterizationCell.NULL_REGION_ID;\n }\n // There is no more border between A and B,\n // update the region from B to C.\n shorterEdgeOtherContour[(shorterEdgeOtherVertexIndex + 1) % shorterEdgeOtherContour.length].region =\n shorterEdgeOtherContour[shorterEdgeOtherVertexIndex].region;\n // Remove in A and B the vertex that's been move in C.\n shorterEdgeContour.splice(shorterEdgeVertexIndex, 1);\n shorterEdgeOtherContour.splice(shorterEdgeOtherVertexIndex, 1);\n movedAnyVertex = true;\n }\n }\n }\n } while (movedAnyVertex);\n // Clean the polygons from identical vertices.\n //\n // This can happen with 2 vertices regions.\n // 2 edges are superposed and there extremity is the same.\n // One is move over the other.\n // I could observe this with a region between 2 regions\n // where one of one of these 2 regions were also encompassed.\n // A bit like a rainbow, 2 big regions: the land, the sky\n // and 2 regions for the colors.\n //\n // The vertex can't be removed during the process because\n // they hold data used by other merging.\n //\n // Some contour will have no vertex left.\n // It more efficient to let the next step ignore them.\n for (var _a = 0, contours_2 = contours; _a < contours_2.length; _a++) {\n var contour = contours_2[_a];\n for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {\n var vertex = contour[vertexIndex];\n var nextVertexIndex = (vertexIndex + 1) % contour.length;\n var nextVertex = contour[nextVertexIndex];\n if (vertex.x === nextVertex.x && vertex.y === nextVertex.y) {\n contour.splice(nextVertexIndex, 1);\n vertexIndex--;\n }\n }\n }\n };\n /**\n * Walk around the edge of this cell's region gathering vertices that\n * represent the corners of each cell on the sides that are external facing.\n *\n * There will be two or three vertices for each edge cell:\n * Two for cells that don't represent a change in edge direction. Three\n * for cells that represent a change in edge direction.\n *\n * The output array will contain vertices ordered as follows:\n * (x, y, z, regionID) where regionID is the region (obstacle or real) that\n * this vertex is considered to be connected to.\n *\n * WARNING: Only run this operation on cells that are already known\n * to be on a region edge. The direction must also be pointing to a\n * valid edge. Otherwise behavior will be undefined.\n *\n * @param grid the grid of cells\n * @param startCell A cell that is known to be on the edge of a region\n * (part of a region contour).\n * @param startDirection The direction of the edge of the cell that is\n * known to point\n * across the region edge.\n * @param outContourVertices The list of vertices that represent the edge\n * of the region.\n */\n ContourBuilder.prototype.buildRawContours = function (grid, startCell, startDirection, outContourVertices) {\n // Flaw in Algorithm:\n //\n // This method of contour generation can result in an inappropriate\n // impassable seam between two adjacent regions in the following case:\n //\n // 1. One region connects to another region on two sides in an\n // uninterrupted manner (visualize one region wrapping in an L\n // shape around the corner of another).\n // 2. At the corner shared by the two regions, a change in height\n // occurs.\n //\n // In this case, the two regions should share a corner vertex\n // (an obtuse corner vertex for one region and an acute corner\n // vertex for the other region).\n //\n // In reality, though this algorithm will select the same (x, z)\n // coordinates for each region's corner vertex, the vertex heights\n // may differ, eventually resulting in an impassable seam.\n // It is a bit hard to describe the stepping portion of this algorithm.\n // One way to visualize it is to think of a robot sitting on the\n // floor facing a known wall. It then does the following to skirt\n // the wall:\n // 1. If there is a wall in front of it, turn clockwise in 90 degrees\n // increments until it finds the wall is gone.\n // 2. Move forward one step.\n // 3. Turn counter-clockwise by 90 degrees.\n // 4. Repeat from step 1 until it finds itself at its original\n // location facing its original direction.\n //\n // See also: http://www.critterai.org/projects/nmgen_study/contourgen.html#robotwalk\n var cell = startCell;\n var direction = startDirection;\n var loopCount = 0;\n do {\n // Note: The design of this loop is such that the cell variable\n // will always reference an edge cell from the same region as\n // the start cell.\n if ((cell.contourFlags & (1 << direction)) !== 0) {\n // The current direction is pointing toward an edge.\n // Get this edge's vertex.\n var delta = ContourBuilder.leftVertexOfFacingCellBorderDeltas[direction];\n var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);\n outContourVertices.push({\n x: cell.x + delta.x,\n y: cell.y + delta.y,\n region: neighbor.regionID,\n });\n // Remove the flag for this edge. We never need to consider\n // it again since we have a vertex for this edge.\n cell.contourFlags &= ~(1 << direction);\n // Rotate in clockwise direction.\n direction = (direction + 1) & 0x3;\n }\n else {\n // The current direction does not point to an edge. So it\n // must point to a neighbor cell in the same region as the\n // current cell. Move to the neighbor and swing the search\n // direction back one increment (counterclockwise).\n // By moving the direction back one increment we guarantee we\n // don't miss any edges.\n var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);\n cell = neighbor;\n direction = (direction + 3) & 0x3; // Rotate counterclockwise.\n }\n // The loop limit is arbitrary. It exists only to guarantee that\n // bad input data doesn't result in an infinite loop.\n // The only down side of this loop limit is that it limits the\n // number of detectable edge vertices (the longer the region edge\n // and the higher the number of \"turns\" in a region's edge, the less\n // edge vertices can be detected for that region).\n } while (!(cell === startCell && direction === startDirection) &&\n ++loopCount < 65535);\n return outContourVertices;\n };\n /**\n * Takes a group of vertices that represent a region contour and changes\n * it in the following manner:\n * - For any edges that connect to non-obstacle regions, remove all\n * vertices except the start and end vertices for that edge (this\n * smooths the edges between non-obstacle regions into a straight line).\n * - Runs an algorithm's against the contour to follow the edge more closely.\n *\n * @param regionID The region the contour was derived from.\n * @param sourceVertices The source vertices that represent the complex\n * contour.\n * @param outVertices The simplified contour vertices.\n * @param threshold The maximum distance the edge of the contour may deviate\n * from the source geometry.\n */\n ContourBuilder.prototype.generateSimplifiedContour = function (regionID, sourceVertices, outVertices, threshold) {\n var noConnections = true;\n for (var _i = 0, sourceVertices_1 = sourceVertices; _i < sourceVertices_1.length; _i++) {\n var sourceVertex = sourceVertices_1[_i];\n if (sourceVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n noConnections = false;\n break;\n }\n }\n // Seed the simplified contour with the mandatory edges\n // (At least one edge).\n if (noConnections) {\n // This contour represents an island region surrounded only by the\n // obstacle region. Seed the simplified contour with the source's\n // lower left (ll) and upper right (ur) vertices.\n var lowerLeftX = sourceVertices[0].x;\n var lowerLeftY = sourceVertices[0].y;\n var lowerLeftIndex = 0;\n var upperRightX = sourceVertices[0].x;\n var upperRightY = sourceVertices[0].y;\n var upperRightIndex = 0;\n for (var index = 0; index < sourceVertices.length; index++) {\n var sourceVertex = sourceVertices[index];\n var x = sourceVertex.x;\n var y = sourceVertex.y;\n if (x < lowerLeftX || (x === lowerLeftX && y < lowerLeftY)) {\n lowerLeftX = x;\n lowerLeftY = y;\n lowerLeftIndex = index;\n }\n if (x >= upperRightX || (x === upperRightX && y > upperRightY)) {\n upperRightX = x;\n upperRightY = y;\n upperRightIndex = index;\n }\n }\n // The region attribute is used to store an index locally in this function.\n // TODO Maybe there is a way to do this cleanly and keep no memory footprint.\n // Seed the simplified contour with this edge.\n outVertices.push({\n x: lowerLeftX,\n y: lowerLeftY,\n region: lowerLeftIndex,\n });\n outVertices.push({\n x: upperRightX,\n y: upperRightY,\n region: upperRightIndex,\n });\n }\n else {\n // The contour shares edges with other non-obstacle regions.\n // Seed the simplified contour with a new vertex for every\n // location where the region connection changes. These are\n // vertices that are important because they represent portals\n // to other regions.\n for (var index = 0; index < sourceVertices.length; index++) {\n var sourceVert = sourceVertices[index];\n if (sourceVert.region !==\n sourceVertices[(index + 1) % sourceVertices.length].region) {\n // The current vertex has a different region than the\n // next vertex. So there is a change in vertex region.\n outVertices.push({\n x: sourceVert.x,\n y: sourceVert.y,\n region: index,\n });\n }\n }\n }\n this.matchObstacleRegionEdges(sourceVertices, outVertices, threshold);\n if (outVertices.length < 2) {\n // It will be ignored by the triangulation.\n // It should be rare enough not to handle it now.\n console.warn(\"A region is encompassed in another region. It will be ignored.\");\n }\n // There can be polygons with only 2 vertices when a region is between\n // 2 non-obstacles regions. It's still a useful information to filter\n // vertices in the middle (not on an obstacle region border).\n // In this case, the CritterAI implementation adds a 3rd point to avoid\n // invisible polygons, but it makes it difficult to filter it later.\n // Replace the index pointers in the output list with region IDs.\n for (var _a = 0, outVertices_1 = outVertices; _a < outVertices_1.length; _a++) {\n var outVertex = outVertices_1[_a];\n outVertex.region = sourceVertices[outVertex.region].region;\n }\n };\n /**\n * Applies an algorithm to contours which results in obstacle-region edges\n * following the original detail source geometry edge more closely.\n * http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple\n *\n * Adds vertices from the source list to the result list such that\n * if any obstacle region vertices are compared against the result list,\n * none of the vertices will be further from the obstacle region edges than\n * the allowed threshold.\n *\n * Only obstacle-region edges are operated on. All other edges are\n * ignored.\n *\n * The result vertices is expected to be seeded with at least two\n * source vertices.\n *\n * @param sourceVertices\n * @param inoutResultVertices\n * @param threshold The maximum distance the edge of the contour may deviate\n * from the source geometry.\n */\n ContourBuilder.prototype.matchObstacleRegionEdges = function (sourceVertices, inoutResultVertices, threshold) {\n // This implementation is strongly inspired from CritterAI class \"MatchNullRegionEdges\".\n // Loop through all edges in this contour.\n //\n // NOTE: The simplifiedVertCount in the loop condition\n // increases over iterations. That is what keeps the loop going beyond\n // the initial vertex count.\n var resultIndexA = 0;\n while (resultIndexA < inoutResultVertices.length) {\n var resultIndexB = (resultIndexA + 1) % inoutResultVertices.length;\n // The line segment's beginning vertex.\n var ax = inoutResultVertices[resultIndexA].x;\n var az = inoutResultVertices[resultIndexA].y;\n var sourceIndexA = inoutResultVertices[resultIndexA].region;\n // The line segment's ending vertex.\n var bx = inoutResultVertices[resultIndexB].x;\n var bz = inoutResultVertices[resultIndexB].y;\n var sourceIndexB = inoutResultVertices[resultIndexB].region;\n // The source index of the next vertex to test (the vertex just\n // after the current vertex in the source vertex list).\n var testedSourceIndex = (sourceIndexA + 1) % sourceVertices.length;\n var maxDeviation = 0;\n // Default to no index. No new vert to add.\n var toInsertSourceIndex = -1;\n if (sourceVertices[testedSourceIndex].region ===\n RasterizationCell.OBSTACLE_REGION_ID) {\n // This test vertex is part of a obstacle region edge.\n // Loop through the source vertices until the end vertex\n // is found, searching for the vertex that is farthest from\n // the line segment formed by the begin/end vertices.\n //\n // Visualizations:\n // http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple\n while (testedSourceIndex !== sourceIndexB) {\n var deviation = Geometry.getPointSegmentDistanceSq(sourceVertices[testedSourceIndex].x, sourceVertices[testedSourceIndex].y, ax, az, bx, bz);\n if (deviation > maxDeviation) {\n // A new maximum deviation was detected.\n maxDeviation = deviation;\n toInsertSourceIndex = testedSourceIndex;\n }\n // Move to the next vertex.\n testedSourceIndex = (testedSourceIndex + 1) % sourceVertices.length;\n }\n }\n if (toInsertSourceIndex !== -1 && maxDeviation > threshold * threshold) {\n // A vertex was found that is further than allowed from the\n // current edge. Add this vertex to the contour.\n inoutResultVertices.splice(resultIndexA + 1, 0, {\n x: sourceVertices[toInsertSourceIndex].x,\n y: sourceVertices[toInsertSourceIndex].y,\n region: toInsertSourceIndex,\n });\n // Not incrementing the vertex since we need to test the edge\n // formed by vertA and this this new vertex on the next\n // iteration of the loop.\n }\n // This edge segment does not need to be altered. Move to\n // the next vertex.\n else\n resultIndexA++;\n }\n };\n ContourBuilder.leftVertexOfFacingCellBorderDeltas = [\n { x: 0, y: 1 },\n { x: 1, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: 0 },\n ];\n return ContourBuilder;\n}());\n\n/**\n * Builds convex polygons from the provided polygons.\n *\n * This implementation is strongly inspired from CritterAI class \"PolyMeshFieldBuilder\".\n * http://www.critterai.org/projects/nmgen_study/polygen.html\n */\nvar ConvexPolygonGenerator = /** @class */ (function () {\n function ConvexPolygonGenerator() {\n }\n /**\n * Builds convex polygons from the provided polygons.\n * @param concavePolygons The content is manipulated during the operation\n * and it will be left in an undefined state at the end of\n * the operation.\n * @param maxVerticesPerPolygon cap the vertex number in return polygons.\n * @return convex polygons.\n */\n ConvexPolygonGenerator.prototype.splitToConvexPolygons = function (concavePolygons, maxVerticesPerPolygon) {\n // The maximum possible number of polygons assuming that all will\n // be triangles.\n var maxPossiblePolygons = 0;\n // The maximum vertices found in a single contour.\n var maxVerticesPerContour = 0;\n for (var _i = 0, concavePolygons_1 = concavePolygons; _i < concavePolygons_1.length; _i++) {\n var contour = concavePolygons_1[_i];\n var count = contour.length;\n maxPossiblePolygons += count - 2;\n maxVerticesPerContour = Math.max(maxVerticesPerContour, count);\n }\n // Each list is initialized to a size that will minimize resizing.\n var convexPolygons = new Array(maxPossiblePolygons);\n convexPolygons.length = 0;\n // Various working variables.\n // (Values are meaningless outside of the iteration)\n var workingContourFlags = new Array(maxVerticesPerContour);\n workingContourFlags.length = 0;\n var workingPolygons = new Array(maxVerticesPerContour + 1);\n workingPolygons.length = 0;\n var workingMergeInfo = {\n lengthSq: -1,\n polygonAVertexIndex: -1,\n polygonBVertexIndex: -1,\n };\n var workingMergedPolygon = new Array(maxVerticesPerPolygon);\n workingMergedPolygon.length = 0;\n var _loop_1 = function (contour) {\n if (contour.length < 3) {\n return \"continue\";\n }\n // Initialize the working polygon array.\n workingPolygons.length = 0;\n // Triangulate the contour.\n var foundAnyTriangle = false;\n this_1.triangulate(contour, workingContourFlags, function (p1, p2, p3) {\n var workingPolygon = new Array(maxVerticesPerPolygon);\n workingPolygon.length = 0;\n workingPolygon.push(p1);\n workingPolygon.push(p2);\n workingPolygon.push(p3);\n workingPolygons.push(workingPolygon);\n foundAnyTriangle = true;\n });\n if (!foundAnyTriangle) {\n /*\n * Failure of the triangulation.\n * This is known to occur if the source polygon is\n * self-intersecting or the source region contains internal\n * holes. In both cases, the problem is likely due to bad\n * region formation.\n */\n console.error(\"Polygon generation failure: Could not triangulate contour.\");\n console.error(\"contour:\" +\n contour.map(function (point) { return point.x + \" \" + point.y; }).join(\" ; \"));\n return \"continue\";\n }\n if (maxVerticesPerPolygon > 3) {\n // Merging of triangles into larger polygons is permitted.\n // Continue until no polygons can be found to merge.\n // http://www.critterai.org/nmgen_polygen#mergepolys\n while (true) {\n var longestMergeEdge = -1;\n var bestPolygonA = [];\n var polygonAVertexIndex = -1; // Start of the shared edge.\n var bestPolygonB = [];\n var polygonBVertexIndex = -1; // Start of the shared edge.\n var bestPolygonBIndex = -1;\n // Loop through all but the last polygon looking for the\n // best polygons to merge in this iteration.\n for (var indexA = 0; indexA < workingPolygons.length - 1; indexA++) {\n var polygonA = workingPolygons[indexA];\n for (var indexB = indexA + 1; indexB < workingPolygons.length; indexB++) {\n var polygonB = workingPolygons[indexB];\n // Can polyB merge with polyA?\n this_1.getPolyMergeInfo(polygonA, polygonB, maxVerticesPerPolygon, workingMergeInfo);\n if (workingMergeInfo.lengthSq > longestMergeEdge) {\n // polyB has the longest shared edge with\n // polyA found so far. Save the merge\n // information.\n longestMergeEdge = workingMergeInfo.lengthSq;\n bestPolygonA = polygonA;\n polygonAVertexIndex = workingMergeInfo.polygonAVertexIndex;\n bestPolygonB = polygonB;\n polygonBVertexIndex = workingMergeInfo.polygonBVertexIndex;\n bestPolygonBIndex = indexB;\n }\n }\n }\n if (longestMergeEdge <= 0)\n // No valid merges found during this iteration.\n break;\n // Found polygons to merge. Perform the merge.\n /*\n * Fill the mergedPoly array.\n * Start the vertex at the end of polygon A's shared edge.\n * Add all vertices until looping back to the vertex just\n * before the start of the shared edge. Repeat for\n * polygon B.\n *\n * Duplicate vertices are avoided, while ensuring we get\n * all vertices, since each loop drops the vertex that\n * starts its polygon's shared edge and:\n *\n * PolyAStartVert == PolyBEndVert and\n * PolyAEndVert == PolyBStartVert.\n */\n var vertCountA = bestPolygonA.length;\n var vertCountB = bestPolygonB.length;\n workingMergedPolygon.length = 0;\n for (var i = 0; i < vertCountA - 1; i++)\n workingMergedPolygon.push(bestPolygonA[(polygonAVertexIndex + 1 + i) % vertCountA]);\n for (var i = 0; i < vertCountB - 1; i++)\n workingMergedPolygon.push(bestPolygonB[(polygonBVertexIndex + 1 + i) % vertCountB]);\n // Copy the merged polygon over the top of polygon A.\n bestPolygonA.length = 0;\n Array.prototype.push.apply(bestPolygonA, workingMergedPolygon);\n // Remove polygon B\n workingPolygons.splice(bestPolygonBIndex, 1);\n }\n }\n // Polygon creation for this contour is complete.\n // Add polygons to the global polygon array\n Array.prototype.push.apply(convexPolygons, workingPolygons);\n };\n var this_1 = this;\n // Split every concave polygon into convex polygons.\n for (var _a = 0, concavePolygons_2 = concavePolygons; _a < concavePolygons_2.length; _a++) {\n var contour = concavePolygons_2[_a];\n _loop_1(contour);\n }\n // The original implementation builds polygon adjacency information.\n // but the library for the pathfinding already does it.\n return convexPolygons;\n };\n /**\n * Checks two polygons to see if they can be merged. If a merge is\n * allowed, provides data via the outResult argument (see {@link PolyMergeResult}).\n *\n * @param polygonA The polygon A\n * @param polygonB The polygon B\n * @param maxVerticesPerPolygon cap the vertex number in return polygons.\n * @param outResult contains merge information.\n */\n ConvexPolygonGenerator.prototype.getPolyMergeInfo = function (polygonA, polygonB, maxVerticesPerPolygon, outResult) {\n outResult.lengthSq = -1; // Default to invalid merge\n outResult.polygonAVertexIndex = -1;\n outResult.polygonBVertexIndex = -1;\n var vertexCountA = polygonA.length;\n var vertexCountB = polygonB.length;\n // If the merged polygon would would have to many vertices, do not\n // merge. Subtracting two since to take into account the effect of\n // a merge.\n if (vertexCountA + vertexCountB - 2 > maxVerticesPerPolygon)\n return;\n // Check if the polygons share an edge.\n for (var indexA = 0; indexA < vertexCountA; indexA++) {\n // Get the vertex indices for the polygonA edge\n var vertexA = polygonA[indexA];\n var nextVertexA = polygonA[(indexA + 1) % vertexCountA];\n // Search polygonB for matches.\n for (var indexB = 0; indexB < vertexCountB; indexB++) {\n // Get the vertex indices for the polygonB edge.\n var vertexB = polygonB[indexB];\n var nextVertexB = polygonB[(indexB + 1) % vertexCountB];\n // === can be used because vertices comme from the same concave polygon.\n if (vertexA === nextVertexB && nextVertexA === vertexB) {\n // The vertex indices for this edge are the same and\n // sequenced in opposite order. So the edge is shared.\n outResult.polygonAVertexIndex = indexA;\n outResult.polygonBVertexIndex = indexB;\n }\n }\n }\n if (outResult.polygonAVertexIndex === -1)\n // No common edge, cannot merge.\n return;\n // Check to see if the merged polygon would be convex.\n //\n // Gets the vertices near the section where the merge would occur.\n // Do they form a concave section? If so, the merge is invalid.\n //\n // Note that the following algorithm is only valid for clockwise\n // wrapped convex polygons.\n var sharedVertMinus = polygonA[(outResult.polygonAVertexIndex - 1 + vertexCountA) % vertexCountA];\n var sharedVert = polygonA[outResult.polygonAVertexIndex];\n var sharedVertPlus = polygonB[(outResult.polygonBVertexIndex + 2) % vertexCountB];\n if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {\n // The shared vertex (center) is not to the left of segment\n // vertMinus->vertPlus. For a clockwise wrapped polygon, this\n // indicates a concave section. Merged polygon would be concave.\n // Invalid merge.\n return;\n }\n sharedVertMinus =\n polygonB[(outResult.polygonBVertexIndex - 1 + vertexCountB) % vertexCountB];\n sharedVert = polygonB[outResult.polygonBVertexIndex];\n sharedVertPlus =\n polygonA[(outResult.polygonAVertexIndex + 2) % vertexCountA];\n if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {\n // The shared vertex (center) is not to the left of segment\n // vertMinus->vertPlus. For a clockwise wrapped polygon, this\n // indicates a concave section. Merged polygon would be concave.\n // Invalid merge.\n return;\n }\n // Get the vertex indices that form the shared edge.\n sharedVertMinus = polygonA[outResult.polygonAVertexIndex];\n sharedVert = polygonA[(outResult.polygonAVertexIndex + 1) % vertexCountA];\n // Store the lengthSq of the shared edge.\n var deltaX = sharedVertMinus.x - sharedVert.x;\n var deltaZ = sharedVertMinus.y - sharedVert.y;\n outResult.lengthSq = deltaX * deltaX + deltaZ * deltaZ;\n };\n /**\n * Attempts to triangulate a polygon.\n *\n * @param vertices the polygon to be triangulate.\n * The content is manipulated during the operation\n * and it will be left in an undefined state at the end of\n * the operation.\n * @param vertexFlags only used internally\n * @param outTriangles is called for each triangle derived\n * from the original polygon.\n * @return The number of triangles generated. Or, if triangulation\n * failed, a negative number.\n */\n ConvexPolygonGenerator.prototype.triangulate = function (vertices, vertexFlags, outTriangles) {\n // Terminology, concepts and such:\n //\n // This algorithm loops around the edges of a polygon looking for\n // new internal edges to add that will partition the polygon into a\n // new valid triangle internal to the starting polygon. During each\n // iteration the shortest potential new edge is selected to form that\n // iteration's new triangle.\n //\n // Triangles will only be formed if a single new edge will create\n // a triangle. Two new edges will never be added during a single\n // iteration. This means that the triangulated portions of the\n // original polygon will only contain triangles and the only\n // non-triangle polygon will exist in the untriangulated portion\n // of the original polygon.\n //\n // \"Partition edge\" refers to a potential new edge that will form a\n // new valid triangle.\n //\n // \"Center\" vertex refers to the vertex in a potential new triangle\n // which, if the triangle is formed, will be external to the\n // remaining untriangulated portion of the polygon. Since it\n // is now external to the polygon, it can't be used to form any\n // new triangles.\n //\n // Some documentation refers to \"iPlus2\" even though the variable is\n // not in scope or does not exist for that section of code. For\n // documentation purposes, iPlus2 refers to the 2nd vertex after the\n // primary vertex.\n // E.g.: i, iPlus1, and iPlus2.\n //\n // Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#triangulation\n // Loop through all vertices, flagging all indices that represent\n // a center vertex of a valid new triangle.\n vertexFlags.length = vertices.length;\n for (var i = 0; i < vertices.length; i++) {\n var iPlus1 = (i + 1) % vertices.length;\n var iPlus2 = (i + 2) % vertices.length;\n // A triangle formed by i, iPlus1, and iPlus2 will result\n // in a valid internal triangle.\n // Flag the center vertex (iPlus1) to indicate a valid triangle\n // location.\n vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, iPlus2, vertices);\n }\n // Loop through the vertices creating triangles. When there is only a\n // single triangle left, the operation is complete.\n //\n // When a valid triangle is formed, remove its center vertex. So for\n // each loop, a single vertex will be removed.\n //\n // At the start of each iteration the indices list is in the following\n // state:\n // - Represents a simple polygon representing the un-triangulated\n // portion of the original polygon.\n // - All valid center vertices are flagged.\n while (vertices.length > 3) {\n // Find the shortest new valid edge.\n // NOTE: i and iPlus1 are defined in two different scopes in\n // this section. So be careful.\n // Loop through all indices in the remaining polygon.\n var minLengthSq = Number.MAX_VALUE;\n var minLengthSqVertexIndex = -1;\n for (var i_1 = 0; i_1 < vertices.length; i_1++) {\n if (vertexFlags[(i_1 + 1) % vertices.length]) {\n // Indices i, iPlus1, and iPlus2 are known to form a\n // valid triangle.\n var vert = vertices[i_1];\n var vertPlus2 = vertices[(i_1 + 2) % vertices.length];\n // Determine the length of the partition edge.\n // (i -> iPlus2)\n var deltaX = vertPlus2.x - vert.x;\n var deltaY = vertPlus2.y - vert.y;\n var lengthSq = deltaX * deltaX + deltaY * deltaY;\n if (lengthSq < minLengthSq) {\n minLengthSq = lengthSq;\n minLengthSqVertexIndex = i_1;\n }\n }\n }\n if (minLengthSqVertexIndex === -1)\n // Could not find a new triangle. Triangulation failed.\n // This happens if there are three or more vertices\n // left, but none of them are flagged as being a\n // potential center vertex.\n return;\n var i = minLengthSqVertexIndex;\n var iPlus1 = (i + 1) % vertices.length;\n // Add the new triangle to the output.\n outTriangles(vertices[i], vertices[iPlus1], vertices[(i + 2) % vertices.length]);\n // iPlus1, the \"center\" vert in the new triangle, is now external\n // to the untriangulated portion of the polygon. Remove it from\n // the vertices list since it cannot be a member of any new\n // triangles.\n vertices.splice(iPlus1, 1);\n vertexFlags.splice(iPlus1, 1);\n if (iPlus1 === 0 || iPlus1 >= vertices.length) {\n // The vertex removal has invalidated iPlus1 and/or i. So\n // force a wrap, fixing the indices so they reference the\n // correct indices again. This only occurs when the new\n // triangle is formed across the wrap location of the polygon.\n // Case 1: i = 14, iPlus1 = 15, iPlus2 = 0\n // Case 2: i = 15, iPlus1 = 0, iPlus2 = 1;\n i = vertices.length - 1;\n iPlus1 = 0;\n }\n // At this point i and iPlus1 refer to the two indices from a\n // successful triangulation that will be part of another new\n // triangle. We now need to re-check these indices to see if they\n // can now be the center index in a potential new partition.\n vertexFlags[i] = ConvexPolygonGenerator.isValidPartition((i - 1 + vertices.length) % vertices.length, iPlus1, vertices);\n vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, (i + 2) % vertices.length, vertices);\n }\n // Only 3 vertices remain.\n // Add their triangle to the output list.\n outTriangles(vertices[0], vertices[1], vertices[2]);\n };\n /**\n * Check if the line segment formed by vertex A and vertex B will\n * form a valid partition of the polygon.\n *\n * I.e. the line segment AB is internal to the polygon and will not\n * cross existing line segments.\n *\n * Assumptions:\n * - The vertices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * - indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if the line segment formed by vertex A and vertex B will\n * form a valid partition of the polygon.\n */\n ConvexPolygonGenerator.isValidPartition = function (indexA, indexB, vertices) {\n // First check whether the segment AB lies within the internal\n // angle formed at A (this is the faster check).\n // If it does, then perform the more costly check.\n return (ConvexPolygonGenerator.liesWithinInternalAngle(indexA, indexB, vertices) &&\n !ConvexPolygonGenerator.hasIllegalEdgeIntersection(indexA, indexB, vertices));\n };\n /**\n * Check if vertex B lies within the internal angle of the polygon\n * at vertex A.\n *\n * Vertex B does not have to be within the polygon border. It just has\n * be be within the area encompassed by the internal angle formed at\n * vertex A.\n *\n * This operation is a fast way of determining whether a line segment\n * can possibly form a valid polygon partition. If this test returns\n * FALSE, then more expensive checks can be skipped.\n *\n * Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#anglecheck\n *\n * Special case:\n * FALSE is returned if vertex B lies directly on either of the rays\n * cast from vertex A along its associated polygon edges. So the test\n * on vertex B is exclusive of the polygon edges.\n *\n * Assumptions:\n * - The vertices and indices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * -indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if vertex B lies within the internal angle of\n * the polygon at vertex A.\n */\n ConvexPolygonGenerator.liesWithinInternalAngle = function (indexA, indexB, vertices) {\n // Get pointers to the main vertices being tested.\n var vertexA = vertices[indexA];\n var vertexB = vertices[indexB];\n // Get pointers to the vertices just before and just after vertA.\n var vertexAMinus = vertices[(indexA - 1 + vertices.length) % vertices.length];\n var vertexAPlus = vertices[(indexA + 1) % vertices.length];\n // First, find which of the two angles formed by the line segments\n // AMinus->A->APlus is internal to (pointing towards) the polygon.\n // Then test to see if B lies within the area formed by that angle.\n // TRUE if A is left of or on line AMinus->APlus\n if (ConvexPolygonGenerator.isLeftOrCollinear(vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y, vertexAPlus.x, vertexAPlus.y))\n // The angle internal to the polygon is <= 180 degrees\n // (non-reflex angle).\n // Test to see if B lies within this angle.\n return (ConvexPolygonGenerator.isLeft(\n // TRUE if B is left of line A->AMinus\n vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y) &&\n // TRUE if B is right of line A->APlus\n ConvexPolygonGenerator.isRight(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y));\n // The angle internal to the polygon is > 180 degrees (reflex angle).\n // Test to see if B lies within the external (<= 180 degree) angle and\n // flip the result. (If B lies within the external angle, it can't\n // lie within the internal angle)\n return !(\n // TRUE if B is left of or on line A->APlus\n (ConvexPolygonGenerator.isLeftOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y) &&\n // TRUE if B is right of or on line A->AMinus\n ConvexPolygonGenerator.isRightOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y)));\n };\n /**\n * Check if the line segment AB intersects any edges not already\n * connected to one of the two vertices.\n *\n * Assumptions:\n * - The vertices and indices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * - indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if the line segment AB intersects any edges not already\n * connected to one of the two vertices.\n */\n ConvexPolygonGenerator.hasIllegalEdgeIntersection = function (indexA, indexB, vertices) {\n // Get pointers to the primary vertices being tested.\n var vertexA = vertices[indexA];\n var vertexB = vertices[indexB];\n // Loop through the polygon edges.\n for (var edgeBeginIndex = 0; edgeBeginIndex < vertices.length; edgeBeginIndex++) {\n var edgeEndIndex = (edgeBeginIndex + 1) % vertices.length;\n if (edgeBeginIndex === indexA ||\n edgeBeginIndex === indexB ||\n edgeEndIndex === indexA ||\n edgeEndIndex === indexB) {\n continue;\n }\n // Neither of the test indices are endpoints of this edge.\n // Get this edge's vertices.\n var edgeBegin = vertices[edgeBeginIndex];\n var edgeEnd = vertices[edgeEndIndex];\n if ((edgeBegin.x === vertexA.x && edgeBegin.y === vertexA.y) ||\n (edgeBegin.x === vertexB.x && edgeBegin.y === vertexB.y) ||\n (edgeEnd.x === vertexA.x && edgeEnd.y === vertexA.y) ||\n (edgeEnd.x === vertexB.x && edgeEnd.y === vertexB.y)) {\n // One of the test vertices is co-located\n // with one of the endpoints of this edge (this is a\n // test of the actual position of the vertices rather than\n // simply the index check performed earlier).\n // Skip this edge.\n continue;\n }\n // This edge is not connected to either of the test vertices.\n // If line segment AB intersects with this edge, then the\n // intersection is illegal.\n // I.e. New edges cannot cross existing edges.\n if (Geometry.segmentsIntersect(vertexA.x, vertexA.y, vertexB.x, vertexB.y, edgeBegin.x, edgeBegin.y, edgeEnd.x, edgeEnd.y)) {\n return true;\n }\n }\n return false;\n };\n /**\n * Check if point P is to the left of line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the left of line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isLeft = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) < 0;\n };\n /**\n * Check if point P is to the left of line AB when looking\n * from A to B or is collinear with line AB.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the left of line AB when looking\n * from A to B, or is collinear with line AB.\n */\n ConvexPolygonGenerator.isLeftOrCollinear = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) <= 0;\n };\n /**\n * Check if point P is to the right of line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the right of line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isRight = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) > 0;\n };\n /**\n * Check if point P is to the right of or on line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the right of or on line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isRightOrCollinear = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) >= 0;\n };\n /**\n * The absolute value of the returned value is two times the area of the\n * triangle defined by points (A, B, C).\n *\n * A positive value indicates:\n * - Counterclockwise wrapping of the points.\n * - Point B lies to the right of line AC, looking from A to C.\n *\n * A negative value indicates:\n * - Clockwise wrapping of the points.<\n * - Point B lies to the left of line AC, looking from A to C.\n *\n * A value of zero indicates that all points are collinear or\n * represent the same point.\n *\n * This is a fast operation.\n *\n * @param ax The x-value for point (ax, ay) for vertex A of the triangle.\n * @param ay The y-value for point (ax, ay) for vertex A of the triangle.\n * @param bx The x-value for point (bx, by) for vertex B of the triangle.\n * @param by The y-value for point (bx, by) for vertex B of the triangle.\n * @param cx The x-value for point (cx, cy) for vertex C of the triangle.\n * @param cy The y-value for point (cx, cy) for vertex C of the triangle.\n * @return The signed value of two times the area of the triangle defined\n * by the points (A, B, C).\n */\n ConvexPolygonGenerator.getSignedAreaX2 = function (ax, ay, bx, by, cx, cy) {\n // References:\n // http://softsurfer.com/Archive/algorithm_0101/algorithm_0101.htm#Modern%20Triangles\n // http://mathworld.wolfram.com/TriangleArea.html (Search for \"signed\")\n return (bx - ax) * (cy - ay) - (cx - ax) * (by - ay);\n };\n return ConvexPolygonGenerator;\n}());\n\nvar GridCoordinateConverter = /** @class */ (function () {\n function GridCoordinateConverter() {\n }\n /**\n *\n * @param gridPosition the position on the grid\n * @param position the position on the scene\n * @param scaleY for isometry\n * @returns the position on the scene\n */\n GridCoordinateConverter.prototype.convertFromGridBasis = function (grid, polygons) {\n // point can be shared so them must be copied to be scaled.\n return polygons.map(function (polygon) {\n return polygon.map(function (point) { return grid.convertFromGridBasis(point, { x: 0, y: 0 }); });\n });\n };\n return GridCoordinateConverter;\n}());\n\n/**\n * It rasterizes obstacles on a grid.\n *\n * It flags cells as obstacle to be used by {@link RegionGenerator}.\n */\nvar ObstacleRasterizer = /** @class */ (function () {\n function ObstacleRasterizer() {\n this.workingNodes = new Array(8);\n this.gridBasisIterable = new GridBasisIterable();\n }\n /**\n * Rasterize obstacles on a grid.\n * @param grid\n * @param obstacles\n */\n ObstacleRasterizer.prototype.rasterizeObstacles = function (grid, obstacles) {\n var obstaclesItr = obstacles[Symbol.iterator]();\n for (var next = obstaclesItr.next(); !next.done; next = obstaclesItr.next()) {\n var obstacle = next.value;\n this.gridBasisIterable.set(grid, obstacle);\n var vertices = this.gridBasisIterable;\n var minX = Number.MAX_VALUE;\n var maxX = -Number.MAX_VALUE;\n var minY = Number.MAX_VALUE;\n var maxY = -Number.MAX_VALUE;\n var verticesItr = vertices[Symbol.iterator]();\n for (var next_1 = verticesItr.next(); !next_1.done; next_1 = verticesItr.next()) {\n var vertex = next_1.value;\n minX = Math.min(minX, vertex.x);\n maxX = Math.max(maxX, vertex.x);\n minY = Math.min(minY, vertex.y);\n maxY = Math.max(maxY, vertex.y);\n }\n minX = Math.max(Math.floor(minX), 0);\n maxX = Math.min(Math.ceil(maxX), grid.dimX());\n minY = Math.max(Math.floor(minY), 0);\n maxY = Math.min(Math.ceil(maxY), grid.dimY());\n this.fillPolygon(vertices, minX, maxX, minY, maxY, function (x, y) { return (grid.get(x, y).distanceToObstacle = 0); });\n }\n };\n ObstacleRasterizer.prototype.fillPolygon = function (vertices, minX, maxX, minY, maxY, fill) {\n // The following implementation of the scan-line polygon fill algorithm\n // is strongly inspired from:\n // https://alienryderflex.com/polygon_fill/\n // The original implementation was under this license:\n // public-domain code by Darel Rex Finley, 2007\n // This implementation differ with the following:\n // - it handles float vertices\n // so it focus on pixels center\n // - it is conservative to thin vertical or horizontal polygons\n var fillAnyPixels = false;\n this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {\n for (var pixelX = minX; pixelX < maxX; pixelX++) {\n fillAnyPixels = true;\n fill(pixelX, pixelY);\n }\n });\n if (fillAnyPixels) {\n return;\n }\n this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {\n // conserve thin (less than one cell large) horizontal polygons\n if (minX === maxX) {\n fill(minX, pixelY);\n }\n });\n this.scanX(vertices, minX, maxX, minY, maxY, function (pixelX, minY, maxY) {\n for (var pixelY = minY; pixelY < maxY; pixelY++) {\n fill(pixelX, pixelY);\n }\n // conserve thin (less than one cell large) vertical polygons\n if (minY === maxY) {\n fill(pixelX, minY);\n }\n });\n };\n ObstacleRasterizer.prototype.scanY = function (vertices, minX, maxX, minY, maxY, checkAndFillY) {\n var workingNodes = this.workingNodes;\n // Loop through the rows of the image.\n for (var pixelY = minY; pixelY < maxY; pixelY++) {\n var pixelCenterY = pixelY + 0.5;\n // Build a list of nodes.\n workingNodes.length = 0;\n //let j = vertices.length - 1;\n var verticesItr = vertices[Symbol.iterator]();\n var next = verticesItr.next();\n var vertex = next.value;\n // The iterator always return the same instance.\n // It must be copied to be save for later.\n var firstVertexX = vertex.x;\n var firstVertexY = vertex.y;\n while (!next.done) {\n var previousVertexX = vertex.x;\n var previousVertexY = vertex.y;\n next = verticesItr.next();\n if (next.done) {\n vertex.x = firstVertexX;\n vertex.y = firstVertexY;\n }\n else {\n vertex = next.value;\n }\n if ((vertex.y <= pixelCenterY && pixelCenterY < previousVertexY) ||\n (previousVertexY < pixelCenterY && pixelCenterY <= vertex.y)) {\n workingNodes.push(Math.round(vertex.x +\n ((pixelCenterY - vertex.y) / (previousVertexY - vertex.y)) *\n (previousVertexX - vertex.x)));\n }\n }\n // Sort the nodes, via a simple “Bubble” sort.\n {\n var i = 0;\n while (i < workingNodes.length - 1) {\n if (workingNodes[i] > workingNodes[i + 1]) {\n var swap = workingNodes[i];\n workingNodes[i] = workingNodes[i + 1];\n workingNodes[i + 1] = swap;\n if (i > 0)\n i--;\n }\n else {\n i++;\n }\n }\n }\n // Fill the pixels between node pairs.\n for (var i = 0; i < workingNodes.length; i += 2) {\n if (workingNodes[i] >= maxX) {\n break;\n }\n if (workingNodes[i + 1] <= minX) {\n continue;\n }\n if (workingNodes[i] < minX) {\n workingNodes[i] = minX;\n }\n if (workingNodes[i + 1] > maxX) {\n workingNodes[i + 1] = maxX;\n }\n checkAndFillY(pixelY, workingNodes[i], workingNodes[i + 1]);\n }\n }\n };\n ObstacleRasterizer.prototype.scanX = function (vertices, minX, maxX, minY, maxY, checkAndFillX) {\n var workingNodes = this.workingNodes;\n // Loop through the columns of the image.\n for (var pixelX = minX; pixelX < maxX; pixelX++) {\n var pixelCenterX = pixelX + 0.5;\n // Build a list of nodes.\n workingNodes.length = 0;\n var verticesItr = vertices[Symbol.iterator]();\n var next = verticesItr.next();\n var vertex = next.value;\n // The iterator always return the same instance.\n // It must be copied to be save for later.\n var firstVertexX = vertex.x;\n var firstVertexY = vertex.y;\n while (!next.done) {\n var previousVertexX = vertex.x;\n var previousVertexY = vertex.y;\n next = verticesItr.next();\n if (next.done) {\n vertex.x = firstVertexX;\n vertex.y = firstVertexY;\n }\n else {\n vertex = next.value;\n }\n if ((vertex.x < pixelCenterX && pixelCenterX < previousVertexX) ||\n (previousVertexX < pixelCenterX && pixelCenterX < vertex.x)) {\n workingNodes.push(Math.round(vertex.y +\n ((pixelCenterX - vertex.x) / (previousVertexX - vertex.x)) *\n (previousVertexY - vertex.y)));\n }\n }\n // Sort the nodes, via a simple “Bubble” sort.\n {\n var i = 0;\n while (i < workingNodes.length - 1) {\n if (workingNodes[i] > workingNodes[i + 1]) {\n var swap = workingNodes[i];\n workingNodes[i] = workingNodes[i + 1];\n workingNodes[i + 1] = swap;\n if (i > 0)\n i--;\n }\n else {\n i++;\n }\n }\n }\n // Fill the pixels between node pairs.\n for (var i = 0; i < workingNodes.length; i += 2) {\n if (workingNodes[i] >= maxY) {\n break;\n }\n if (workingNodes[i + 1] <= minY) {\n continue;\n }\n if (workingNodes[i] < minY) {\n workingNodes[i] = minY;\n }\n if (workingNodes[i + 1] > maxY) {\n workingNodes[i + 1] = maxY;\n }\n checkAndFillX(pixelX, workingNodes[i], workingNodes[i + 1]);\n }\n }\n };\n return ObstacleRasterizer;\n}());\n/**\n * Iterable that converts coordinates to the grid.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar GridBasisIterable = /** @class */ (function () {\n function GridBasisIterable() {\n this.grid = null;\n this.sceneVertices = [];\n this.verticesItr = this.sceneVertices[Symbol.iterator]();\n this.result = {\n value: { x: 0, y: 0 },\n done: false,\n };\n }\n GridBasisIterable.prototype.set = function (grid, sceneVertices) {\n this.grid = grid;\n this.sceneVertices = sceneVertices;\n };\n GridBasisIterable.prototype[Symbol.iterator] = function () {\n this.verticesItr = this.sceneVertices[Symbol.iterator]();\n return this;\n };\n GridBasisIterable.prototype.next = function () {\n var next = this.verticesItr.next();\n if (next.done) {\n return next;\n }\n this.grid.convertToGridBasis(next.value, this.result.value);\n return this.result;\n };\n return GridBasisIterable;\n}());\n\n/**\n * Build cohesive regions from the non-obstacle space. It uses the data\n * from the obstacles rasterization {@link ObstacleRasterizer}.\n *\n * This implementation is strongly inspired from CritterAI class \"OpenHeightfieldBuilder\".\n *\n * Introduction to Height Fields: http://www.critterai.org/projects/nmgen_study/heightfields.html\n *\n * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html\n */\nvar RegionGenerator = /** @class */ (function () {\n function RegionGenerator() {\n this.obstacleRegionBordersCleaner = new ObstacleRegionBordersCleaner();\n this.floodedCells = new Array(1024);\n this.workingStack = new Array(1024);\n }\n //TODO implement the smoothing pass on the distance field?\n /**\n * Groups cells into cohesive regions using an watershed based algorithm.\n *\n * This operation depends on neighbor and distance field information.\n * So {@link RegionGenerator.generateDistanceField} operations must be\n * run before this operation.\n *\n * @param grid A field with cell distance information fully generated.\n * @param obstacleCellPadding a padding in cells to apply around the\n * obstacles.\n */\n RegionGenerator.prototype.generateRegions = function (grid, obstacleCellPadding) {\n // Watershed Algorithm\n //\n // Reference: http://en.wikipedia.org/wiki/Watershed_%28algorithm%29\n // A good visualization:\n // http://artis.imag.fr/Publications/2003/HDS03/ (PDF)\n //\n // Summary:\n //\n // This algorithm utilizes the cell.distanceToObstacle value, which\n // is generated by the generateDistanceField() operation.\n //\n // Using the watershed analogy, the cells which are furthest from\n // a border (highest distance to border) represent the lowest points\n // in the watershed. A border cell represents the highest possible\n // water level.\n //\n // The main loop iterates, starting at the lowest point in the\n // watershed, then incrementing with each loop until the highest\n // allowed water level is reached. This slowly \"floods\" the cells\n // starting at the lowest points.\n //\n // During each iteration of the loop, cells that are below the\n // current water level are located and an attempt is made to either\n // add them to exiting regions or create new regions from them.\n //\n // During the region expansion phase, if a newly flooded cell\n // borders on an existing region, it is usually added to the region.\n //\n // Any newly flooded cell that survives the region expansion phase\n // is used as a seed for a new region.\n //\n // At the end of the main loop, a final region expansion is\n // performed which should catch any stray cells that escaped region\n // assignment during the main loop.\n // Represents the minimum distance to an obstacle that is considered\n // traversable. I.e. Can't traverse cells closer than this distance\n // to a border. This provides a way of artificially capping the\n // height to which watershed flooding can occur.\n // I.e. Don't let the algorithm flood all the way to the actual border.\n //\n // We add the minimum border distance to take into account the\n // blurring algorithm which can result in a border cell having a\n // border distance > 0.\n var distanceMin = obstacleCellPadding * 2;\n // TODO: EVAL: Figure out why this iteration limit is needed\n // (todo from the CritterAI sources).\n var expandIterations = 4 + distanceMin * 2;\n // Zero is reserved for the obstacle-region. So initializing to 1.\n var nextRegionID = 1;\n var floodedCells = this.floodedCells;\n // Search until the current distance reaches the minimum allowed\n // distance.\n //\n // Note: This loop will not necessarily complete all region\n // assignments. This is OK since a final region assignment step\n // occurs after the loop iteration is complete.\n for (\n // This value represents the current distance from the border which\n // is to be searched. The search starts at the maximum distance then\n // moves toward zero (toward borders).\n //\n // This number will always be divisible by 2.\n var distance = grid.obstacleDistanceMax() & ~1; distance > distanceMin; distance = Math.max(distance - 2, 0)) {\n // Find all cells that are at or below the current \"water level\"\n // and are not already assigned to a region. Add these cells to\n // the flooded cell list for processing.\n floodedCells.length = 0;\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.regionID === RasterizationCell.NULL_REGION_ID &&\n cell.distanceToObstacle >= distance) {\n // The cell is not already assigned a region and is\n // below the current \"water level\". So the cell can be\n // considered for region assignment.\n floodedCells.push(cell);\n }\n }\n }\n if (nextRegionID > 1) {\n // At least one region has already been created, so first\n // try to put the newly flooded cells into existing regions.\n if (distance > 0) {\n this.expandRegions(grid, floodedCells, expandIterations);\n }\n else {\n this.expandRegions(grid, floodedCells, -1);\n }\n }\n // Create new regions for all cells that could not be added to\n // existing regions.\n for (var _i = 0, floodedCells_1 = floodedCells; _i < floodedCells_1.length; _i++) {\n var floodedCell = floodedCells_1[_i];\n if (!floodedCell ||\n floodedCell.regionID !== RasterizationCell.NULL_REGION_ID) {\n // This cell was assigned to a newly created region\n // during an earlier iteration of this loop.\n // So it can be skipped.\n continue;\n }\n // Fill to slightly more than the current \"water level\".\n // This improves efficiency of the algorithm.\n // And it is necessary with the conservative expansion to ensure that\n // more than one cell is added initially to a new regions otherwise\n // no cell could be added to it later because of the conservative\n // constraint.\n var fillTo = Math.max(distance - 2, distanceMin + 1, 1);\n if (this.floodNewRegion(grid, floodedCell, fillTo, nextRegionID)) {\n nextRegionID++;\n }\n }\n }\n // Find all cells that haven't been assigned regions by the main loop\n // (up to the minimum distance).\n floodedCells.length = 0;\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.distanceToObstacle > distanceMin &&\n cell.regionID === RasterizationCell.NULL_REGION_ID) {\n // Not a border or obstacle region cell. Should be in a region.\n floodedCells.push(cell);\n }\n }\n }\n // Perform a final expansion of existing regions.\n // Allow more iterations than normal for this last expansion.\n if (distanceMin > 0) {\n this.expandRegions(grid, floodedCells, expandIterations * 8);\n }\n else {\n this.expandRegions(grid, floodedCells, -1);\n }\n grid.regionCount = nextRegionID;\n this.obstacleRegionBordersCleaner.fixObstacleRegion(grid);\n //TODO Also port FilterOutSmallRegions?\n // The algorithm to remove vertices in the middle (added at the end of\n // ContourBuilder.buildContours) may already filter them and contour are\n // faster to process than cells.\n };\n /**\n * Attempts to find the most appropriate regions to attach cells to.\n *\n * Any cells successfully attached to a region will have their list\n * entry set to null. So any non-null entries in the list will be cells\n * for which a region could not be determined.\n *\n * @param grid\n * @param inoutCells As input, the list of cells available for formation\n * of new regions. As output, the cells that could not be assigned\n * to new regions.\n * @param maxIterations If set to -1, will iterate through completion.\n */\n RegionGenerator.prototype.expandRegions = function (grid, inoutCells, iterationMax) {\n if (inoutCells.length === 0)\n return;\n var skipped = 0;\n for (var iteration = 0; (iteration < iterationMax || iterationMax === -1) &&\n // All cells have either been processed or could not be\n // processed during the last cycle.\n skipped < inoutCells.length; iteration++) {\n // The number of cells in the working list that have been\n // successfully processed or could not be processed successfully\n // for some reason.\n // This value controls when iteration ends.\n skipped = 0;\n for (var index = 0; index < inoutCells.length; index++) {\n var cell = inoutCells[index];\n if (cell === null) {\n // The cell originally at this index location has\n // already been successfully assigned a region. Nothing\n // else to do with it.\n skipped++;\n continue;\n }\n // Default to unassigned.\n var cellRegion = RasterizationCell.NULL_REGION_ID;\n var regionCenterDist = Number.MAX_VALUE;\n for (var _i = 0, _a = RasterizationGrid.neighbor4Deltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.regionID !== RasterizationCell.NULL_REGION_ID) {\n if (neighbor.distanceToRegionCore + 2 < regionCenterDist) {\n // This neighbor is closer to its region core\n // than previously detected neighbors.\n // Conservative expansion constraint:\n // Check to ensure that this neighbor has\n // at least two other neighbors in its region.\n // This makes sure that adding this cell to\n // this neighbor's region will not result\n // in a single width line of cells.\n var sameRegionCount = 0;\n for (var neighborDirection = 0; neighborDirection < 4; neighborDirection++) {\n var nnCell = grid.getNeighbor(neighbor, neighborDirection);\n // There is a diagonal-neighbor\n if (nnCell.regionID === neighbor.regionID) {\n // This neighbor has a neighbor in\n // the same region.\n sameRegionCount++;\n }\n }\n if (sameRegionCount > 1) {\n cellRegion = neighbor.regionID;\n regionCenterDist = neighbor.distanceToRegionCore + 2;\n }\n }\n }\n }\n if (cellRegion !== RasterizationCell.NULL_REGION_ID) {\n // Found a suitable region for this cell to belong to.\n // Mark this index as having been processed.\n inoutCells[index] = null;\n cell.regionID = cellRegion;\n cell.distanceToRegionCore = regionCenterDist;\n }\n else {\n // Could not find an existing region for this cell.\n skipped++;\n }\n }\n }\n };\n /**\n * Creates a new region surrounding a cell, adding neighbor cells to the\n * new region as appropriate.\n *\n * The new region creation will fail if the root cell is on the\n * border of an existing region.\n *\n * All cells added to the new region as part of this process become\n * \"core\" cells with a distance to region core of zero.\n *\n * @param grid\n * @param rootCell The cell used to seed the new region.\n * @param fillToDist The watershed distance to flood to.\n * @param regionID The region ID to use for the new region\n * (if creation is successful).\n * @return true if a new region was created.\n */\n RegionGenerator.prototype.floodNewRegion = function (grid, rootCell, fillToDist, regionID) {\n var workingStack = this.workingStack;\n workingStack.length = 0;\n workingStack.push(rootCell);\n rootCell.regionID = regionID;\n rootCell.distanceToRegionCore = 0;\n var regionSize = 0;\n var cell;\n while ((cell = workingStack.pop())) {\n // Check regions of neighbor cells.\n //\n // If any neighbor is found to have a region assigned, then\n // the current cell can't be in the new region\n // (want standard flooding algorithm to handle deciding which\n // region this cell should go in).\n //\n // Up to 8 neighbors are checked.\n //\n // Neighbor searches:\n // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch\n var isOnRegionBorder = false;\n for (var _i = 0, _a = RasterizationGrid.neighbor8Deltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n isOnRegionBorder =\n neighbor.regionID !== RasterizationCell.NULL_REGION_ID &&\n neighbor.regionID !== regionID;\n if (isOnRegionBorder)\n break;\n }\n if (isOnRegionBorder) {\n cell.regionID = RasterizationCell.NULL_REGION_ID;\n continue;\n }\n regionSize++;\n // If got this far, we know the current cell is part of the new\n // region. Now check its neighbors to see if they should be\n // assigned to this new region.\n for (var _b = 0, _c = RasterizationGrid.neighbor4Deltas; _b < _c.length; _b++) {\n var delta = _c[_b];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.distanceToObstacle >= fillToDist &&\n neighbor.regionID === RasterizationCell.NULL_REGION_ID) {\n neighbor.regionID = regionID;\n neighbor.distanceToRegionCore = 0;\n workingStack.push(neighbor);\n }\n }\n }\n return regionSize > 0;\n };\n /**\n * Generates distance field information.\n * The {@link RasterizationCell.distanceToObstacle} information is generated\n * for all cells in the field.\n *\n * All distance values are relative and do not represent explicit\n * distance values (such as grid unit distance). The algorithm which is\n * used results in an approximation only. It is not exhaustive.\n *\n * The data generated by this operation is required by\n * {@link RegionGenerator.generateRegions}.\n *\n * @param grid A field with cells obstacle information already generated.\n */\n RegionGenerator.prototype.generateDistanceField = function (grid) {\n // close borders\n for (var x = 0; x < grid.dimX(); x++) {\n var leftCell = grid.get(x, 0);\n leftCell.distanceToObstacle = 0;\n var rightCell = grid.get(x, grid.dimY() - 1);\n rightCell.distanceToObstacle = 0;\n }\n for (var y = 1; y < grid.dimY() - 1; y++) {\n var topCell = grid.get(0, y);\n topCell.distanceToObstacle = 0;\n var bottomCell = grid.get(grid.dimX() - 1, y);\n bottomCell.distanceToObstacle = 0;\n }\n // The next two phases basically check the neighbors of a cell and\n // set the cell's distance field to be slightly greater than the\n // neighbor with the lowest border distance. Distance is increased\n // slightly more for diagonal-neighbors than for axis-neighbors.\n // 1st pass\n // During this pass, the following neighbors are checked:\n // (-1, 0) (-1, -1) (0, -1) (1, -1)\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n for (var _i = 0, _a = RegionGenerator.firstPassDeltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +\n delta.distance;\n if (cell.distanceToObstacle > distanceByNeighbor) {\n cell.distanceToObstacle = distanceByNeighbor;\n }\n }\n }\n }\n // 2nd pass\n // During this pass, the following neighbors are checked:\n // (1, 0) (1, 1) (0, 1) (-1, 1)\n //\n // Besides checking different neighbors, this pass performs its\n // grid search in reverse order.\n for (var y = grid.dimY() - 2; y >= 1; y--) {\n for (var x = grid.dimX() - 2; x >= 1; x--) {\n var cell = grid.get(x, y);\n for (var _b = 0, _c = RegionGenerator.secondPassDeltas; _b < _c.length; _b++) {\n var delta = _c[_b];\n var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +\n delta.distance;\n if (cell.distanceToObstacle > distanceByNeighbor) {\n cell.distanceToObstacle = distanceByNeighbor;\n }\n }\n }\n }\n };\n RegionGenerator.firstPassDeltas = [\n { x: -1, y: 0, distance: 2 },\n { x: -1, y: -1, distance: 3 },\n { x: 0, y: -1, distance: 2 },\n { x: 1, y: -1, distance: 3 },\n ];\n RegionGenerator.secondPassDeltas = [\n { x: 1, y: 0, distance: 2 },\n { x: 1, y: 1, distance: 3 },\n { x: 0, y: 1, distance: 2 },\n { x: -1, y: 1, distance: 3 },\n ];\n return RegionGenerator;\n}());\n/**\n * Implements three algorithms that clean up issues that can\n * develop around obstacle region boarders.\n *\n * - Detect and fix encompassed obstacle regions:\n *\n * If a obstacle region is found that is fully encompassed by a single\n * region, then the region will be split into two regions at the\n * obstacle region border.\n *\n * - Detect and fix \"short wrapping\" of obstacle regions:\n *\n * Regions can sometimes wrap slightly around the corner of a obstacle region\n * in a manner that eventually results in the formation of self-intersecting\n * polygons.\n *\n * Example: Before the algorithm is applied:\n * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_08_cornerwrapbefore.jpg\"\n *\n * Example: After the algorithm is applied:\n * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_09_cornerwrapafter.jpg\n *\n * - Detect and fix incomplete obstacle region connections:\n *\n * If a region touches obstacle region only diagonally, then contour detection\n * algorithms may not properly detect the obstacle region connection. This can\n * adversely effect other algorithms in the pipeline.\n *\n * Example: Before algorithm is applied:\n *\n * b b a a a a\n * b b a a a a\n * a a x x x x\n * a a x x x x\n *\n * Example: After algorithm is applied:\n *\n * b b a a a a\n * b b b a a a <-- Cell transferred to region B.\n * a a x x x x\n * a a x x x x\n *\n *\n * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html\n */\nvar ObstacleRegionBordersCleaner = /** @class */ (function () {\n function ObstacleRegionBordersCleaner() {\n this.workingUpLeftOpenCells = new Array(512);\n this.workingDownRightOpenCells = new Array(512);\n this.workingOpenCells = new Array(512);\n }\n /**\n * This operation utilizes {@link RasterizationCell.contourFlags}. It\n * expects the value to be zero on entry, and re-zero's the value\n * on exit.\n *\n * @param grid a grid with fully built regions.\n */\n ObstacleRegionBordersCleaner.prototype.fixObstacleRegion = function (grid) {\n var workingUpLeftOpenCells = this.workingUpLeftOpenCells;\n workingUpLeftOpenCells.length = 0;\n var workingDownRightOpenCells = this.workingDownRightOpenCells;\n workingDownRightOpenCells.length = 0;\n var workingOpenCells = this.workingOpenCells;\n workingOpenCells.length = 0;\n var extremeCells = [\n null,\n null,\n ];\n var nextRegionID = grid.regionCount;\n // Iterate over the cells, trying to find obstacle region borders.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.contourFlags !== 0)\n // Cell was processed in a previous iteration.\n // Ignore it.\n continue;\n cell.contourFlags = 1;\n var workingCell = null;\n var edgeDirection = -1;\n if (cell.regionID !== RasterizationCell.OBSTACLE_REGION_ID) {\n // Not interested in this cell.\n continue;\n }\n // This is a obstacle region cell. See if it\n // connects to a cell in a non-obstacle region.\n edgeDirection = this.getNonNullBorderDirection(grid, cell);\n if (edgeDirection === -1)\n // This cell is not a border cell. Ignore it.\n continue;\n // This is a border cell. Step into the non-null\n // region and swing the direction around 180 degrees.\n workingCell = grid.getNeighbor(cell, edgeDirection);\n edgeDirection = (edgeDirection + 2) & 0x3;\n // Process the obstacle region contour. Detect and fix\n // local issues. Determine if the region is\n // fully encompassed by a single non-obstacle region.\n var isEncompassedNullRegion = this.processNullRegion(grid, workingCell, edgeDirection, extremeCells);\n if (isEncompassedNullRegion) {\n // This cell is part of a group of obstacle region cells\n // that is encompassed within a single non-obstacle region.\n // This is not permitted. Need to fix it.\n this.partialFloodRegion(grid, extremeCells[0], extremeCells[1], nextRegionID);\n nextRegionID++;\n }\n }\n }\n grid.regionCount = nextRegionID;\n // Clear all flags.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n cell.contourFlags = 0;\n }\n }\n };\n /**\n * Partially flood a region away from the specified direction.\n *\n * {@link RasterizationCell.contourFlags}\n * is set to zero for all flooded cells.\n *\n * @param grid\n * @param startCell The cell to start the flood from.\n * @param borderDirection The hard border for flooding. No\n * cells in this direction from the startCell will be flooded.\n * @param newRegionID The region id to assign the flooded\n * cells to.\n */\n ObstacleRegionBordersCleaner.prototype.partialFloodRegion = function (grid, upLeftCell, downRightCell, newRegionID) {\n var upLeftOpenCells = this.workingUpLeftOpenCells;\n var downRightOpenCells = this.workingDownRightOpenCells;\n var workingOpenCells = this.workingOpenCells;\n // The implementation differs from CritterAI to avoid non-contiguous\n // sections. Instead of brushing in one direction, it floods from\n // 2 extremities of the encompassed obstacle region.\n var regionID = upLeftCell.regionID;\n if (regionID === newRegionID) {\n // avoid infinity loop\n console.error(\"Can't create a new region with an ID that already exist.\");\n return;\n }\n // The 1st flooding set a new the regionID\n upLeftCell.regionID = newRegionID;\n upLeftCell.distanceToRegionCore = 0; // This information is lost.\n upLeftOpenCells.length = 0;\n upLeftOpenCells.push(upLeftCell);\n // The 2nd flooding keep the regionID and mark the cell as visited.\n downRightCell.contourFlags = 2;\n downRightCell.distanceToRegionCore = 0; // This information is lost.\n downRightOpenCells.length = 0;\n downRightOpenCells.push(downRightCell);\n var swap;\n workingOpenCells.length = 0;\n while (upLeftOpenCells.length !== 0 || downRightOpenCells.length !== 0) {\n for (var _i = 0, upLeftOpenCells_1 = upLeftOpenCells; _i < upLeftOpenCells_1.length; _i++) {\n var cell = upLeftOpenCells_1[_i];\n for (var direction = 0; direction < 4; direction++) {\n var neighbor = grid.getNeighbor(cell, direction);\n if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {\n continue;\n }\n // Transfer the neighbor to the new region.\n neighbor.regionID = newRegionID;\n neighbor.distanceToRegionCore = 0; // This information is lost.\n workingOpenCells.push(neighbor);\n }\n }\n // This allows to flood the nearest cells first without needing lifo queue.\n // But a queue would take less memory.\n swap = upLeftOpenCells;\n upLeftOpenCells = workingOpenCells;\n workingOpenCells = swap;\n workingOpenCells.length = 0;\n for (var _a = 0, downRightOpenCells_1 = downRightOpenCells; _a < downRightOpenCells_1.length; _a++) {\n var cell = downRightOpenCells_1[_a];\n for (var direction = 0; direction < 4; direction++) {\n var neighbor = grid.getNeighbor(cell, direction);\n if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {\n continue;\n }\n // Keep the neighbor to the current region.\n neighbor.contourFlags = 2;\n neighbor.distanceToRegionCore = 0; // This information is lost.\n workingOpenCells.push(neighbor);\n }\n }\n swap = downRightOpenCells;\n downRightOpenCells = workingOpenCells;\n workingOpenCells = swap;\n workingOpenCells.length = 0;\n }\n };\n /**\n * Detects and fixes bad cell configurations in the vicinity of a\n * obstacle region contour (See class description for details).\n * @param grid\n * @param startCell A cell in a non-obstacle region that borders a null\n * region.\n * @param startDirection The direction of the obstacle region border.\n * @return TRUE if the start cell's region completely encompasses\n * the obstacle region.\n */\n ObstacleRegionBordersCleaner.prototype.processNullRegion = function (grid, startCell, startDirection, extremeCells) {\n // This algorithm traverses the contour. As it does so, it detects\n // and fixes various known dangerous cell configurations.\n //\n // Traversing the contour: A good way to visualize it is to think\n // of a robot sitting on the floor facing a known wall. It then\n // does the following to skirt the wall:\n // 1. If there is a wall in front of it, turn clockwise in 90 degrees\n // increments until it finds the wall is gone.\n // 2. Move forward one step.\n // 3. Turn counter-clockwise by 90 degrees.\n // 4. Repeat from step 1 until it finds itself at its original\n // location facing its original direction.\n //\n // See also: http://www.critterai.org/projects/nmgen_study/regiongen.html#robotwalk\n //\n // As the traversal occurs, the number of acute (90 degree) and\n // obtuse (270 degree) corners are monitored. If a complete contour is\n // detected and (obtuse corners > acute corners), then the null\n // region is inside the contour. Otherwise the obstacle region is\n // outside the contour, which we don't care about.\n var borderRegionID = startCell.regionID;\n // Prepare for loop.\n var cell = startCell;\n var neighbor = null;\n var direction = startDirection;\n var upLeftCell = cell;\n var downRightCell = cell;\n // Initialize monitoring variables.\n var loopCount = 0;\n var acuteCornerCount = 0;\n var obtuseCornerCount = 0;\n var stepsWithoutBorder = 0;\n var borderSeenLastLoop = false;\n var isBorder = true; // Initial value doesn't matter.\n // Assume a single region is connected to the obstacle region\n // until proven otherwise.\n var hasSingleConnection = true;\n // The loop limit exists for the sole reason of preventing\n // an infinite loop in case of bad input data.\n // It is set to a very high value because there is no way of\n // definitively determining a safe smaller value. Setting\n // the value too low can result in rescanning a contour\n // multiple times, killing performance.\n while (++loopCount < 1 << 30) {\n // Get the cell across the border.\n neighbor = grid.getNeighbor(cell, direction);\n // Detect which type of edge this direction points across.\n if (neighbor === null) {\n // It points across a obstacle region border edge.\n isBorder = true;\n }\n else {\n // We never need to perform contour detection\n // on this cell again. So mark it as processed.\n neighbor.contourFlags = 1;\n if (neighbor.regionID === RasterizationCell.OBSTACLE_REGION_ID) {\n // It points across a obstacle region border edge.\n isBorder = true;\n }\n else {\n // This isn't a obstacle region border.\n isBorder = false;\n if (neighbor.regionID !== borderRegionID)\n // It points across a border to a non-obstacle region.\n // This means the current contour can't\n // represent a fully encompassed obstacle region.\n hasSingleConnection = false;\n }\n }\n // Process the border.\n if (isBorder) {\n // It is a border edge.\n if (borderSeenLastLoop) {\n // A border was detected during the last loop as well.\n // Two detections in a row indicates we passed an acute\n // (inner) corner.\n //\n // a x\n // x x\n acuteCornerCount++;\n }\n else if (stepsWithoutBorder > 1) {\n // We have moved at least two cells before detecting\n // a border. This indicates we passed an obtuse\n // (outer) corner.\n //\n // a a\n // a x\n obtuseCornerCount++;\n stepsWithoutBorder = 0;\n // Detect and fix cell configuration issue around this\n // corner.\n if (this.processOuterCorner(grid, cell, direction))\n // A change was made and it resulted in the\n // corner area having multiple region connections.\n hasSingleConnection = false;\n }\n direction = (direction + 1) & 0x3; // Rotate in clockwise direction.\n borderSeenLastLoop = true;\n stepsWithoutBorder = 0;\n }\n else {\n // Not a obstacle region border.\n // Move to the neighbor and swing the search direction back\n // one increment (counterclockwise). By moving the direction\n // back one increment we guarantee we don't miss any edges.\n cell = neighbor;\n direction = (direction + 3) & 0x3; // Rotate counterclockwise direction.\n borderSeenLastLoop = false;\n stepsWithoutBorder++;\n if (cell.x < upLeftCell.x ||\n (cell.x === upLeftCell.x && cell.y < upLeftCell.y)) {\n upLeftCell = cell;\n }\n if (cell.x > downRightCell.x ||\n (cell.x === downRightCell.x && cell.y > downRightCell.y)) {\n downRightCell = cell;\n }\n }\n if (startCell === cell && startDirection === direction) {\n extremeCells[0] = upLeftCell;\n extremeCells[1] = downRightCell;\n // Have returned to the original cell and direction.\n // The search is complete.\n // Is the obstacle region inside the contour?\n return hasSingleConnection && obtuseCornerCount > acuteCornerCount;\n }\n }\n // If got here then the obstacle region boarder is too large to be fully\n // explored. So it can't be encompassed.\n return false;\n };\n /**\n * Detects and fixes cell configuration issues in the vicinity\n * of obtuse (outer) obstacle region corners.\n * @param grid\n * @param referenceCell The cell in a non-obstacle region that is\n * just past the outer corner.\n * @param borderDirection The direction of the obstacle region border.\n * @return TRUE if more than one region connects to the obstacle region\n * in the vicinity of the corner (this may or may not be due to\n * a change made by this operation).\n */\n ObstacleRegionBordersCleaner.prototype.processOuterCorner = function (grid, referenceCell, borderDirection) {\n var hasMultiRegions = false;\n // Get the previous two cells along the border.\n var backOne = grid.getNeighbor(referenceCell, (borderDirection + 3) & 0x3);\n var backTwo = grid.getNeighbor(backOne, borderDirection);\n var testCell;\n if (backOne.regionID !== referenceCell.regionID &&\n // This differ from the CritterAI implementation.\n // To filter vertices in the middle, this must be avoided too:\n // a x\n // b c\n backTwo.regionID !== backOne.regionID) {\n // Dangerous corner configuration.\n //\n // a x\n // b a\n //\n // Need to change to one of the following configurations:\n //\n // b x a x\n // b a b b\n //\n // Reason: During contour detection this type of configuration can\n // result in the region connection being detected as a\n // region-region portal, when it is not. The region connection\n // is actually interrupted by the obstacle region.\n //\n // This configuration has been demonstrated to result in\n // two regions being improperly merged to encompass an\n // internal obstacle region.\n //\n // Example:\n //\n // a a x x x a\n // a a x x a a\n // b b a a a a\n // b b a a a a\n //\n // During contour and connection detection for region b, at no\n // point will the obstacle region be detected. It will appear\n // as if a clean a-b portal exists.\n //\n // An investigation into fixing this issue via updates to the\n // watershed or contour detection algorithms did not turn\n // up a better way of resolving this issue.\n hasMultiRegions = true;\n // Determine how many connections backTwo has to backOne's region.\n testCell = grid.getNeighbor(backOne, (borderDirection + 3) & 0x3);\n var backTwoConnections = 0;\n if (testCell.regionID === backOne.regionID) {\n backTwoConnections++;\n testCell = grid.getNeighbor(testCell, borderDirection);\n if (testCell.regionID === backOne.regionID)\n backTwoConnections++;\n }\n // Determine how many connections the reference cell has\n // to backOne's region.\n var referenceConnections = 0;\n testCell = grid.getNeighbor(backOne, (borderDirection + 2) & 0x3);\n if (testCell.regionID === backOne.regionID) {\n referenceConnections++;\n testCell = grid.getNeighbor(testCell, (borderDirection + 2) & 0x3);\n if (testCell.regionID === backOne.regionID)\n backTwoConnections++;\n }\n // Change the region of the cell that has the most connections\n // to the target region.\n if (referenceConnections > backTwoConnections)\n referenceCell.regionID = backOne.regionID;\n else\n backTwo.regionID = backOne.regionID;\n }\n else if (backOne.regionID === referenceCell.regionID &&\n backTwo.regionID === referenceCell.regionID) {\n // Potential dangerous short wrap.\n //\n // a x\n // a a\n //\n // Example of actual problem configuration:\n //\n // b b x x\n // b a x x <- Short wrap.\n // b a a a\n //\n // In the above case, the short wrap around the corner of the\n // obstacle region has been demonstrated to cause self-intersecting\n // polygons during polygon formation.\n //\n // This algorithm detects whether or not one (and only one)\n // of the axis neighbors of the corner should be re-assigned to\n // a more appropriate region.\n //\n // In the above example, the following configuration is more\n // appropriate:\n //\n // b b x x\n // b b x x <- Change to this row.\n // b a a a\n // Check to see if backTwo should be in a different region.\n var selectedRegion = this.selectedRegionID(grid, backTwo, (borderDirection + 1) & 0x3, (borderDirection + 2) & 0x3);\n if (selectedRegion === backTwo.regionID) {\n // backTwo should not be re-assigned. How about\n // the reference cell?\n selectedRegion = this.selectedRegionID(grid, referenceCell, borderDirection, (borderDirection + 3) & 0x3);\n if (selectedRegion !== referenceCell.regionID) {\n // The reference cell should be reassigned\n // to a new region.\n referenceCell.regionID = selectedRegion;\n hasMultiRegions = true;\n }\n }\n else {\n // backTwo should be re-assigned to a new region.\n backTwo.regionID = selectedRegion;\n hasMultiRegions = true;\n }\n }\n else\n hasMultiRegions = true;\n // No dangerous configurations detected. But definitely\n // has a change in regions at the corner. We know this\n // because one of the previous checks looked for a single\n // region for all wrap cells.\n return hasMultiRegions;\n };\n /**\n * Checks the cell to see if it should be reassigned to a new region.\n *\n * @param grid\n * @param referenceCell A cell on one side of an obstacle region contour's\n * outer corner. It is expected that the all cells that wrap the\n * corner are in the same region.\n * @param borderDirection The direction of the obstacle region border.\n * @param cornerDirection The direction of the outer corner from the\n * reference cell.\n * @return The region the cell should be a member of. May be the\n * region the cell is currently a member of.\n */\n ObstacleRegionBordersCleaner.prototype.selectedRegionID = function (grid, referenceCell, borderDirection, cornerDirection) {\n // Initial example state:\n //\n // a - Known region.\n // x - Null region.\n // u - Unknown, not checked yet.\n //\n // u u u\n // u a x\n // u a a\n // The only possible alternate region id is from\n // the cell that is opposite the border. So check it first.\n var regionID = grid.getNeighbor(referenceCell, (borderDirection + 2) & 0x3)\n .regionID;\n if (regionID === referenceCell.regionID ||\n regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // The region away from the border is either a obstacle region\n // or the same region. So we keep the current region.\n //\n // u u u u u u\n // a a x or x a x <-- Potentially bad, but stuck with it.\n // u a a u a a\n return referenceCell.regionID;\n // Candidate region for re-assignment.\n var potentialRegion = regionID;\n // Next we check the region opposite from the corner direction.\n // If it is the current region, then we definitely can't\n // change the region id without risk of splitting the region.\n regionID = grid.getNeighbor(referenceCell, (cornerDirection + 2) & 0x3)\n .regionID;\n if (regionID === referenceCell.regionID ||\n regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // The region opposite from the corner direction is\n // either a obstacle region or the same region. So we\n // keep the current region.\n //\n // u a u u x u\n // b a x or b a x\n // u a a u a a\n return referenceCell.regionID;\n // We have checked the early exit special cases. Now a generalized\n // brute count is performed.\n //\n // Priority is given to the potential region. Here is why:\n // (Highly unlikely worst case scenario)\n //\n // c c c c c c\n // b a x -> b b x Select b even though b count == a count.\n // b a a b a a\n // Neighbors in potential region.\n // We know this will have a minimum value of 1.\n var potentialCount = 0;\n // Neighbors in the cell's current region.\n // We know this will have a minimum value of 2.\n var currentCount = 0;\n // Maximum edge case:\n //\n // b b b\n // b a x\n // b a a\n //\n // The maximum edge case for region A can't exist. It\n // is filtered out during one of the earlier special cases\n // handlers.\n //\n // Other cases may exist if more regions are involved.\n // Such cases will tend to favor the current region.\n for (var direction = 0; direction < 8; direction++) {\n var regionID_1 = grid.getNeighbor(referenceCell, direction).regionID;\n if (regionID_1 === referenceCell.regionID)\n currentCount++;\n else if (regionID_1 === potentialRegion)\n potentialCount++;\n }\n return potentialCount < currentCount\n ? referenceCell.regionID\n : potentialRegion;\n };\n /**\n * Returns the direction of the first neighbor in a non-obstacle region.\n * @param grid\n * @param cell The cell to check.\n * @return The direction of the first neighbor in a non-obstacle region, or\n * -1 if all neighbors are in the obstacle region.\n */\n ObstacleRegionBordersCleaner.prototype.getNonNullBorderDirection = function (grid, cell) {\n // Search axis-neighbors.\n for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {\n var delta = RasterizationGrid.neighbor4Deltas[direction];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.regionID !== RasterizationCell.OBSTACLE_REGION_ID)\n // The neighbor is a obstacle region.\n return direction;\n }\n // All neighbors are in a non-obstacle region.\n return -1;\n };\n return ObstacleRegionBordersCleaner;\n}());\n\n// This implementation is strongly inspired from a Java one\n// by Stephen A. Pratt:\n// http://www.critterai.org/projects/nmgen_study/\n//\n// Most of the comments were written by him and were adapted to fit this implementation.\n// This implementation differs a bit from the original:\n// - it's only 2D instead of 3D\n// - it has less features (see TODO) and might have lesser performance\n// - it uses objects for points instead of pointer-like in arrays of numbers\n// - the rasterization comes from other sources because of the 2d focus\n// - partialFloodRegion was rewritten to fix an issue\n// - filterNonObstacleVertices was added\n//\n// The Java implementation was also inspired from Recast that can be found here:\n// https://github.com/recastnavigation/recastnavigation\nvar NavMeshGenerator = /** @class */ (function () {\n function NavMeshGenerator(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, isometricRatio) {\n if (isometricRatio === void 0) { isometricRatio = 1; }\n this.grid = new RasterizationGrid(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, \n // make cells square in the world\n rasterizationCellSize / isometricRatio);\n this.isometricRatio = isometricRatio;\n this.obstacleRasterizer = new ObstacleRasterizer();\n this.regionGenerator = new RegionGenerator();\n this.contourBuilder = new ContourBuilder();\n this.convexPolygonGenerator = new ConvexPolygonGenerator();\n this.gridCoordinateConverter = new GridCoordinateConverter();\n }\n NavMeshGenerator.prototype.buildNavMesh = function (obstacles, obstacleCellPadding) {\n var _this = this;\n this.grid.clear();\n this.obstacleRasterizer.rasterizeObstacles(this.grid, obstacles);\n this.regionGenerator.generateDistanceField(this.grid);\n this.regionGenerator.generateRegions(this.grid, obstacleCellPadding);\n // It's probably not a good idea to expose the vectorization threshold.\n // As stated in the parameter documentation, the value 1 gives good\n // results in any situations.\n var threshold = 1;\n var contours = this.contourBuilder.buildContours(this.grid, threshold);\n var meshField = this.convexPolygonGenerator.splitToConvexPolygons(contours, 16);\n var scaledMeshField = this.gridCoordinateConverter.convertFromGridBasis(this.grid, meshField);\n if (this.isometricRatio != 1) {\n // Rescale the mesh to have the same unit length on the 2 axis for the pathfinding.\n scaledMeshField.forEach(function (polygon) {\n return polygon.forEach(function (point) {\n point.y *= _this.isometricRatio;\n });\n });\n }\n return scaledMeshField;\n };\n return NavMeshGenerator;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * PathfindingObstaclesManager manages the common objects shared by objects having a\n * pathfinding behavior: In particular, the obstacles behaviors are required to declare\n * themselves (see `PathfindingObstaclesManager.addObstacle`) to the manager of their associated scene\n * (see `gdjs.NavMeshPathfindingRuntimeBehavior.obstaclesManagers`).\n */\nvar NavMeshPathfindingObstaclesManager = /** @class */ (function () {\n function NavMeshPathfindingObstaclesManager(instanceContainer, configuration) {\n /**\n * The navigation meshes by moving object size\n * (rounded on _cellSize)\n */\n this._navMeshes = new Map();\n /**\n * Used while NavMeshes update is disabled to remember to do the update\n * when it's enable back.\n */\n this._navMeshesAreUpToDate = true;\n /**\n * This allows to continue finding paths with the old NavMeshes while\n * moving obstacles.\n */\n this._navMeshesUpdateIsEnabled = true;\n var viewpoint = configuration._getViewpoint();\n if (viewpoint === 'Isometry 2:1 (26.565°)') {\n configuration._setIsometricRatio(2);\n }\n else if (viewpoint === 'True Isometry (30°)') {\n configuration._setIsometricRatio(Math.sqrt(3));\n }\n else {\n configuration._setIsometricRatio(1);\n }\n if (configuration._getCellSize() <= 0) {\n configuration._setCellSize(10);\n }\n if (configuration._getAreaLeftBound() === 0 &&\n configuration._getAreaTopBound() === 0 &&\n configuration._getAreaRightBound() === 0 &&\n configuration._getAreaBottomBound() === 0) {\n var game = instanceContainer.getGame();\n configuration._setAreaLeftBound(0);\n configuration._setAreaTopBound(0);\n configuration._setAreaRightBound(game.getGameResolutionWidth());\n configuration._setAreaBottomBound(game.getGameResolutionHeight());\n }\n this.configuration = configuration;\n this._obstacles = new Set();\n this._polygonIterableAdapter = new PolygonIterableAdapter();\n this._navMeshGenerator = new NavMeshGenerator(configuration._getAreaLeftBound(), configuration._getAreaTopBound(), configuration._getAreaRightBound(), configuration._getAreaBottomBound(), configuration._getCellSize(), \n // make cells square in the world\n configuration._getIsometricRatio());\n }\n /**\n * Get the obstacles manager of a scene.\n */\n NavMeshPathfindingObstaclesManager.getManager = function (instanceContainer) {\n // @ts-ignore\n return instanceContainer.navMeshPathfindingObstaclesManager;\n };\n NavMeshPathfindingObstaclesManager.getManagerOrCreate = function (instanceContainer, configuration) {\n // @ts-ignore\n if (!instanceContainer.navMeshPathfindingObstaclesManager) {\n // Create the shared manager if necessary.\n // @ts-ignore\n instanceContainer.navMeshPathfindingObstaclesManager = new NavMeshPathfindingObstaclesManager(instanceContainer, configuration);\n }\n // @ts-ignore\n return instanceContainer.navMeshPathfindingObstaclesManager;\n };\n NavMeshPathfindingObstaclesManager.prototype.setNavMeshesUpdateEnabled = function (navMeshesUpdateIsEnabled) {\n this._navMeshesUpdateIsEnabled = navMeshesUpdateIsEnabled;\n if (navMeshesUpdateIsEnabled && !this._navMeshesAreUpToDate) {\n this._navMeshes.clear();\n this._navMeshesAreUpToDate = true;\n }\n };\n /**\n * Add a obstacle to the list of existing obstacles.\n */\n NavMeshPathfindingObstaclesManager.prototype.addObstacle = function (pathfindingObstacleBehavior) {\n this._obstacles.add(pathfindingObstacleBehavior.behavior.owner);\n this.invalidateNavMesh();\n };\n /**\n * Remove a obstacle from the list of existing obstacles. Be sure that the obstacle was\n * added before.\n */\n NavMeshPathfindingObstaclesManager.prototype.removeObstacle = function (pathfindingObstacleBehavior) {\n this._obstacles.delete(pathfindingObstacleBehavior.behavior.owner);\n this.invalidateNavMesh();\n };\n NavMeshPathfindingObstaclesManager.prototype.invalidateNavMesh = function () {\n if (this._navMeshesUpdateIsEnabled) {\n this._navMeshes.clear();\n this._navMeshesAreUpToDate = true;\n }\n else {\n this._navMeshesAreUpToDate = false;\n }\n };\n NavMeshPathfindingObstaclesManager.prototype.getNavMesh = function (obstacleCellPadding) {\n var navMesh = this._navMeshes.get(obstacleCellPadding);\n if (!navMesh) {\n var navMeshPolygons = this._navMeshGenerator.buildNavMesh(this._getVerticesIterable(this._obstacles), obstacleCellPadding);\n navMesh = new NavMesh(navMeshPolygons);\n this._navMeshes.set(obstacleCellPadding, navMesh);\n }\n return navMesh;\n };\n NavMeshPathfindingObstaclesManager.prototype._getVerticesIterable = function (objects) {\n this._polygonIterableAdapter.set(objects);\n return this._polygonIterableAdapter;\n };\n return NavMeshPathfindingObstaclesManager;\n}());\n/**\n * Iterable that adapts `RuntimeObject` to `Iterable<{x: float y: float}>`.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar PolygonIterableAdapter = /** @class */ (function () {\n function PolygonIterableAdapter() {\n this.objects = [];\n this.objectsItr = this.objects[Symbol.iterator]();\n this.polygonsItr = [][Symbol.iterator]();\n this.pointIterableAdapter = new PointIterableAdapter();\n this.result = {\n value: this.pointIterableAdapter,\n done: false,\n };\n }\n PolygonIterableAdapter.prototype.set = function (objects) {\n this.objects = objects;\n };\n PolygonIterableAdapter.prototype[Symbol.iterator] = function () {\n this.objectsItr = this.objects[Symbol.iterator]();\n this.polygonsItr = [][Symbol.iterator]();\n return this;\n };\n PolygonIterableAdapter.prototype.next = function () {\n var polygonNext = this.polygonsItr.next();\n while (polygonNext.done) {\n var objectNext = this.objectsItr.next();\n if (objectNext.done) {\n // IteratorReturnResult require a defined value\n // even though the spec state otherwise.\n // So, this class can't be typed as an iterable.\n this.result.value = undefined;\n this.result.done = true;\n return this.result;\n }\n this.polygonsItr = objectNext.value.getHitBoxes().values();\n polygonNext = this.polygonsItr.next();\n }\n this.pointIterableAdapter.set(polygonNext.value.vertices);\n this.result.value = this.pointIterableAdapter;\n this.result.done = false;\n return this.result;\n };\n return PolygonIterableAdapter;\n}());\n/**\n * Iterable that adapts coordinates from `[int, int]` to `{x: int, y: int}`.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar PointIterableAdapter = /** @class */ (function () {\n function PointIterableAdapter() {\n this.vertices = [];\n this.verticesItr = this.vertices[Symbol.iterator]();\n this.result = {\n value: { x: 0, y: 0 },\n done: false,\n };\n }\n PointIterableAdapter.prototype.set = function (vertices) {\n this.vertices = vertices;\n };\n PointIterableAdapter.prototype[Symbol.iterator] = function () {\n this.verticesItr = this.vertices[Symbol.iterator]();\n return this;\n };\n PointIterableAdapter.prototype.next = function () {\n var next = this.verticesItr.next();\n if (next.done) {\n return next;\n }\n this.result.value.x = next.value[0];\n this.result.value.y = next.value[1];\n return this.result;\n };\n return PointIterableAdapter;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar NavMeshRenderer = /** @class */ (function () {\n function NavMeshRenderer() {\n /** Used to draw traces for debugging */\n this._lastUsedObstacleCellPadding = null;\n }\n NavMeshRenderer.prototype.setLastUsedObstacleCellPadding = function (lastUsedObstacleCellPadding) {\n this._lastUsedObstacleCellPadding = lastUsedObstacleCellPadding;\n };\n NavMeshRenderer.prototype.render = function (instanceContainer, shapePainter) {\n if (this._lastUsedObstacleCellPadding === null) {\n return;\n }\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n // TODO find a way to rebuild drawing only when necessary.\n // Draw the navigation mesh on a shape painter object for debugging purpose\n var navMesh = manager.getNavMesh(this._lastUsedObstacleCellPadding);\n for (var _i = 0, _a = navMesh.getPolygons(); _i < _a.length; _i++) {\n var navPoly = _a[_i];\n var polygon = navPoly.getPoints();\n if (polygon.length === 0)\n continue;\n for (var index = 1; index < polygon.length; index++) {\n // It helps to spot vertices with 180° between edges.\n shapePainter.drawCircle(polygon[index].x, polygon[index].y / isometricRatio, 3);\n }\n }\n for (var _b = 0, _c = navMesh.getPolygons(); _b < _c.length; _b++) {\n var navPoly = _c[_b];\n var polygon = navPoly.getPoints();\n if (polygon.length === 0)\n continue;\n shapePainter.beginFillPath(polygon[0].x, polygon[0].y / isometricRatio);\n for (var index = 1; index < polygon.length; index++) {\n shapePainter.drawPathLineTo(polygon[index].x, polygon[index].y / isometricRatio);\n }\n shapePainter.closePath();\n shapePainter.endFillPath();\n }\n };\n return NavMeshRenderer;\n}());\n\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar PathFollower = /** @class */ (function () {\n function PathFollower(configuration) {\n // Attributes used for traveling on the path:\n this._path = [];\n this._speed = 0;\n this._distanceOnSegment = 0;\n this._totalSegmentDistance = 0;\n this._currentSegment = 0;\n this._movementAngle = 0;\n this.configuration = configuration;\n }\n PathFollower.prototype.setSpeed = function (speed) {\n this._speed = speed;\n };\n PathFollower.prototype.getSpeed = function () {\n return this._speed;\n };\n PathFollower.prototype.getMovementAngle = function () {\n return this._movementAngle;\n };\n PathFollower.prototype.movementAngleIsAround = function (degreeAngle, tolerance) {\n return (Math.abs(gdjs.evtTools.common.angleDifference(this._movementAngle, degreeAngle)) <= tolerance);\n };\n PathFollower.prototype.getNodeX = function (index) {\n if (index < this._path.length) {\n return this._path[index][0];\n }\n return 0;\n };\n PathFollower.prototype.getNodeY = function (index) {\n if (index < this._path.length) {\n return this._path[index][1];\n }\n return 0;\n };\n PathFollower.prototype.getNextNodeIndex = function () {\n return Math.min(this._currentSegment + 1, this._path.length - 1);\n };\n PathFollower.prototype.getNodeCount = function () {\n return this._path.length;\n };\n PathFollower.prototype.getNextNodeX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);\n return this._path[nextIndex][0];\n };\n PathFollower.prototype.getNextNodeY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);\n return this._path[nextIndex][1];\n };\n PathFollower.prototype.getPreviousNodeX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var previousIndex = Math.min(this._currentSegment, this._path.length - 1);\n return this._path[previousIndex][0];\n };\n PathFollower.prototype.getPreviousNodeY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var previousIndex = Math.min(this._currentSegment, this._path.length - 1);\n return this._path[previousIndex][1];\n };\n PathFollower.prototype.getDestinationX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n return this._path[this._path.length - 1][0];\n };\n PathFollower.prototype.getDestinationY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n return (this._path[this._path.length - 1][1]);\n };\n /**\n * Return true if the object reached its destination.\n */\n PathFollower.prototype.destinationReached = function () {\n return this._currentSegment >= this._path.length - 1;\n };\n /**\n * Compute and move on the path to the specified destination.\n */\n PathFollower.prototype.setPath = function (path) {\n this._path = path;\n this._enterSegment(0);\n };\n PathFollower.prototype._enterSegment = function (segmentNumber) {\n if (this._path.length === 0) {\n return;\n }\n this._currentSegment = segmentNumber;\n if (this._currentSegment < this._path.length - 1) {\n var pathX = this._path[this._currentSegment + 1][0] -\n this._path[this._currentSegment][0];\n var pathY = this._path[this._currentSegment + 1][1] -\n this._path[this._currentSegment][1];\n this._totalSegmentDistance = Math.sqrt(pathX * pathX + pathY * pathY);\n this._distanceOnSegment = 0;\n this._movementAngle =\n (gdjs.toDegrees(Math.atan2(pathY, pathX)) + 360) % 360;\n }\n else {\n this._speed = 0;\n }\n };\n PathFollower.prototype.isMoving = function () {\n return !(this._path.length === 0 || this.destinationReached());\n };\n PathFollower.prototype.step = function (timeDelta) {\n if (this._path.length === 0 || this.destinationReached()) {\n return;\n }\n // Update the speed of the object\n var previousSpeed = this._speed;\n var maxSpeed = this.configuration._getMaxSpeed();\n if (this._speed !== maxSpeed) {\n this._speed += this.configuration._getAcceleration() * timeDelta;\n if (this._speed > maxSpeed) {\n this._speed = maxSpeed;\n }\n }\n // Update the time on the segment and change segment if needed\n // Use a Verlet integration to be frame rate independent.\n this._distanceOnSegment +=\n ((this._speed + previousSpeed) / 2) * timeDelta;\n var remainingDistanceOnSegment = this._totalSegmentDistance - this._distanceOnSegment;\n if (remainingDistanceOnSegment <= 0 &&\n this._currentSegment < this._path.length) {\n this._enterSegment(this._currentSegment + 1);\n this._distanceOnSegment = -remainingDistanceOnSegment;\n }\n };\n PathFollower.prototype.getX = function () {\n return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][0], this._path[this._currentSegment + 1][0], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][0];\n };\n PathFollower.prototype.getY = function () {\n return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][1], this._path[this._currentSegment + 1][1], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][1];\n };\n return PathFollower;\n}());\n\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar NavMeshPathfindingBehavior = /** @class */ (function () {\n function NavMeshPathfindingBehavior(behavior) {\n // Attributes used for traveling on the path:\n this._pathFound = false;\n this.behavior = behavior;\n this.pathFollower = new PathFollower(behavior);\n this.navMeshRenderer = new NavMeshRenderer();\n }\n /**\n * Return true if the latest call to moveTo succeeded.\n */\n NavMeshPathfindingBehavior.prototype.pathFound = function () {\n return this._pathFound;\n };\n /**\n * Compute and move on the path to the specified destination.\n */\n NavMeshPathfindingBehavior.prototype.moveTo = function (instanceContainer, x, y) {\n var owner = this.behavior.owner;\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n this._pathFound = true;\n this.pathFollower.setPath([[owner.getX(), owner.getY()], [x, y]]);\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n var cellSize = manager.configuration._getCellSize();\n var collisionShape = this.behavior._getCollisionShape();\n var extraBorder = this.behavior._getExtraBorder();\n var radiusSqMax = 0;\n if (collisionShape !== 'Dot at center') {\n var centerX = owner.getCenterXInScene();\n var centerY = owner.getCenterYInScene();\n for (var _i = 0, _a = owner.getHitBoxes(); _i < _a.length; _i++) {\n var hitBox = _a[_i];\n for (var _b = 0, _c = hitBox.vertices; _b < _c.length; _b++) {\n var vertex = _c[_b];\n var deltaX = vertex[0] - centerX;\n // to have the same unit on x and y\n var deltaY = (vertex[1] - centerY) * isometricRatio;\n var radiusSq = deltaX * deltaX + deltaY * deltaY;\n radiusSqMax = Math.max(radiusSq, radiusSqMax);\n }\n }\n }\n // Round to avoid to flicker between 2 NavMesh\n // because of trigonometry rounding errors.\n // Round the padding on cellSize to avoid almost identical NavMesh\n var obstacleCellPadding = Math.max(0, Math.round((Math.sqrt(radiusSqMax) + extraBorder) / cellSize));\n this.navMeshRenderer.setLastUsedObstacleCellPadding(obstacleCellPadding);\n var navMesh = manager.getNavMesh(obstacleCellPadding);\n // TODO avoid the path allocation\n var path = navMesh.findPath({\n x: owner.getX(),\n y: owner.getY() * isometricRatio,\n }, { x: x, y: y * isometricRatio }) || [];\n this._pathFound = path.length > 0;\n this.pathFollower.setPath(path.map(function (_a) {\n var x = _a.x, y = _a.y;\n return [x, y];\n }));\n };\n NavMeshPathfindingBehavior.prototype.doStepPreEvents = function (instanceContainer) {\n if (this.pathFollower.destinationReached()) {\n return;\n }\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n var owner = this.behavior.owner;\n var angleOffset = this.behavior._getAngleOffset();\n var angularMaxSpeed = this.behavior._getMaxSpeed();\n var rotateObject = this.behavior._getRotateObject();\n var timeDelta = owner.getElapsedTime(instanceContainer) / 1000;\n this.pathFollower.step(timeDelta);\n // Position object on the segment and update its angle\n var movementAngle = this.pathFollower.getMovementAngle();\n if (rotateObject &&\n owner.getAngle() !== movementAngle + angleOffset) {\n owner.rotateTowardAngle(movementAngle + angleOffset, angularMaxSpeed, instanceContainer);\n }\n owner.setX(this.pathFollower.getX());\n // In case of isometry, convert coords back in screen.\n owner.setY(this.pathFollower.getY() / isometricRatio);\n };\n return NavMeshPathfindingBehavior;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * NavMeshPathfindingObstacleRuntimeBehavior represents a behavior allowing objects to be\n * considered as a obstacle by objects having Pathfinding Behavior.\n */\nvar NavMeshPathfindingObstacleBehavior = /** @class */ (function () {\n function NavMeshPathfindingObstacleBehavior(instanceContainer, behavior) {\n this._oldX = 0;\n this._oldY = 0;\n this._oldWidth = 0;\n this._oldHeight = 0;\n this._registeredInManager = false;\n this.behavior = behavior;\n this._manager = NavMeshPathfindingObstaclesManager.getManagerOrCreate(instanceContainer, \n // @ts-ignore\n behavior._sharedData);\n //Note that we can't use getX(), getWidth()... of owner here:\n //The owner is not yet fully constructed.\n }\n NavMeshPathfindingObstacleBehavior.prototype.onDestroy = function () {\n if (this._manager && this._registeredInManager) {\n this._manager.removeObstacle(this);\n }\n };\n NavMeshPathfindingObstacleBehavior.prototype.doStepPreEvents = function (instanceContainer) {\n var owner = this.behavior.owner;\n //Make sure the obstacle is or is not in the obstacles manager.\n if (!this.behavior.activated() && this._registeredInManager) {\n this._manager.removeObstacle(this);\n this._registeredInManager = false;\n }\n else {\n if (this.behavior.activated() && !this._registeredInManager) {\n this._manager.addObstacle(this);\n this._registeredInManager = true;\n }\n }\n //Track changes in size or position\n if (this._oldX !== owner.getX() ||\n this._oldY !== owner.getY() ||\n this._oldWidth !== owner.getWidth() ||\n this._oldHeight !== owner.getHeight()) {\n if (this._registeredInManager) {\n this._manager.removeObstacle(this);\n this._manager.addObstacle(this);\n }\n this._oldX = owner.getX();\n this._oldY = owner.getY();\n this._oldWidth = owner.getWidth();\n this._oldHeight = owner.getHeight();\n }\n };\n NavMeshPathfindingObstacleBehavior.prototype.doStepPostEvents = function (instanceContainer) { };\n NavMeshPathfindingObstacleBehavior.prototype.onActivate = function () {\n if (this._registeredInManager) {\n return;\n }\n this._manager.addObstacle(this);\n this._registeredInManager = true;\n };\n NavMeshPathfindingObstacleBehavior.prototype.onDeActivate = function () {\n if (!this._registeredInManager) {\n return;\n }\n this._manager.removeObstacle(this);\n this._registeredInManager = false;\n };\n return NavMeshPathfindingObstacleBehavior;\n}());\n\ngdjs.__NavMeshPathfinding = gdjs.__NavMeshPathfinding || {};\ngdjs.__NavMeshPathfinding.NavMeshPathfindingBehavior = NavMeshPathfindingBehavior;\ngdjs.__NavMeshPathfinding.NavMeshPathfindingObstacleBehavior = NavMeshPathfindingObstacleBehavior;\n", + "inlineCode": "\n// This code has been built from https://github.com/D8H/NavMesh-GDevelop-Extension\n// If you need to make any modification, please open a PR on github.\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nfunction __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\n/**\n * Stripped down version of Phaser's Vector2 with just the functionality needed for navmeshes.\n *\n * @export\n * @class Vector2\n */\nvar Vector2 = /** @class */ (function () {\n function Vector2(x, y) {\n if (x === void 0) { x = 0; }\n if (y === void 0) { y = 0; }\n this.x = x;\n this.y = y;\n }\n Vector2.prototype.equals = function (v) {\n return this.x === v.x && this.y === v.y;\n };\n Vector2.prototype.angle = function (v) {\n return Math.atan2(v.y - this.y, v.x - this.x);\n };\n Vector2.prototype.distance = function (v) {\n var dx = v.x - this.x;\n var dy = v.y - this.y;\n return Math.sqrt(dx * dx + dy * dy);\n };\n Vector2.prototype.add = function (v) {\n this.x += v.x;\n this.y += v.y;\n };\n Vector2.prototype.subtract = function (v) {\n this.x -= v.x;\n this.y -= v.y;\n };\n Vector2.prototype.clone = function () {\n return new Vector2(this.x, this.y);\n };\n return Vector2;\n}());\n\nvar GridNode = /** @class */ (function () {\n function GridNode(weight) {\n this.h = 0;\n this.g = 0;\n this.f = 0;\n this.closed = false;\n this.visited = false;\n this.parent = null;\n this.weight = weight;\n }\n GridNode.prototype.isWall = function () {\n return this.weight === 0;\n };\n GridNode.prototype.clean = function () {\n this.f = 0;\n this.g = 0;\n this.h = 0;\n this.visited = false;\n this.closed = false;\n this.parent = null;\n };\n return GridNode;\n}());\n\n/**\n * A class that represents a navigable polygon with a navmesh. It is built on top of a\n * {@link Polygon}. It implements the properties and fields that javascript-astar needs - weight,\n * toString, isWall and getCost. See GPS test from astar repo for structure:\n * https://github.com/bgrins/javascript-astar/blob/master/test/tests.js\n */\nvar NavPoly = /** @class */ (function (_super) {\n __extends(NavPoly, _super);\n /**\n * Creates an instance of NavPoly.\n */\n function NavPoly(id, polygon) {\n var _this = _super.call(this, 1) || this;\n _this.id = id;\n _this.polygon = polygon;\n _this.edges = polygon.edges;\n _this.neighbors = [];\n _this.portals = [];\n _this.centroid = _this.calculateCentroid();\n _this.boundingRadius = _this.calculateRadius();\n return _this;\n }\n /**\n * Returns an array of points that form the polygon.\n */\n NavPoly.prototype.getPoints = function () {\n return this.polygon.points;\n };\n /**\n * Check if the given point-like object is within the polygon.\n */\n NavPoly.prototype.contains = function (point) {\n // Phaser's polygon check doesn't handle when a point is on one of the edges of the line. Note:\n // check numerical stability here. It would also be good to optimize this for different shapes.\n return this.polygon.contains(point.x, point.y) || this.isPointOnEdge(point);\n };\n /**\n * Only rectangles are supported, so this calculation works, but this is not actually the centroid\n * calculation for a polygon. This is just the average of the vertices - proper centroid of a\n * polygon factors in the area.\n */\n NavPoly.prototype.calculateCentroid = function () {\n var centroid = new Vector2(0, 0);\n var length = this.polygon.points.length;\n this.polygon.points.forEach(function (p) { return centroid.add(p); });\n centroid.x /= length;\n centroid.y /= length;\n return centroid;\n };\n /**\n * Calculate the radius of a circle that circumscribes the polygon.\n */\n NavPoly.prototype.calculateRadius = function () {\n var boundingRadius = 0;\n for (var _i = 0, _a = this.polygon.points; _i < _a.length; _i++) {\n var point = _a[_i];\n var d = this.centroid.distance(point);\n if (d > boundingRadius)\n boundingRadius = d;\n }\n return boundingRadius;\n };\n /**\n * Check if the given point-like object is on one of the edges of the polygon.\n */\n NavPoly.prototype.isPointOnEdge = function (_a) {\n var x = _a.x, y = _a.y;\n for (var _i = 0, _b = this.edges; _i < _b.length; _i++) {\n var edge = _b[_i];\n if (edge.pointOnSegment(x, y))\n return true;\n }\n return false;\n };\n NavPoly.prototype.destroy = function () {\n this.neighbors = [];\n this.portals = [];\n };\n // === jsastar methods ===\n NavPoly.prototype.toString = function () {\n return \"NavPoly(id: \" + this.id + \" at: \" + this.centroid + \")\";\n };\n NavPoly.prototype.isWall = function () {\n return false;\n };\n NavPoly.prototype.centroidDistance = function (navPolygon) {\n return this.centroid.distance(navPolygon.centroid);\n };\n NavPoly.prototype.getCost = function (navPolygon) {\n //TODO the cost method should not be in the Node\n return this.centroidDistance(navPolygon);\n };\n return NavPoly;\n}(GridNode));\n\n/**\n * A graph memory structure\n */\nvar Graph = /** @class */ (function () {\n /**\n * A graph memory structure\n * @param {Array} gridIn 2D array of input weights\n * @param {Object} [options]\n * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed\n */\n function Graph(nodes, options) {\n this.dirtyNodes = [];\n options = options || {};\n this.nodes = nodes;\n this.diagonal = !!options.diagonal;\n this.init();\n }\n Graph.prototype.init = function () {\n this.dirtyNodes = [];\n for (var i = 0; i < this.nodes.length; i++) {\n this.nodes[i].clean();\n }\n };\n Graph.prototype.cleanDirty = function () {\n for (var i = 0; i < this.dirtyNodes.length; i++) {\n this.dirtyNodes[i].clean();\n }\n this.dirtyNodes = [];\n };\n Graph.prototype.markDirty = function (node) {\n this.dirtyNodes.push(node);\n };\n return Graph;\n}());\n\n/**\n * Graph for javascript-astar. It implements the functionality for astar. See GPS test from astar\n * repo for structure: https://github.com/bgrins/javascript-astar/blob/master/test/tests.js\n *\n * @class NavGraph\n * @private\n */\nvar NavGraph = /** @class */ (function (_super) {\n __extends(NavGraph, _super);\n function NavGraph(navPolygons) {\n var _this = _super.call(this, navPolygons) || this;\n _this.nodes = navPolygons;\n _this.init();\n return _this;\n }\n NavGraph.prototype.neighbors = function (navPolygon) {\n return navPolygon.neighbors;\n };\n NavGraph.prototype.navHeuristic = function (navPolygon1, navPolygon2) {\n return navPolygon1.centroidDistance(navPolygon2);\n };\n NavGraph.prototype.destroy = function () {\n this.cleanDirty();\n this.nodes = [];\n };\n return NavGraph;\n}(Graph));\n\n/**\n * Calculate the distance squared between two points. This is an optimization to a square root when\n * you just need to compare relative distances without needing to know the specific distance.\n * @param a\n * @param b\n */\nfunction distanceSquared(a, b) {\n var dx = b.x - a.x;\n var dy = b.y - a.y;\n return dx * dx + dy * dy;\n}\n/**\n * Project a point onto a line segment.\n * JS Source: http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment\n * @param point\n * @param line\n */\nfunction projectPointToEdge(point, line) {\n var a = line.start;\n var b = line.end;\n // Consider the parametric equation for the edge's line, p = a + t (b - a). We want to find\n // where our point lies on the line by solving for t:\n // t = [(p-a) . (b-a)] / |b-a|^2\n var l2 = distanceSquared(a, b);\n var t = ((point.x - a.x) * (b.x - a.x) + (point.y - a.y) * (b.y - a.y)) / l2;\n // We clamp t from [0,1] to handle points outside the segment vw.\n t = clamp(t, 0, 1);\n // Project onto the segment\n var p = new Vector2(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y));\n return p;\n}\n/**\n * Twice the area of the triangle formed by a, b and c.\n */\nfunction triarea2(a, b, c) {\n var ax = b.x - a.x;\n var ay = b.y - a.y;\n var bx = c.x - a.x;\n var by = c.y - a.y;\n return bx * ay - ax * by;\n}\n/**\n * Clamp the given value between min and max.\n */\nfunction clamp(value, min, max) {\n if (value < min)\n value = min;\n if (value > max)\n value = max;\n return value;\n}\n/**\n * Check if two values are within a small margin of one another.\n */\nfunction almostEqual(value1, value2, errorMargin) {\n if (errorMargin === void 0) { errorMargin = 0.0001; }\n if (Math.abs(value1 - value2) <= errorMargin)\n return true;\n else\n return false;\n}\n/**\n * Find the smallest angle difference between two angles\n * https://gist.github.com/Aaronduino/4068b058f8dbc34b4d3a9eedc8b2cbe0\n */\nfunction angleDifference(x, y) {\n var a = x - y;\n var i = a + Math.PI;\n var j = Math.PI * 2;\n a = i - Math.floor(i / j) * j; // (a+180) % 360; this ensures the correct sign\n a -= Math.PI;\n return a;\n}\n/**\n * Check if two lines are collinear (within a small error margin).\n */\nfunction areCollinear(line1, line2, errorMargin) {\n if (errorMargin === void 0) { errorMargin = 0.0001; }\n // Figure out if the two lines are equal by looking at the area of the triangle formed\n // by their points\n var area1 = triarea2(line1.start, line1.end, line2.start);\n var area2 = triarea2(line1.start, line1.end, line2.end);\n if (almostEqual(area1, 0, errorMargin) && almostEqual(area2, 0, errorMargin)) {\n return true;\n }\n else\n return false;\n}\n\n// Mostly sourced from PatrolJS at the moment. TODO: come back and reimplement this as an incomplete\n/**\n * @private\n */\nvar Channel = /** @class */ (function () {\n function Channel() {\n this.portals = [];\n this.path = [];\n }\n Channel.prototype.push = function (p1, p2) {\n if (p2 === undefined)\n p2 = p1;\n this.portals.push({\n left: p1,\n right: p2,\n });\n };\n Channel.prototype.stringPull = function () {\n var portals = this.portals;\n var pts = [];\n // Init scan state\n var apexIndex = 0;\n var leftIndex = 0;\n var rightIndex = 0;\n var portalApex = portals[0].left;\n var portalLeft = portals[0].left;\n var portalRight = portals[0].right;\n // Add start point.\n pts.push(portalApex);\n for (var i = 1; i < portals.length; i++) {\n // Find the next portal vertices\n var left = portals[i].left;\n var right = portals[i].right;\n // Update right vertex.\n if (triarea2(portalApex, portalRight, right) <= 0.0) {\n if (portalApex.equals(portalRight) || triarea2(portalApex, portalLeft, right) > 0.0) {\n // Tighten the funnel.\n portalRight = right;\n rightIndex = i;\n }\n else {\n // Right vertex just crossed over the left vertex, so the left vertex should\n // now be part of the path.\n pts.push(portalLeft);\n // Restart scan from portal left point.\n // Make current left the new apex.\n portalApex = portalLeft;\n apexIndex = leftIndex;\n // Reset portal\n portalLeft = portalApex;\n portalRight = portalApex;\n leftIndex = apexIndex;\n rightIndex = apexIndex;\n // Restart scan\n i = apexIndex;\n continue;\n }\n }\n // Update left vertex.\n if (triarea2(portalApex, portalLeft, left) >= 0.0) {\n if (portalApex.equals(portalLeft) || triarea2(portalApex, portalRight, left) < 0.0) {\n // Tighten the funnel.\n portalLeft = left;\n leftIndex = i;\n }\n else {\n // Left vertex just crossed over the right vertex, so the right vertex should\n // now be part of the path\n pts.push(portalRight);\n // Restart scan from portal right point.\n // Make current right the new apex.\n portalApex = portalRight;\n apexIndex = rightIndex;\n // Reset portal\n portalLeft = portalApex;\n portalRight = portalApex;\n leftIndex = apexIndex;\n rightIndex = apexIndex;\n // Restart scan\n i = apexIndex;\n continue;\n }\n }\n }\n if (pts.length === 0 || !pts[pts.length - 1].equals(portals[portals.length - 1].left)) {\n // Append last point to path.\n pts.push(portals[portals.length - 1].left);\n }\n this.path = pts;\n return pts;\n };\n return Channel;\n}());\n\n/**\n * Stripped down version of Phaser's Line with just the functionality needed for navmeshes.\n *\n * @export\n * @class Line\n */\nvar Line = /** @class */ (function () {\n function Line(x1, y1, x2, y2) {\n this.start = new Vector2(x1, y1);\n this.end = new Vector2(x2, y2);\n this.left = Math.min(x1, x2);\n this.right = Math.max(x1, x2);\n this.top = Math.min(y1, y2);\n this.bottom = Math.max(y1, y2);\n }\n Line.prototype.pointOnSegment = function (x, y) {\n return (x >= this.left &&\n x <= this.right &&\n y >= this.top &&\n y <= this.bottom &&\n this.pointOnLine(x, y));\n };\n Line.prototype.pointOnLine = function (x, y) {\n // Compare slope of line start -> xy to line start -> line end\n return (x - this.left) * (this.bottom - this.top) === (this.right - this.left) * (y - this.top);\n };\n return Line;\n}());\n\n/**\n * Stripped down version of Phaser's Polygon with just the functionality needed for navmeshes.\n *\n * @export\n * @class Polygon\n */\nvar Polygon = /** @class */ (function () {\n function Polygon(points, closed) {\n if (closed === void 0) { closed = true; }\n this.isClosed = closed;\n this.points = points;\n this.edges = [];\n for (var i = 1; i < points.length; i++) {\n var p1 = points[i - 1];\n var p2 = points[i];\n this.edges.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n if (this.isClosed) {\n var first = points[0];\n var last = points[points.length - 1];\n this.edges.push(new Line(first.x, first.y, last.x, last.y));\n }\n }\n Polygon.prototype.contains = function (x, y) {\n var inside = false;\n for (var i = -1, j = this.points.length - 1; ++i < this.points.length; j = i) {\n var ix = this.points[i].x;\n var iy = this.points[i].y;\n var jx = this.points[j].x;\n var jy = this.points[j].y;\n if (((iy <= y && y < jy) || (jy <= y && y < iy)) &&\n x < ((jx - ix) * (y - iy)) / (jy - iy) + ix) {\n inside = !inside;\n }\n }\n return inside;\n };\n return Polygon;\n}());\n\nvar BinaryHeap = /** @class */ (function () {\n function BinaryHeap(scoreFunction) {\n this.content = new Array();\n this.scoreFunction = scoreFunction;\n }\n BinaryHeap.prototype.push = function (element) {\n // Add the new element to the end of the array.\n this.content.push(element);\n // Allow it to sink down.\n this.sinkDown(this.content.length - 1);\n };\n BinaryHeap.prototype.pop = function () {\n // Store the first element so we can return it later.\n var result = this.content[0];\n // Get the element at the end of the array.\n var end = this.content.pop();\n if (!end)\n return;\n // If there are any elements left, put the end element at the\n // start, and let it bubble up.\n if (this.content.length > 0) {\n this.content[0] = end;\n this.bubbleUp(0);\n }\n return result;\n };\n BinaryHeap.prototype.remove = function (node) {\n var i = this.content.indexOf(node);\n // When it is found, the process seen in 'pop' is repeated\n // to fill up the hole.\n var end = this.content.pop();\n if (!end)\n return;\n if (i !== this.content.length - 1) {\n this.content[i] = end;\n if (this.scoreFunction(end) < this.scoreFunction(node)) {\n this.sinkDown(i);\n }\n else {\n this.bubbleUp(i);\n }\n }\n };\n BinaryHeap.prototype.size = function () {\n return this.content.length;\n };\n BinaryHeap.prototype.rescoreElement = function (node) {\n this.sinkDown(this.content.indexOf(node));\n };\n BinaryHeap.prototype.sinkDown = function (n) {\n // Fetch the element that has to be sunk.\n var element = this.content[n];\n // When at 0, an element can not sink any further.\n while (n > 0) {\n // Compute the parent element's index, and fetch it.\n var parentN = ((n + 1) >> 1) - 1;\n var parent = this.content[parentN];\n // Swap the elements if the parent is greater.\n if (this.scoreFunction(element) < this.scoreFunction(parent)) {\n this.content[parentN] = element;\n this.content[n] = parent;\n // Update 'n' to continue at the new position.\n n = parentN;\n }\n // Found a parent that is less, no need to sink any further.\n else {\n break;\n }\n }\n };\n BinaryHeap.prototype.bubbleUp = function (n) {\n // Look up the target element and its score.\n var length = this.content.length;\n var element = this.content[n];\n var elemScore = this.scoreFunction(element);\n while (true) {\n // Compute the indices of the child elements.\n var child2N = (n + 1) << 1;\n var child1N = child2N - 1;\n // This is used to store the new position of the element, if any.\n var swap = null;\n var child1Score = 0;\n // If the first child exists (is inside the array)...\n if (child1N < length) {\n // Look it up and compute its score.\n var child1 = this.content[child1N];\n child1Score = this.scoreFunction(child1);\n // If the score is less than our element's, we need to swap.\n if (child1Score < elemScore) {\n swap = child1N;\n }\n }\n // Do the same checks for the other child.\n if (child2N < length) {\n var child2 = this.content[child2N];\n var child2Score = this.scoreFunction(child2);\n if (child2Score < (swap === null ? elemScore : child1Score)) {\n swap = child2N;\n }\n }\n // If the element needs to be moved, swap it, and continue.\n if (swap !== null) {\n this.content[n] = this.content[swap];\n this.content[swap] = element;\n n = swap;\n }\n // Otherwise, we are done.\n else {\n break;\n }\n }\n };\n return BinaryHeap;\n}());\n\n// The following implementation of the A* algorithm is from:\nvar AStar = /** @class */ (function () {\n function AStar() {\n }\n /**\n * Perform an A* Search on a graph given a start and end node.\n * @param {Graph} graph\n * @param {GridNode} start\n * @param {GridNode} end\n * @param {Object} [options]\n * @param {bool} [options.closest] Specifies whether to return the\n path to the closest node if the target is unreachable.\n * @param {Function} [options.heuristic] Heuristic function (see\n * astar.heuristics).\n */\n AStar.prototype.search = function (graph, start, end, \n // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html\n heuristic, closest) {\n if (closest === void 0) { closest = false; }\n graph.cleanDirty();\n var openHeap = this.getHeap();\n var closestNode = start; // set the start node to be the closest if required\n start.h = heuristic(start, end);\n graph.markDirty(start);\n openHeap.push(start);\n while (openHeap.size() > 0) {\n // Grab the lowest f(x) to process next. Heap keeps this sorted for us.\n var currentNode = openHeap.pop();\n // never happen\n if (!currentNode)\n return [];\n // End case -- result has been found, return the traced path.\n if (currentNode === end) {\n return this.pathTo(currentNode);\n }\n // Normal case -- move currentNode from open to closed, process each of its neighbors.\n currentNode.closed = true;\n // Find all neighbors for the current node.\n var neighbors = graph.neighbors(currentNode);\n for (var i = 0, il = neighbors.length; i < il; ++i) {\n var neighbor = neighbors[i];\n if (neighbor.closed || neighbor.isWall()) {\n // Not a valid node to process, skip to next neighbor.\n continue;\n }\n // The g score is the shortest distance from start to current node.\n // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.\n var gScore = currentNode.g + neighbor.getCost(currentNode);\n var beenVisited = neighbor.visited;\n if (!beenVisited || gScore < neighbor.g) {\n // Found an optimal (so far) path to this node. Take score for node to see how good it is.\n neighbor.visited = true;\n neighbor.parent = currentNode;\n neighbor.h = neighbor.h || heuristic(neighbor, end);\n neighbor.g = gScore;\n neighbor.f = neighbor.g + neighbor.h;\n graph.markDirty(neighbor);\n if (closest) {\n // If the neighbor is closer than the current closestNode or if it's equally close but has\n // a cheaper path than the current closest node then it becomes the closest node\n if (neighbor.h < closestNode.h ||\n (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) {\n closestNode = neighbor;\n }\n }\n if (!beenVisited) {\n // Pushing to heap will put it in proper place based on the 'f' value.\n openHeap.push(neighbor);\n }\n else {\n // Already seen the node, but since it has been rescored we need to reorder it in the heap\n openHeap.rescoreElement(neighbor);\n }\n }\n }\n }\n if (closest) {\n return this.pathTo(closestNode);\n }\n // No result was found - empty array signifies failure to find path.\n return [];\n };\n AStar.prototype.pathTo = function (node) {\n var curr = node;\n var path = new Array();\n while (curr.parent) {\n path.unshift(curr);\n curr = curr.parent;\n }\n return path;\n };\n AStar.prototype.getHeap = function () {\n return new BinaryHeap(function (node) {\n return node.f;\n });\n };\n return AStar;\n}());\n\n/**\n * The `NavMesh` class is the workhorse that represents a navigation mesh built from a series of\n * polygons. Once built, the mesh can be asked for a path from one point to another point. Some\n * internal terminology usage:\n * - neighbor: a polygon that shares part of an edge with another polygon\n * - portal: when two neighbor's have edges that overlap, the portal is the overlapping line segment\n * - channel: the path of polygons from starting point to end point\n * - pull the string: run the funnel algorithm on the channel so that the path hugs the edges of the\n * channel. Equivalent to having a string snaking through a hallway and then pulling it taut.\n */\nvar NavMesh = /** @class */ (function () {\n /**\n * @param meshPolygonPoints Array where each element is an array of point-like objects that\n * defines a polygon.\n * @param meshShrinkAmount The amount (in pixels) that the navmesh has been shrunk around\n * obstacles (a.k.a the amount obstacles have been expanded).\n */\n function NavMesh(meshPolygonPoints, meshShrinkAmount) {\n if (meshShrinkAmount === void 0) { meshShrinkAmount = 0; }\n this.meshShrinkAmount = meshShrinkAmount;\n // Convert the PolyPoints[] into NavPoly instances.\n this.navPolygons = meshPolygonPoints.map(function (polyPoints, i) { return new NavPoly(i, new Polygon(polyPoints)); });\n this.calculateNeighbors();\n // Astar graph of connections between polygons\n this.graph = new NavGraph(this.navPolygons);\n }\n /**\n * Get the NavPolys that are in this navmesh.\n */\n NavMesh.prototype.getPolygons = function () {\n return this.navPolygons;\n };\n /**\n * Cleanup method to remove references.\n */\n NavMesh.prototype.destroy = function () {\n this.graph.destroy();\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var poly = _a[_i];\n poly.destroy();\n }\n this.navPolygons = [];\n };\n /**\n * Find if the given point is within any of the polygons in the mesh.\n * @param point\n */\n NavMesh.prototype.isPointInMesh = function (point) {\n return this.navPolygons.some(function (navPoly) { return navPoly.contains(point); });\n };\n /**\n * Find the closest point in the mesh to the given point. If the point is already in the mesh,\n * this will give you that point. If the point is outside of the mesh, this will attempt to\n * project this point into the mesh (up to the given maxAllowableDist). This returns an object\n * with:\n * - distance - from the given point to the mesh\n * - polygon - the one the point is closest to, or null\n * - point - the point inside the mesh, or null\n * @param point\n * @param maxAllowableDist\n */\n NavMesh.prototype.findClosestMeshPoint = function (point, maxAllowableDist) {\n if (maxAllowableDist === void 0) { maxAllowableDist = Number.POSITIVE_INFINITY; }\n var minDistance = maxAllowableDist;\n var closestPoly = null;\n var pointOnClosestPoly = null;\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var navPoly = _a[_i];\n // If we are inside a poly, we've got the closest.\n if (navPoly.contains(point)) {\n minDistance = 0;\n closestPoly = navPoly;\n pointOnClosestPoly = point;\n break;\n }\n // Is the poly close enough to warrant a more accurate check? Point is definitely outside of\n // the polygon. Distance - Radius is the smallest possible distance to an edge of the poly.\n // This will underestimate distance, but that's perfectly fine.\n var r = navPoly.boundingRadius;\n var d = navPoly.centroid.distance(point);\n if (d - r < minDistance) {\n var result = this.projectPointToPolygon(point, navPoly);\n if (result.distance < minDistance) {\n minDistance = result.distance;\n closestPoly = navPoly;\n pointOnClosestPoly = result.point;\n }\n }\n }\n return { distance: minDistance, polygon: closestPoly, point: pointOnClosestPoly };\n };\n /**\n * Find a path from the start point to the end point using this nav mesh.\n * @param startPoint A point-like object in the form {x, y}\n * @param endPoint A point-like object in the form {x, y}\n * @returns An array of points if a path is found, or null if no path\n */\n NavMesh.prototype.findPath = function (startPoint, endPoint) {\n var startPoly = null;\n var endPoly = null;\n var startDistance = Number.MAX_VALUE;\n var endDistance = Number.MAX_VALUE;\n var d, r;\n var startVector = new Vector2(startPoint.x, startPoint.y);\n var endVector = new Vector2(endPoint.x, endPoint.y);\n // Find the closest poly for the starting and ending point\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var navPoly = _a[_i];\n r = navPoly.boundingRadius;\n // Start\n d = navPoly.centroid.distance(startVector);\n if (d <= startDistance && d <= r && navPoly.contains(startVector)) {\n startPoly = navPoly;\n startDistance = d;\n }\n // End\n d = navPoly.centroid.distance(endVector);\n if (d <= endDistance && d <= r && navPoly.contains(endVector)) {\n endPoly = navPoly;\n endDistance = d;\n }\n }\n // If the end point wasn't inside a polygon, run a more liberal check that allows a point\n // to be within meshShrinkAmount radius of a polygon\n if (!endPoly && this.meshShrinkAmount > 0) {\n for (var _b = 0, _c = this.navPolygons; _b < _c.length; _b++) {\n var navPoly = _c[_b];\n r = navPoly.boundingRadius + this.meshShrinkAmount;\n d = navPoly.centroid.distance(endVector);\n if (d <= r) {\n var distance = this.projectPointToPolygon(endVector, navPoly).distance;\n if (distance <= this.meshShrinkAmount && distance < endDistance) {\n endPoly = navPoly;\n endDistance = distance;\n }\n }\n }\n }\n // No matching polygons locations for the end, so no path found\n // because start point is valid normally, check end point first\n if (!endPoly)\n return null;\n // Same check as above, but for the start point\n if (!startPoly && this.meshShrinkAmount > 0) {\n for (var _d = 0, _e = this.navPolygons; _d < _e.length; _d++) {\n var navPoly = _e[_d];\n // Check if point is within bounding circle to avoid extra projection calculations\n r = navPoly.boundingRadius + this.meshShrinkAmount;\n d = navPoly.centroid.distance(startVector);\n if (d <= r) {\n // Check if projected point is within range of a polygon and is closer than the\n // previous point\n var distance = this.projectPointToPolygon(startVector, navPoly).distance;\n if (distance <= this.meshShrinkAmount && distance < startDistance) {\n startPoly = navPoly;\n startDistance = distance;\n }\n }\n }\n }\n // No matching polygons locations for the start, so no path found\n if (!startPoly)\n return null;\n // If the start and end polygons are the same, return a direct path\n if (startPoly === endPoly)\n return [startVector, endVector];\n // Search!\n var astarPath = new AStar().search(this.graph, startPoly, endPoly, this.graph.navHeuristic);\n // While the start and end polygons may be valid, no path between them\n if (astarPath.length === 0)\n return null;\n // jsastar drops the first point from the path, but the funnel algorithm needs it\n astarPath.unshift(startPoly);\n // We have a path, so now time for the funnel algorithm\n var channel = new Channel();\n channel.push(startVector);\n for (var i = 0; i < astarPath.length - 1; i++) {\n var navPolygon = astarPath[i];\n var nextNavPolygon = astarPath[i + 1];\n // Find the portal\n var portal = null;\n for (var i_1 = 0; i_1 < navPolygon.neighbors.length; i_1++) {\n if (navPolygon.neighbors[i_1].id === nextNavPolygon.id) {\n portal = navPolygon.portals[i_1];\n }\n }\n if (!portal)\n throw new Error(\"Path was supposed to be found, but portal is missing!\");\n // Push the portal vertices into the channel\n channel.push(portal.start, portal.end);\n }\n channel.push(endVector);\n // Pull a string along the channel to run the funnel\n channel.stringPull();\n // Clone path, excluding duplicates\n var lastPoint = null;\n var phaserPath = new Array();\n for (var _f = 0, _g = channel.path; _f < _g.length; _f++) {\n var p = _g[_f];\n var newPoint = p.clone();\n if (!lastPoint || !newPoint.equals(lastPoint))\n phaserPath.push(newPoint);\n lastPoint = newPoint;\n }\n return phaserPath;\n };\n NavMesh.prototype.calculateNeighbors = function () {\n // Fill out the neighbor information for each navpoly\n for (var i = 0; i < this.navPolygons.length; i++) {\n var navPoly = this.navPolygons[i];\n for (var j = i + 1; j < this.navPolygons.length; j++) {\n var otherNavPoly = this.navPolygons[j];\n // Check if the other navpoly is within range to touch\n var d = navPoly.centroid.distance(otherNavPoly.centroid);\n if (d > navPoly.boundingRadius + otherNavPoly.boundingRadius)\n continue;\n // The are in range, so check each edge pairing\n for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {\n var edge = _a[_i];\n for (var _b = 0, _c = otherNavPoly.edges; _b < _c.length; _b++) {\n var otherEdge = _c[_b];\n // If edges aren't collinear, not an option for connecting navpolys\n if (!areCollinear(edge, otherEdge))\n continue;\n // If they are collinear, check if they overlap\n var overlap = this.getSegmentOverlap(edge, otherEdge);\n if (!overlap)\n continue;\n // Connections are symmetric!\n navPoly.neighbors.push(otherNavPoly);\n otherNavPoly.neighbors.push(navPoly);\n // Calculate the portal between the two polygons - this needs to be in\n // counter-clockwise order, relative to each polygon\n var p1 = overlap[0], p2 = overlap[1];\n var edgeStartAngle = navPoly.centroid.angle(edge.start);\n var a1 = navPoly.centroid.angle(overlap[0]);\n var a2 = navPoly.centroid.angle(overlap[1]);\n var d1 = angleDifference(edgeStartAngle, a1);\n var d2 = angleDifference(edgeStartAngle, a2);\n if (d1 < d2) {\n navPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n else {\n navPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));\n }\n edgeStartAngle = otherNavPoly.centroid.angle(otherEdge.start);\n a1 = otherNavPoly.centroid.angle(overlap[0]);\n a2 = otherNavPoly.centroid.angle(overlap[1]);\n d1 = angleDifference(edgeStartAngle, a1);\n d2 = angleDifference(edgeStartAngle, a2);\n if (d1 < d2) {\n otherNavPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n else {\n otherNavPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));\n }\n // Two convex polygons shouldn't be connected more than once! (Unless\n // there are unnecessary vertices...)\n }\n }\n }\n }\n };\n // Check two collinear line segments to see if they overlap by sorting the points.\n // Algorithm source: http://stackoverflow.com/a/17152247\n NavMesh.prototype.getSegmentOverlap = function (line1, line2) {\n var points = [\n { line: line1, point: line1.start },\n { line: line1, point: line1.end },\n { line: line2, point: line2.start },\n { line: line2, point: line2.end },\n ];\n points.sort(function (a, b) {\n if (a.point.x < b.point.x)\n return -1;\n else if (a.point.x > b.point.x)\n return 1;\n else {\n if (a.point.y < b.point.y)\n return -1;\n else if (a.point.y > b.point.y)\n return 1;\n else\n return 0;\n }\n });\n // If the first two points in the array come from the same line, no overlap\n var noOverlap = points[0].line === points[1].line;\n // If the two middle points in the array are the same coordinates, then there is a\n // single point of overlap.\n var singlePointOverlap = points[1].point.equals(points[2].point);\n if (noOverlap || singlePointOverlap)\n return null;\n else\n return [points[1].point, points[2].point];\n };\n /**\n * Project a point onto a polygon in the shortest distance possible.\n *\n * @param {Phaser.Point} point The point to project\n * @param {NavPoly} navPoly The navigation polygon to test against\n * @returns {{point: Phaser.Point, distance: number}}\n */\n NavMesh.prototype.projectPointToPolygon = function (point, navPoly) {\n var closestProjection = null;\n var closestDistance = Number.MAX_VALUE;\n for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {\n var edge = _a[_i];\n var projectedPoint = projectPointToEdge(point, edge);\n var d = point.distance(projectedPoint);\n if (closestProjection === null || d < closestDistance) {\n closestDistance = d;\n closestProjection = projectedPoint;\n }\n }\n return { point: closestProjection, distance: closestDistance };\n };\n return NavMesh;\n}());\n\n/**\n * This implementation is strongly inspired from CritterAI class \"Geometry\".\n */\nvar Geometry = /** @class */ (function () {\n function Geometry() {\n }\n /**\n * Returns TRUE if line segment AB intersects with line segment CD in any\n * manner. Either collinear or at a single point.\n * @param ax The x-value for point (ax, ay) in line segment AB.\n * @param ay The y-value for point (ax, ay) in line segment AB.\n * @param bx The x-value for point (bx, by) in line segment AB.\n * @param by The y-value for point (bx, by) in line segment AB.\n * @param cx The x-value for point (cx, cy) in line segment CD.\n * @param cy The y-value for point (cx, cy) in line segment CD.\n * @param dx The x-value for point (dx, dy) in line segment CD.\n * @param dy The y-value for point (dx, dy) in line segment CD.\n * @return TRUE if line segment AB intersects with line segment CD in any\n * manner.\n */\n Geometry.segmentsIntersect = function (ax, ay, bx, by, cx, cy, dx, dy) {\n // This is modified 2D line-line intersection/segment-segment\n // intersection test.\n var deltaABx = bx - ax;\n var deltaABy = by - ay;\n var deltaCAx = ax - cx;\n var deltaCAy = ay - cy;\n var deltaCDx = dx - cx;\n var deltaCDy = dy - cy;\n var numerator = deltaCAy * deltaCDx - deltaCAx * deltaCDy;\n var denominator = deltaABx * deltaCDy - deltaABy * deltaCDx;\n // Perform early exit tests.\n if (denominator === 0 && numerator !== 0) {\n // If numerator is zero, then the lines are colinear.\n // Since it isn't, then the lines must be parallel.\n return false;\n }\n // Lines intersect. But do the segments intersect?\n // Forcing float division on both of these via casting of the\n // denominator.\n var factorAB = numerator / denominator;\n var factorCD = (deltaCAy * deltaABx - deltaCAx * deltaABy) / denominator;\n // Determine the type of intersection\n if (factorAB >= 0.0 &&\n factorAB <= 1.0 &&\n factorCD >= 0.0 &&\n factorCD <= 1.0) {\n return true; // The two segments intersect.\n }\n // The lines intersect, but segments to not.\n return false;\n };\n /**\n * Returns the distance squared from the point to the line segment.\n *\n * Behavior is undefined if the the closest distance is outside the\n * line segment.\n *\n * @param px The x-value of point (px, py).\n * @param py The y-value of point (px, py)\n * @param ax The x-value of the line segment's vertex A.\n * @param ay The y-value of the line segment's vertex A.\n * @param bx The x-value of the line segment's vertex B.\n * @param by The y-value of the line segment's vertex B.\n * @return The distance squared from the point (px, py) to line segment AB.\n */\n Geometry.getPointSegmentDistanceSq = function (px, py, ax, ay, bx, by) {\n // Reference: http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/\n //\n // The goal of the algorithm is to find the point on line segment AB\n // that is closest to P and then calculate the distance between P\n // and that point.\n var deltaABx = bx - ax;\n var deltaABy = by - ay;\n var deltaAPx = px - ax;\n var deltaAPy = py - ay;\n var segmentABLengthSq = deltaABx * deltaABx + deltaABy * deltaABy;\n if (segmentABLengthSq === 0) {\n // AB is not a line segment. So just return\n // distanceSq from P to A\n return deltaAPx * deltaAPx + deltaAPy * deltaAPy;\n }\n var u = (deltaAPx * deltaABx + deltaAPy * deltaABy) / segmentABLengthSq;\n if (u < 0) {\n // Closest point on line AB is outside outside segment AB and\n // closer to A. So return distanceSq from P to A.\n return deltaAPx * deltaAPx + deltaAPy * deltaAPy;\n }\n else if (u > 1) {\n // Closest point on line AB is outside segment AB and closer to B.\n // So return distanceSq from P to B.\n return (px - bx) * (px - bx) + (py - by) * (py - by);\n }\n // Closest point on lineAB is inside segment AB. So find the exact\n // point on AB and calculate the distanceSq from it to P.\n // The calculation in parenthesis is the location of the point on\n // the line segment.\n var deltaX = ax + u * deltaABx - px;\n var deltaY = ay + u * deltaABy - py;\n return deltaX * deltaX + deltaY * deltaY;\n };\n return Geometry;\n}());\n\n/**\n * A cell that holds data needed by the 1st steps of the NavMesh generation.\n */\nvar RasterizationCell = /** @class */ (function () {\n function RasterizationCell(x, y) {\n /**\n * 0 means there is an obstacle in the cell.\n * See {@link RegionGenerator}\n */\n this.distanceToObstacle = Number.MAX_VALUE;\n this.regionID = RasterizationCell.NULL_REGION_ID;\n this.distanceToRegionCore = 0;\n /**\n * If a cell is connected to one or more external regions then the\n * flag will be a 4 bit value where connections are recorded as\n * follows:\n * - bit1 = neighbor0\n * - bit2 = neighbor1\n * - bit3 = neighbor2\n * - bit4 = neighbor3\n * With the meaning of the bits as follows:\n * - 0 = neighbor in same region.\n * - 1 = neighbor not in same region (neighbor may be the obstacle\n * region or a real region).\n *\n * See {@link ContourBuilder}\n */\n this.contourFlags = 0;\n this.x = x;\n this.y = y;\n this.clear();\n }\n RasterizationCell.prototype.clear = function () {\n this.distanceToObstacle = Number.MAX_VALUE;\n this.regionID = RasterizationCell.NULL_REGION_ID;\n this.distanceToRegionCore = 0;\n this.contourFlags = 0;\n };\n /** A cell that has not been assigned to any region yet */\n RasterizationCell.NULL_REGION_ID = 0;\n /**\n * A cell that contains an obstacle.\n *\n * The value is the same as NULL_REGION_ID because the cells that are\n * not assigned to any region at the end of the flooding algorithm are\n * the obstacle cells.\n */\n RasterizationCell.OBSTACLE_REGION_ID = 0;\n return RasterizationCell;\n}());\n\nvar RasterizationGrid = /** @class */ (function () {\n function RasterizationGrid(left, top, right, bottom, cellWidth, cellHeight) {\n this.regionCount = 0;\n this.cellWidth = cellWidth;\n this.cellHeight = cellHeight;\n this.originX = left - cellWidth;\n this.originY = top - cellHeight;\n var dimX = 2 + Math.ceil((right - left) / cellWidth);\n var dimY = 2 + Math.ceil((bottom - top) / cellHeight);\n this.cells = [];\n for (var y = 0; y < dimY; y++) {\n this.cells[y] = [];\n for (var x = 0; x < dimX; x++) {\n this.cells[y][x] = new RasterizationCell(x, y);\n }\n }\n }\n RasterizationGrid.prototype.clear = function () {\n for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {\n var row = _a[_i];\n for (var _b = 0, row_1 = row; _b < row_1.length; _b++) {\n var cell = row_1[_b];\n cell.clear();\n }\n }\n this.regionCount = 0;\n };\n /**\n *\n * @param position the position on the scene\n * @param gridPosition the position on the grid\n * @returns the position on the grid\n */\n RasterizationGrid.prototype.convertToGridBasis = function (position, gridPosition) {\n gridPosition.x = (position.x - this.originX) / this.cellWidth;\n gridPosition.y = (position.y - this.originY) / this.cellHeight;\n return gridPosition;\n };\n /**\n *\n * @param gridPosition the position on the grid\n * @param position the position on the scene\n * @returns the position on the scene\n */\n RasterizationGrid.prototype.convertFromGridBasis = function (gridPosition, position) {\n position.x = gridPosition.x * this.cellWidth + this.originX;\n position.y = gridPosition.y * this.cellHeight + this.originY;\n return position;\n };\n RasterizationGrid.prototype.get = function (x, y) {\n return this.cells[y][x];\n };\n RasterizationGrid.prototype.getNeighbor = function (cell, direction) {\n var delta = RasterizationGrid.neighbor8Deltas[direction];\n return this.cells[cell.y + delta.y][cell.x + delta.x];\n };\n RasterizationGrid.prototype.dimY = function () {\n return this.cells.length;\n };\n RasterizationGrid.prototype.dimX = function () {\n var firstColumn = this.cells[0];\n return firstColumn ? firstColumn.length : 0;\n };\n RasterizationGrid.prototype.obstacleDistanceMax = function () {\n var max = 0;\n for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {\n var cellRow = _a[_i];\n for (var _b = 0, cellRow_1 = cellRow; _b < cellRow_1.length; _b++) {\n var cell = cellRow_1[_b];\n if (cell.distanceToObstacle > max) {\n max = cell.distanceToObstacle;\n }\n }\n }\n return max;\n };\n RasterizationGrid.neighbor4Deltas = [\n { x: -1, y: 0 },\n { x: 0, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: -1 },\n ];\n RasterizationGrid.neighbor8Deltas = [\n { x: -1, y: 0 },\n { x: 0, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: -1 },\n { x: 1, y: 1 },\n { x: -1, y: 1 },\n { x: -1, y: -1 },\n { x: 1, y: -1 },\n ];\n return RasterizationGrid;\n}());\n\n/**\n * Builds a set of contours from the region information contained in\n * {@link RasterizationCell}. It does this by locating and \"walking\" the edges.\n *\n * This implementation is strongly inspired from CritterAI class \"ContourSetBuilder\".\n * http://www.critterai.org/projects/nmgen_study/contourgen.html\n */\nvar ContourBuilder = /** @class */ (function () {\n function ContourBuilder() {\n // These are working lists whose content changes with each iteration\n // of the up coming loop. They represent the detailed and simple\n // contour vertices.\n // Initial sizing is arbitrary.\n this.workingRawVertices = new Array(256);\n this.workingSimplifiedVertices = new Array(64);\n }\n /**\n * Generates a contour set from the provided {@link RasterizationGrid}\n *\n * The provided field is expected to contain region information.\n * Behavior is undefined if the provided field is malformed or incomplete.\n *\n * This operation overwrites the flag fields for all cells in the\n * provided field. So the flags must be saved and restored if they are\n * important.\n *\n * @param grid A fully generated field.\n * @param threshold The maximum distance (in cells) the edge of the contour\n * may deviate from the source geometry when the rastered obstacles are\n * vectorized.\n *\n * Setting it to:\n * - 1 ensure that an aliased edge won't be split to more edges.\n * - more that 1 will reduce the number of edges but the obstacles edges\n * will be followed with less accuracy.\n * - less that 1 might be more accurate but it may try to follow the\n * aliasing and be a lot less accurate.\n *\n * Values under 1 can be useful in specific cases:\n * - when edges are horizontal or vertical, there is no aliasing so value\n * near 0 can do better results.\n * - when edges are 45° multiples, aliased vertex won't be farther than\n * sqrt(2)/2 so values over 0.71 should give good results but not\n * necessarily better than 1.\n *\n * @return The contours generated from the field.\n */\n ContourBuilder.prototype.buildContours = function (grid, threshold) {\n var contours = new Array(grid.regionCount);\n contours.length = 0;\n var contoursByRegion = new Array(grid.regionCount);\n var discardedContours = 0;\n // Set the flags on all cells in non-obstacle regions to indicate which\n // edges are connected to external regions.\n //\n // Reference: Neighbor search and nomenclature.\n // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch\n //\n // If a cell has no connections to external regions or is\n // completely surrounded by other regions (a single cell island),\n // its flag will be zero.\n //\n // If a cell is connected to one or more external regions then the\n // flag will be a 4 bit value where connections are recorded as\n // follows:\n // bit1 = neighbor0\n // bit2 = neighbor1\n // bit3 = neighbor2\n // bit4 = neighbor3\n // With the meaning of the bits as follows:\n // 0 = neighbor in same region.\n // 1 = neighbor not in same region (neighbor may be the obstacle\n // region or a real region).\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n // Note: This algorithm first sets the flag bits such that\n // 1 = \"neighbor is in the same region\". At the end it inverts\n // the bits so flags are as expected.\n // Default to \"not connected to any external region\".\n cell.contourFlags = 0;\n if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // Don't care about cells in the obstacle region.\n continue;\n for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {\n var delta = RasterizationGrid.neighbor4Deltas[direction];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (cell.regionID === neighbor.regionID) {\n // Neighbor is in same region as this cell.\n // Set the bit for this neighbor to 1 (Will be inverted later).\n cell.contourFlags |= 1 << direction;\n }\n }\n // Invert the bits so a bit value of 1 indicates neighbor NOT in\n // same region.\n cell.contourFlags ^= 0xf;\n if (cell.contourFlags === 0xf) {\n // This is an island cell (All neighbors are from other regions)\n // Get rid of flags.\n cell.contourFlags = 0;\n console.warn(\"Discarded contour: Island cell. Can't form a contour. Region: \" +\n cell.regionID);\n discardedContours++;\n }\n }\n }\n // Loop through all cells looking for cells on the edge of a region.\n //\n // At this point, only cells with flags != 0 are edge cells that\n // are part of a region contour.\n //\n // The process of building a contour will clear the flags on all cells\n // that make up the contour to ensure they are only processed once.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID ||\n cell.contourFlags === 0) {\n // cell is either: Part of the obstacle region, does not\n // represent an edge cell, or was already processed during\n // an earlier iteration.\n continue;\n }\n this.workingRawVertices.length = 0;\n this.workingSimplifiedVertices.length = 0;\n // The cell is part of an unprocessed region's contour.\n // Locate a direction of the cell's edge which points toward\n // another region (there is at least one).\n var startDirection = 0;\n while ((cell.contourFlags & (1 << startDirection)) === 0) {\n startDirection++;\n }\n // We now have a cell that is part of a contour and a direction\n // that points to a different region (obstacle or real).\n // Build the contour.\n this.buildRawContours(grid, cell, startDirection, this.workingRawVertices);\n // Perform post processing on the contour in order to\n // create the final, simplified contour.\n this.generateSimplifiedContour(cell.regionID, this.workingRawVertices, this.workingSimplifiedVertices, threshold);\n // The CritterAI implementation filters polygons with less than\n // 3 vertices, but they are needed to filter vertices in the middle\n // (not on an obstacle region border).\n var contour = Array.from(this.workingSimplifiedVertices);\n contours.push(contour);\n contoursByRegion[cell.regionID] = contour;\n }\n }\n if (contours.length + discardedContours !== grid.regionCount - 1) {\n // The only valid state is one contour per region.\n //\n // The only time this should occur is if an invalid contour\n // was formed or if a region resulted in multiple\n // contours (bad region data).\n //\n // IMPORTANT: While a mismatch may not be a fatal error,\n // it should be addressed since it can result in odd,\n // hard to spot anomalies later in the pipeline.\n //\n // A known cause is if a region fully encompasses another\n // region. In such a case, two contours will be formed.\n // The normal outer contour and an inner contour.\n // The CleanNullRegionBorders algorithm protects\n // against internal encompassed obstacle regions.\n console.error(\"Contour generation failed: Detected contours does\" +\n \" not match the number of regions. Regions: \" +\n (grid.regionCount - 1) +\n \", Detected contours: \" +\n (contours.length + discardedContours) +\n \" (Actual: \" +\n contours.length +\n \", Discarded: \" +\n discardedContours +\n \")\");\n // The CritterAI implementation has more detailed logs.\n // They can be interesting for debugging.\n }\n this.filterNonObstacleVertices(contours, contoursByRegion);\n return contours;\n };\n /**\n * Search vertices that are not shared with the obstacle region and\n * remove them.\n *\n * Some contours will have no vertex left.\n *\n * @param contours\n * @param contoursByRegion Some regions may have been discarded\n * so contours index can't be used.\n */\n ContourBuilder.prototype.filterNonObstacleVertices = function (contours, contoursByRegion) {\n // This was not part of the CritterAI implementation.\n // The removed vertex is merged on the nearest of the edges other extremity\n // that is on an obstacle border.\n var commonVertexContours = new Array(5);\n var commonVertexIndexes = new Array(5);\n // Each pass only filter vertex that have an edge other extremity on an obstacle.\n // Vertex depth (in number of edges to reach an obstacle) is reduces by\n // at least one by each pass.\n var movedAnyVertex = false;\n do {\n movedAnyVertex = false;\n for (var _i = 0, contours_1 = contours; _i < contours_1.length; _i++) {\n var contour = contours_1[_i];\n for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {\n var vertex = contour[vertexIndex];\n var nextVertex = contour[(vertexIndex + 1) % contour.length];\n if (vertex.region !== RasterizationCell.OBSTACLE_REGION_ID &&\n nextVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n // This is a vertex in the middle. It must be removed.\n // Search the contours around the vertex.\n //\n // Typically a contour point to its neighbor and it form a cycle.\n //\n // \\ C /\n // \\ /\n // A | B\n // |\n //\n // C -> B -> A -> C\n //\n // There can be more than 3 contours even if it's rare.\n commonVertexContours.length = 0;\n commonVertexIndexes.length = 0;\n commonVertexContours.push(contour);\n commonVertexIndexes.push(vertexIndex);\n var errorFound = false;\n var commonVertex = vertex;\n do {\n var neighborContour = contoursByRegion[commonVertex.region];\n if (!neighborContour) {\n errorFound = true;\n if (commonVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n console.warn(\"contour already discarded: \" + commonVertex.region);\n }\n break;\n }\n var foundVertex = false;\n for (var neighborVertexIndex = 0; neighborVertexIndex < neighborContour.length; neighborVertexIndex++) {\n var neighborVertex = neighborContour[neighborVertexIndex];\n if (neighborVertex.x === commonVertex.x &&\n neighborVertex.y === commonVertex.y) {\n commonVertexContours.push(neighborContour);\n commonVertexIndexes.push(neighborVertexIndex);\n commonVertex = neighborVertex;\n foundVertex = true;\n break;\n }\n }\n if (!foundVertex) {\n errorFound = true;\n console.error(\"Can't find a common vertex with a neighbor contour. There is probably a superposition.\");\n break;\n }\n } while (commonVertex !== vertex);\n if (errorFound) {\n continue;\n }\n if (commonVertexContours.length < 3) {\n console.error(\"The vertex is shared by only \" + commonVertexContours.length + \" regions.\");\n }\n var shorterEdgeContourIndex = -1;\n var edgeLengthMin = Number.MAX_VALUE;\n for (var index = 0; index < commonVertexContours.length; index++) {\n var vertexContour = commonVertexContours[index];\n var vertexIndex_1 = commonVertexIndexes[index];\n var previousVertex = vertexContour[(vertexIndex_1 - 1 + vertexContour.length) %\n vertexContour.length];\n if (previousVertex.region === RasterizationCell.OBSTACLE_REGION_ID) {\n var deltaX = previousVertex.x - vertex.x;\n var deltaY = previousVertex.y - vertex.y;\n var lengthSq = deltaX * deltaX + deltaY * deltaY;\n if (lengthSq < edgeLengthMin) {\n edgeLengthMin = lengthSq;\n shorterEdgeContourIndex = index;\n }\n }\n }\n if (shorterEdgeContourIndex === -1) {\n // A vertex has no neighbor on an obstacle.\n // It will be solved in next iterations.\n continue;\n }\n // Merge the vertex on the other extremity of the smallest of the 3 edges.\n //\n // \\ C /\n // \\ /\n // A | B\n // |\n //\n // - the shortest edge is between A and B\n // - the Y will become a V\n // - vertices are store clockwise\n // - there can be more than one C (it's rare)\n // This is B\n var shorterEdgeContour = commonVertexContours[shorterEdgeContourIndex];\n var shorterEdgeVertexIndex = commonVertexIndexes[shorterEdgeContourIndex];\n var shorterEdgeExtremityVertex = shorterEdgeContour[(shorterEdgeVertexIndex - 1 + shorterEdgeContour.length) %\n shorterEdgeContour.length];\n // This is A\n var shorterEdgeOtherContourIndex = (shorterEdgeContourIndex + 1) % commonVertexContours.length;\n var shorterEdgeOtherContour = commonVertexContours[shorterEdgeOtherContourIndex];\n var shorterEdgeOtherVertexIndex = commonVertexIndexes[shorterEdgeOtherContourIndex];\n for (var index = 0; index < commonVertexContours.length; index++) {\n if (index === shorterEdgeContourIndex ||\n index === shorterEdgeOtherContourIndex) {\n continue;\n }\n // These are C\n var commonVertexContour = commonVertexContours[index];\n var commonVertexIndex = commonVertexIndexes[index];\n // Move the vertex to an obstacle border\n var movedVertex = commonVertexContour[commonVertexIndex];\n movedVertex.x = shorterEdgeExtremityVertex.x;\n movedVertex.y = shorterEdgeExtremityVertex.y;\n movedVertex.region = RasterizationCell.NULL_REGION_ID;\n }\n // There is no more border between A and B,\n // update the region from B to C.\n shorterEdgeOtherContour[(shorterEdgeOtherVertexIndex + 1) % shorterEdgeOtherContour.length].region =\n shorterEdgeOtherContour[shorterEdgeOtherVertexIndex].region;\n // Remove in A and B the vertex that's been move in C.\n shorterEdgeContour.splice(shorterEdgeVertexIndex, 1);\n shorterEdgeOtherContour.splice(shorterEdgeOtherVertexIndex, 1);\n movedAnyVertex = true;\n }\n }\n }\n } while (movedAnyVertex);\n // Clean the polygons from identical vertices.\n //\n // This can happen with 2 vertices regions.\n // 2 edges are superposed and there extremity is the same.\n // One is move over the other.\n // I could observe this with a region between 2 regions\n // where one of one of these 2 regions were also encompassed.\n // A bit like a rainbow, 2 big regions: the land, the sky\n // and 2 regions for the colors.\n //\n // The vertex can't be removed during the process because\n // they hold data used by other merging.\n //\n // Some contour will have no vertex left.\n // It more efficient to let the next step ignore them.\n for (var _a = 0, contours_2 = contours; _a < contours_2.length; _a++) {\n var contour = contours_2[_a];\n for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {\n var vertex = contour[vertexIndex];\n var nextVertexIndex = (vertexIndex + 1) % contour.length;\n var nextVertex = contour[nextVertexIndex];\n if (vertex.x === nextVertex.x && vertex.y === nextVertex.y) {\n contour.splice(nextVertexIndex, 1);\n vertexIndex--;\n }\n }\n }\n };\n /**\n * Walk around the edge of this cell's region gathering vertices that\n * represent the corners of each cell on the sides that are external facing.\n *\n * There will be two or three vertices for each edge cell:\n * Two for cells that don't represent a change in edge direction. Three\n * for cells that represent a change in edge direction.\n *\n * The output array will contain vertices ordered as follows:\n * (x, y, z, regionID) where regionID is the region (obstacle or real) that\n * this vertex is considered to be connected to.\n *\n * WARNING: Only run this operation on cells that are already known\n * to be on a region edge. The direction must also be pointing to a\n * valid edge. Otherwise behavior will be undefined.\n *\n * @param grid the grid of cells\n * @param startCell A cell that is known to be on the edge of a region\n * (part of a region contour).\n * @param startDirection The direction of the edge of the cell that is\n * known to point\n * across the region edge.\n * @param outContourVertices The list of vertices that represent the edge\n * of the region.\n */\n ContourBuilder.prototype.buildRawContours = function (grid, startCell, startDirection, outContourVertices) {\n // Flaw in Algorithm:\n //\n // This method of contour generation can result in an inappropriate\n // impassable seam between two adjacent regions in the following case:\n //\n // 1. One region connects to another region on two sides in an\n // uninterrupted manner (visualize one region wrapping in an L\n // shape around the corner of another).\n // 2. At the corner shared by the two regions, a change in height\n // occurs.\n //\n // In this case, the two regions should share a corner vertex\n // (an obtuse corner vertex for one region and an acute corner\n // vertex for the other region).\n //\n // In reality, though this algorithm will select the same (x, z)\n // coordinates for each region's corner vertex, the vertex heights\n // may differ, eventually resulting in an impassable seam.\n // It is a bit hard to describe the stepping portion of this algorithm.\n // One way to visualize it is to think of a robot sitting on the\n // floor facing a known wall. It then does the following to skirt\n // the wall:\n // 1. If there is a wall in front of it, turn clockwise in 90 degrees\n // increments until it finds the wall is gone.\n // 2. Move forward one step.\n // 3. Turn counter-clockwise by 90 degrees.\n // 4. Repeat from step 1 until it finds itself at its original\n // location facing its original direction.\n //\n // See also: http://www.critterai.org/projects/nmgen_study/contourgen.html#robotwalk\n var cell = startCell;\n var direction = startDirection;\n var loopCount = 0;\n do {\n // Note: The design of this loop is such that the cell variable\n // will always reference an edge cell from the same region as\n // the start cell.\n if ((cell.contourFlags & (1 << direction)) !== 0) {\n // The current direction is pointing toward an edge.\n // Get this edge's vertex.\n var delta = ContourBuilder.leftVertexOfFacingCellBorderDeltas[direction];\n var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);\n outContourVertices.push({\n x: cell.x + delta.x,\n y: cell.y + delta.y,\n region: neighbor.regionID,\n });\n // Remove the flag for this edge. We never need to consider\n // it again since we have a vertex for this edge.\n cell.contourFlags &= ~(1 << direction);\n // Rotate in clockwise direction.\n direction = (direction + 1) & 0x3;\n }\n else {\n // The current direction does not point to an edge. So it\n // must point to a neighbor cell in the same region as the\n // current cell. Move to the neighbor and swing the search\n // direction back one increment (counterclockwise).\n // By moving the direction back one increment we guarantee we\n // don't miss any edges.\n var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);\n cell = neighbor;\n direction = (direction + 3) & 0x3; // Rotate counterclockwise.\n }\n // The loop limit is arbitrary. It exists only to guarantee that\n // bad input data doesn't result in an infinite loop.\n // The only down side of this loop limit is that it limits the\n // number of detectable edge vertices (the longer the region edge\n // and the higher the number of \"turns\" in a region's edge, the less\n // edge vertices can be detected for that region).\n } while (!(cell === startCell && direction === startDirection) &&\n ++loopCount < 65535);\n return outContourVertices;\n };\n /**\n * Takes a group of vertices that represent a region contour and changes\n * it in the following manner:\n * - For any edges that connect to non-obstacle regions, remove all\n * vertices except the start and end vertices for that edge (this\n * smooths the edges between non-obstacle regions into a straight line).\n * - Runs an algorithm's against the contour to follow the edge more closely.\n *\n * @param regionID The region the contour was derived from.\n * @param sourceVertices The source vertices that represent the complex\n * contour.\n * @param outVertices The simplified contour vertices.\n * @param threshold The maximum distance the edge of the contour may deviate\n * from the source geometry.\n */\n ContourBuilder.prototype.generateSimplifiedContour = function (regionID, sourceVertices, outVertices, threshold) {\n var noConnections = true;\n for (var _i = 0, sourceVertices_1 = sourceVertices; _i < sourceVertices_1.length; _i++) {\n var sourceVertex = sourceVertices_1[_i];\n if (sourceVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n noConnections = false;\n break;\n }\n }\n // Seed the simplified contour with the mandatory edges\n // (At least one edge).\n if (noConnections) {\n // This contour represents an island region surrounded only by the\n // obstacle region. Seed the simplified contour with the source's\n // lower left (ll) and upper right (ur) vertices.\n var lowerLeftX = sourceVertices[0].x;\n var lowerLeftY = sourceVertices[0].y;\n var lowerLeftIndex = 0;\n var upperRightX = sourceVertices[0].x;\n var upperRightY = sourceVertices[0].y;\n var upperRightIndex = 0;\n for (var index = 0; index < sourceVertices.length; index++) {\n var sourceVertex = sourceVertices[index];\n var x = sourceVertex.x;\n var y = sourceVertex.y;\n if (x < lowerLeftX || (x === lowerLeftX && y < lowerLeftY)) {\n lowerLeftX = x;\n lowerLeftY = y;\n lowerLeftIndex = index;\n }\n if (x >= upperRightX || (x === upperRightX && y > upperRightY)) {\n upperRightX = x;\n upperRightY = y;\n upperRightIndex = index;\n }\n }\n // The region attribute is used to store an index locally in this function.\n // TODO Maybe there is a way to do this cleanly and keep no memory footprint.\n // Seed the simplified contour with this edge.\n outVertices.push({\n x: lowerLeftX,\n y: lowerLeftY,\n region: lowerLeftIndex,\n });\n outVertices.push({\n x: upperRightX,\n y: upperRightY,\n region: upperRightIndex,\n });\n }\n else {\n // The contour shares edges with other non-obstacle regions.\n // Seed the simplified contour with a new vertex for every\n // location where the region connection changes. These are\n // vertices that are important because they represent portals\n // to other regions.\n for (var index = 0; index < sourceVertices.length; index++) {\n var sourceVert = sourceVertices[index];\n if (sourceVert.region !==\n sourceVertices[(index + 1) % sourceVertices.length].region) {\n // The current vertex has a different region than the\n // next vertex. So there is a change in vertex region.\n outVertices.push({\n x: sourceVert.x,\n y: sourceVert.y,\n region: index,\n });\n }\n }\n }\n this.matchObstacleRegionEdges(sourceVertices, outVertices, threshold);\n if (outVertices.length < 2) {\n // It will be ignored by the triangulation.\n // It should be rare enough not to handle it now.\n console.warn(\"A region is encompassed in another region. It will be ignored.\");\n }\n // There can be polygons with only 2 vertices when a region is between\n // 2 non-obstacles regions. It's still a useful information to filter\n // vertices in the middle (not on an obstacle region border).\n // In this case, the CritterAI implementation adds a 3rd point to avoid\n // invisible polygons, but it makes it difficult to filter it later.\n // Replace the index pointers in the output list with region IDs.\n for (var _a = 0, outVertices_1 = outVertices; _a < outVertices_1.length; _a++) {\n var outVertex = outVertices_1[_a];\n outVertex.region = sourceVertices[outVertex.region].region;\n }\n };\n /**\n * Applies an algorithm to contours which results in obstacle-region edges\n * following the original detail source geometry edge more closely.\n * http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple\n *\n * Adds vertices from the source list to the result list such that\n * if any obstacle region vertices are compared against the result list,\n * none of the vertices will be further from the obstacle region edges than\n * the allowed threshold.\n *\n * Only obstacle-region edges are operated on. All other edges are\n * ignored.\n *\n * The result vertices is expected to be seeded with at least two\n * source vertices.\n *\n * @param sourceVertices\n * @param inoutResultVertices\n * @param threshold The maximum distance the edge of the contour may deviate\n * from the source geometry.\n */\n ContourBuilder.prototype.matchObstacleRegionEdges = function (sourceVertices, inoutResultVertices, threshold) {\n // This implementation is strongly inspired from CritterAI class \"MatchNullRegionEdges\".\n // Loop through all edges in this contour.\n //\n // NOTE: The simplifiedVertCount in the loop condition\n // increases over iterations. That is what keeps the loop going beyond\n // the initial vertex count.\n var resultIndexA = 0;\n while (resultIndexA < inoutResultVertices.length) {\n var resultIndexB = (resultIndexA + 1) % inoutResultVertices.length;\n // The line segment's beginning vertex.\n var ax = inoutResultVertices[resultIndexA].x;\n var az = inoutResultVertices[resultIndexA].y;\n var sourceIndexA = inoutResultVertices[resultIndexA].region;\n // The line segment's ending vertex.\n var bx = inoutResultVertices[resultIndexB].x;\n var bz = inoutResultVertices[resultIndexB].y;\n var sourceIndexB = inoutResultVertices[resultIndexB].region;\n // The source index of the next vertex to test (the vertex just\n // after the current vertex in the source vertex list).\n var testedSourceIndex = (sourceIndexA + 1) % sourceVertices.length;\n var maxDeviation = 0;\n // Default to no index. No new vert to add.\n var toInsertSourceIndex = -1;\n if (sourceVertices[testedSourceIndex].region ===\n RasterizationCell.OBSTACLE_REGION_ID) {\n // This test vertex is part of a obstacle region edge.\n // Loop through the source vertices until the end vertex\n // is found, searching for the vertex that is farthest from\n // the line segment formed by the begin/end vertices.\n //\n // Visualizations:\n // http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple\n while (testedSourceIndex !== sourceIndexB) {\n var deviation = Geometry.getPointSegmentDistanceSq(sourceVertices[testedSourceIndex].x, sourceVertices[testedSourceIndex].y, ax, az, bx, bz);\n if (deviation > maxDeviation) {\n // A new maximum deviation was detected.\n maxDeviation = deviation;\n toInsertSourceIndex = testedSourceIndex;\n }\n // Move to the next vertex.\n testedSourceIndex = (testedSourceIndex + 1) % sourceVertices.length;\n }\n }\n if (toInsertSourceIndex !== -1 && maxDeviation > threshold * threshold) {\n // A vertex was found that is further than allowed from the\n // current edge. Add this vertex to the contour.\n inoutResultVertices.splice(resultIndexA + 1, 0, {\n x: sourceVertices[toInsertSourceIndex].x,\n y: sourceVertices[toInsertSourceIndex].y,\n region: toInsertSourceIndex,\n });\n // Not incrementing the vertex since we need to test the edge\n // formed by vertA and this this new vertex on the next\n // iteration of the loop.\n }\n // This edge segment does not need to be altered. Move to\n // the next vertex.\n else\n resultIndexA++;\n }\n };\n ContourBuilder.leftVertexOfFacingCellBorderDeltas = [\n { x: 0, y: 1 },\n { x: 1, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: 0 },\n ];\n return ContourBuilder;\n}());\n\n/**\n * Builds convex polygons from the provided polygons.\n *\n * This implementation is strongly inspired from CritterAI class \"PolyMeshFieldBuilder\".\n * http://www.critterai.org/projects/nmgen_study/polygen.html\n */\nvar ConvexPolygonGenerator = /** @class */ (function () {\n function ConvexPolygonGenerator() {\n }\n /**\n * Builds convex polygons from the provided polygons.\n * @param concavePolygons The content is manipulated during the operation\n * and it will be left in an undefined state at the end of\n * the operation.\n * @param maxVerticesPerPolygon cap the vertex number in return polygons.\n * @return convex polygons.\n */\n ConvexPolygonGenerator.prototype.splitToConvexPolygons = function (concavePolygons, maxVerticesPerPolygon) {\n // The maximum possible number of polygons assuming that all will\n // be triangles.\n var maxPossiblePolygons = 0;\n // The maximum vertices found in a single contour.\n var maxVerticesPerContour = 0;\n for (var _i = 0, concavePolygons_1 = concavePolygons; _i < concavePolygons_1.length; _i++) {\n var contour = concavePolygons_1[_i];\n var count = contour.length;\n maxPossiblePolygons += count - 2;\n maxVerticesPerContour = Math.max(maxVerticesPerContour, count);\n }\n // Each list is initialized to a size that will minimize resizing.\n var convexPolygons = new Array(maxPossiblePolygons);\n convexPolygons.length = 0;\n // Various working variables.\n // (Values are meaningless outside of the iteration)\n var workingContourFlags = new Array(maxVerticesPerContour);\n workingContourFlags.length = 0;\n var workingPolygons = new Array(maxVerticesPerContour + 1);\n workingPolygons.length = 0;\n var workingMergeInfo = {\n lengthSq: -1,\n polygonAVertexIndex: -1,\n polygonBVertexIndex: -1,\n };\n var workingMergedPolygon = new Array(maxVerticesPerPolygon);\n workingMergedPolygon.length = 0;\n var _loop_1 = function (contour) {\n if (contour.length < 3) {\n return \"continue\";\n }\n // Initialize the working polygon array.\n workingPolygons.length = 0;\n // Triangulate the contour.\n var foundAnyTriangle = false;\n this_1.triangulate(contour, workingContourFlags, function (p1, p2, p3) {\n var workingPolygon = new Array(maxVerticesPerPolygon);\n workingPolygon.length = 0;\n workingPolygon.push(p1);\n workingPolygon.push(p2);\n workingPolygon.push(p3);\n workingPolygons.push(workingPolygon);\n foundAnyTriangle = true;\n });\n if (!foundAnyTriangle) {\n /*\n * Failure of the triangulation.\n * This is known to occur if the source polygon is\n * self-intersecting or the source region contains internal\n * holes. In both cases, the problem is likely due to bad\n * region formation.\n */\n console.error(\"Polygon generation failure: Could not triangulate contour.\");\n console.error(\"contour:\" +\n contour.map(function (point) { return point.x + \" \" + point.y; }).join(\" ; \"));\n return \"continue\";\n }\n if (maxVerticesPerPolygon > 3) {\n // Merging of triangles into larger polygons is permitted.\n // Continue until no polygons can be found to merge.\n // http://www.critterai.org/nmgen_polygen#mergepolys\n while (true) {\n var longestMergeEdge = -1;\n var bestPolygonA = [];\n var polygonAVertexIndex = -1; // Start of the shared edge.\n var bestPolygonB = [];\n var polygonBVertexIndex = -1; // Start of the shared edge.\n var bestPolygonBIndex = -1;\n // Loop through all but the last polygon looking for the\n // best polygons to merge in this iteration.\n for (var indexA = 0; indexA < workingPolygons.length - 1; indexA++) {\n var polygonA = workingPolygons[indexA];\n for (var indexB = indexA + 1; indexB < workingPolygons.length; indexB++) {\n var polygonB = workingPolygons[indexB];\n // Can polyB merge with polyA?\n this_1.getPolyMergeInfo(polygonA, polygonB, maxVerticesPerPolygon, workingMergeInfo);\n if (workingMergeInfo.lengthSq > longestMergeEdge) {\n // polyB has the longest shared edge with\n // polyA found so far. Save the merge\n // information.\n longestMergeEdge = workingMergeInfo.lengthSq;\n bestPolygonA = polygonA;\n polygonAVertexIndex = workingMergeInfo.polygonAVertexIndex;\n bestPolygonB = polygonB;\n polygonBVertexIndex = workingMergeInfo.polygonBVertexIndex;\n bestPolygonBIndex = indexB;\n }\n }\n }\n if (longestMergeEdge <= 0)\n // No valid merges found during this iteration.\n break;\n // Found polygons to merge. Perform the merge.\n /*\n * Fill the mergedPoly array.\n * Start the vertex at the end of polygon A's shared edge.\n * Add all vertices until looping back to the vertex just\n * before the start of the shared edge. Repeat for\n * polygon B.\n *\n * Duplicate vertices are avoided, while ensuring we get\n * all vertices, since each loop drops the vertex that\n * starts its polygon's shared edge and:\n *\n * PolyAStartVert == PolyBEndVert and\n * PolyAEndVert == PolyBStartVert.\n */\n var vertCountA = bestPolygonA.length;\n var vertCountB = bestPolygonB.length;\n workingMergedPolygon.length = 0;\n for (var i = 0; i < vertCountA - 1; i++)\n workingMergedPolygon.push(bestPolygonA[(polygonAVertexIndex + 1 + i) % vertCountA]);\n for (var i = 0; i < vertCountB - 1; i++)\n workingMergedPolygon.push(bestPolygonB[(polygonBVertexIndex + 1 + i) % vertCountB]);\n // Copy the merged polygon over the top of polygon A.\n bestPolygonA.length = 0;\n Array.prototype.push.apply(bestPolygonA, workingMergedPolygon);\n // Remove polygon B\n workingPolygons.splice(bestPolygonBIndex, 1);\n }\n }\n // Polygon creation for this contour is complete.\n // Add polygons to the global polygon array\n Array.prototype.push.apply(convexPolygons, workingPolygons);\n };\n var this_1 = this;\n // Split every concave polygon into convex polygons.\n for (var _a = 0, concavePolygons_2 = concavePolygons; _a < concavePolygons_2.length; _a++) {\n var contour = concavePolygons_2[_a];\n _loop_1(contour);\n }\n // The original implementation builds polygon adjacency information.\n // but the library for the pathfinding already does it.\n return convexPolygons;\n };\n /**\n * Checks two polygons to see if they can be merged. If a merge is\n * allowed, provides data via the outResult argument (see {@link PolyMergeResult}).\n *\n * @param polygonA The polygon A\n * @param polygonB The polygon B\n * @param maxVerticesPerPolygon cap the vertex number in return polygons.\n * @param outResult contains merge information.\n */\n ConvexPolygonGenerator.prototype.getPolyMergeInfo = function (polygonA, polygonB, maxVerticesPerPolygon, outResult) {\n outResult.lengthSq = -1; // Default to invalid merge\n outResult.polygonAVertexIndex = -1;\n outResult.polygonBVertexIndex = -1;\n var vertexCountA = polygonA.length;\n var vertexCountB = polygonB.length;\n // If the merged polygon would would have to many vertices, do not\n // merge. Subtracting two since to take into account the effect of\n // a merge.\n if (vertexCountA + vertexCountB - 2 > maxVerticesPerPolygon)\n return;\n // Check if the polygons share an edge.\n for (var indexA = 0; indexA < vertexCountA; indexA++) {\n // Get the vertex indices for the polygonA edge\n var vertexA = polygonA[indexA];\n var nextVertexA = polygonA[(indexA + 1) % vertexCountA];\n // Search polygonB for matches.\n for (var indexB = 0; indexB < vertexCountB; indexB++) {\n // Get the vertex indices for the polygonB edge.\n var vertexB = polygonB[indexB];\n var nextVertexB = polygonB[(indexB + 1) % vertexCountB];\n // === can be used because vertices comme from the same concave polygon.\n if (vertexA === nextVertexB && nextVertexA === vertexB) {\n // The vertex indices for this edge are the same and\n // sequenced in opposite order. So the edge is shared.\n outResult.polygonAVertexIndex = indexA;\n outResult.polygonBVertexIndex = indexB;\n }\n }\n }\n if (outResult.polygonAVertexIndex === -1)\n // No common edge, cannot merge.\n return;\n // Check to see if the merged polygon would be convex.\n //\n // Gets the vertices near the section where the merge would occur.\n // Do they form a concave section? If so, the merge is invalid.\n //\n // Note that the following algorithm is only valid for clockwise\n // wrapped convex polygons.\n var sharedVertMinus = polygonA[(outResult.polygonAVertexIndex - 1 + vertexCountA) % vertexCountA];\n var sharedVert = polygonA[outResult.polygonAVertexIndex];\n var sharedVertPlus = polygonB[(outResult.polygonBVertexIndex + 2) % vertexCountB];\n if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {\n // The shared vertex (center) is not to the left of segment\n // vertMinus->vertPlus. For a clockwise wrapped polygon, this\n // indicates a concave section. Merged polygon would be concave.\n // Invalid merge.\n return;\n }\n sharedVertMinus =\n polygonB[(outResult.polygonBVertexIndex - 1 + vertexCountB) % vertexCountB];\n sharedVert = polygonB[outResult.polygonBVertexIndex];\n sharedVertPlus =\n polygonA[(outResult.polygonAVertexIndex + 2) % vertexCountA];\n if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {\n // The shared vertex (center) is not to the left of segment\n // vertMinus->vertPlus. For a clockwise wrapped polygon, this\n // indicates a concave section. Merged polygon would be concave.\n // Invalid merge.\n return;\n }\n // Get the vertex indices that form the shared edge.\n sharedVertMinus = polygonA[outResult.polygonAVertexIndex];\n sharedVert = polygonA[(outResult.polygonAVertexIndex + 1) % vertexCountA];\n // Store the lengthSq of the shared edge.\n var deltaX = sharedVertMinus.x - sharedVert.x;\n var deltaZ = sharedVertMinus.y - sharedVert.y;\n outResult.lengthSq = deltaX * deltaX + deltaZ * deltaZ;\n };\n /**\n * Attempts to triangulate a polygon.\n *\n * @param vertices the polygon to be triangulate.\n * The content is manipulated during the operation\n * and it will be left in an undefined state at the end of\n * the operation.\n * @param vertexFlags only used internally\n * @param outTriangles is called for each triangle derived\n * from the original polygon.\n * @return The number of triangles generated. Or, if triangulation\n * failed, a negative number.\n */\n ConvexPolygonGenerator.prototype.triangulate = function (vertices, vertexFlags, outTriangles) {\n // Terminology, concepts and such:\n //\n // This algorithm loops around the edges of a polygon looking for\n // new internal edges to add that will partition the polygon into a\n // new valid triangle internal to the starting polygon. During each\n // iteration the shortest potential new edge is selected to form that\n // iteration's new triangle.\n //\n // Triangles will only be formed if a single new edge will create\n // a triangle. Two new edges will never be added during a single\n // iteration. This means that the triangulated portions of the\n // original polygon will only contain triangles and the only\n // non-triangle polygon will exist in the untriangulated portion\n // of the original polygon.\n //\n // \"Partition edge\" refers to a potential new edge that will form a\n // new valid triangle.\n //\n // \"Center\" vertex refers to the vertex in a potential new triangle\n // which, if the triangle is formed, will be external to the\n // remaining untriangulated portion of the polygon. Since it\n // is now external to the polygon, it can't be used to form any\n // new triangles.\n //\n // Some documentation refers to \"iPlus2\" even though the variable is\n // not in scope or does not exist for that section of code. For\n // documentation purposes, iPlus2 refers to the 2nd vertex after the\n // primary vertex.\n // E.g.: i, iPlus1, and iPlus2.\n //\n // Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#triangulation\n // Loop through all vertices, flagging all indices that represent\n // a center vertex of a valid new triangle.\n vertexFlags.length = vertices.length;\n for (var i = 0; i < vertices.length; i++) {\n var iPlus1 = (i + 1) % vertices.length;\n var iPlus2 = (i + 2) % vertices.length;\n // A triangle formed by i, iPlus1, and iPlus2 will result\n // in a valid internal triangle.\n // Flag the center vertex (iPlus1) to indicate a valid triangle\n // location.\n vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, iPlus2, vertices);\n }\n // Loop through the vertices creating triangles. When there is only a\n // single triangle left, the operation is complete.\n //\n // When a valid triangle is formed, remove its center vertex. So for\n // each loop, a single vertex will be removed.\n //\n // At the start of each iteration the indices list is in the following\n // state:\n // - Represents a simple polygon representing the un-triangulated\n // portion of the original polygon.\n // - All valid center vertices are flagged.\n while (vertices.length > 3) {\n // Find the shortest new valid edge.\n // NOTE: i and iPlus1 are defined in two different scopes in\n // this section. So be careful.\n // Loop through all indices in the remaining polygon.\n var minLengthSq = Number.MAX_VALUE;\n var minLengthSqVertexIndex = -1;\n for (var i_1 = 0; i_1 < vertices.length; i_1++) {\n if (vertexFlags[(i_1 + 1) % vertices.length]) {\n // Indices i, iPlus1, and iPlus2 are known to form a\n // valid triangle.\n var vert = vertices[i_1];\n var vertPlus2 = vertices[(i_1 + 2) % vertices.length];\n // Determine the length of the partition edge.\n // (i -> iPlus2)\n var deltaX = vertPlus2.x - vert.x;\n var deltaY = vertPlus2.y - vert.y;\n var lengthSq = deltaX * deltaX + deltaY * deltaY;\n if (lengthSq < minLengthSq) {\n minLengthSq = lengthSq;\n minLengthSqVertexIndex = i_1;\n }\n }\n }\n if (minLengthSqVertexIndex === -1)\n // Could not find a new triangle. Triangulation failed.\n // This happens if there are three or more vertices\n // left, but none of them are flagged as being a\n // potential center vertex.\n return;\n var i = minLengthSqVertexIndex;\n var iPlus1 = (i + 1) % vertices.length;\n // Add the new triangle to the output.\n outTriangles(vertices[i], vertices[iPlus1], vertices[(i + 2) % vertices.length]);\n // iPlus1, the \"center\" vert in the new triangle, is now external\n // to the untriangulated portion of the polygon. Remove it from\n // the vertices list since it cannot be a member of any new\n // triangles.\n vertices.splice(iPlus1, 1);\n vertexFlags.splice(iPlus1, 1);\n if (iPlus1 === 0 || iPlus1 >= vertices.length) {\n // The vertex removal has invalidated iPlus1 and/or i. So\n // force a wrap, fixing the indices so they reference the\n // correct indices again. This only occurs when the new\n // triangle is formed across the wrap location of the polygon.\n // Case 1: i = 14, iPlus1 = 15, iPlus2 = 0\n // Case 2: i = 15, iPlus1 = 0, iPlus2 = 1;\n i = vertices.length - 1;\n iPlus1 = 0;\n }\n // At this point i and iPlus1 refer to the two indices from a\n // successful triangulation that will be part of another new\n // triangle. We now need to re-check these indices to see if they\n // can now be the center index in a potential new partition.\n vertexFlags[i] = ConvexPolygonGenerator.isValidPartition((i - 1 + vertices.length) % vertices.length, iPlus1, vertices);\n vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, (i + 2) % vertices.length, vertices);\n }\n // Only 3 vertices remain.\n // Add their triangle to the output list.\n outTriangles(vertices[0], vertices[1], vertices[2]);\n };\n /**\n * Check if the line segment formed by vertex A and vertex B will\n * form a valid partition of the polygon.\n *\n * I.e. the line segment AB is internal to the polygon and will not\n * cross existing line segments.\n *\n * Assumptions:\n * - The vertices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * - indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if the line segment formed by vertex A and vertex B will\n * form a valid partition of the polygon.\n */\n ConvexPolygonGenerator.isValidPartition = function (indexA, indexB, vertices) {\n // First check whether the segment AB lies within the internal\n // angle formed at A (this is the faster check).\n // If it does, then perform the more costly check.\n return (ConvexPolygonGenerator.liesWithinInternalAngle(indexA, indexB, vertices) &&\n !ConvexPolygonGenerator.hasIllegalEdgeIntersection(indexA, indexB, vertices));\n };\n /**\n * Check if vertex B lies within the internal angle of the polygon\n * at vertex A.\n *\n * Vertex B does not have to be within the polygon border. It just has\n * be be within the area encompassed by the internal angle formed at\n * vertex A.\n *\n * This operation is a fast way of determining whether a line segment\n * can possibly form a valid polygon partition. If this test returns\n * FALSE, then more expensive checks can be skipped.\n *\n * Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#anglecheck\n *\n * Special case:\n * FALSE is returned if vertex B lies directly on either of the rays\n * cast from vertex A along its associated polygon edges. So the test\n * on vertex B is exclusive of the polygon edges.\n *\n * Assumptions:\n * - The vertices and indices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * -indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if vertex B lies within the internal angle of\n * the polygon at vertex A.\n */\n ConvexPolygonGenerator.liesWithinInternalAngle = function (indexA, indexB, vertices) {\n // Get pointers to the main vertices being tested.\n var vertexA = vertices[indexA];\n var vertexB = vertices[indexB];\n // Get pointers to the vertices just before and just after vertA.\n var vertexAMinus = vertices[(indexA - 1 + vertices.length) % vertices.length];\n var vertexAPlus = vertices[(indexA + 1) % vertices.length];\n // First, find which of the two angles formed by the line segments\n // AMinus->A->APlus is internal to (pointing towards) the polygon.\n // Then test to see if B lies within the area formed by that angle.\n // TRUE if A is left of or on line AMinus->APlus\n if (ConvexPolygonGenerator.isLeftOrCollinear(vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y, vertexAPlus.x, vertexAPlus.y))\n // The angle internal to the polygon is <= 180 degrees\n // (non-reflex angle).\n // Test to see if B lies within this angle.\n return (ConvexPolygonGenerator.isLeft(\n // TRUE if B is left of line A->AMinus\n vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y) &&\n // TRUE if B is right of line A->APlus\n ConvexPolygonGenerator.isRight(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y));\n // The angle internal to the polygon is > 180 degrees (reflex angle).\n // Test to see if B lies within the external (<= 180 degree) angle and\n // flip the result. (If B lies within the external angle, it can't\n // lie within the internal angle)\n return !(\n // TRUE if B is left of or on line A->APlus\n (ConvexPolygonGenerator.isLeftOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y) &&\n // TRUE if B is right of or on line A->AMinus\n ConvexPolygonGenerator.isRightOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y)));\n };\n /**\n * Check if the line segment AB intersects any edges not already\n * connected to one of the two vertices.\n *\n * Assumptions:\n * - The vertices and indices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * - indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if the line segment AB intersects any edges not already\n * connected to one of the two vertices.\n */\n ConvexPolygonGenerator.hasIllegalEdgeIntersection = function (indexA, indexB, vertices) {\n // Get pointers to the primary vertices being tested.\n var vertexA = vertices[indexA];\n var vertexB = vertices[indexB];\n // Loop through the polygon edges.\n for (var edgeBeginIndex = 0; edgeBeginIndex < vertices.length; edgeBeginIndex++) {\n var edgeEndIndex = (edgeBeginIndex + 1) % vertices.length;\n if (edgeBeginIndex === indexA ||\n edgeBeginIndex === indexB ||\n edgeEndIndex === indexA ||\n edgeEndIndex === indexB) {\n continue;\n }\n // Neither of the test indices are endpoints of this edge.\n // Get this edge's vertices.\n var edgeBegin = vertices[edgeBeginIndex];\n var edgeEnd = vertices[edgeEndIndex];\n if ((edgeBegin.x === vertexA.x && edgeBegin.y === vertexA.y) ||\n (edgeBegin.x === vertexB.x && edgeBegin.y === vertexB.y) ||\n (edgeEnd.x === vertexA.x && edgeEnd.y === vertexA.y) ||\n (edgeEnd.x === vertexB.x && edgeEnd.y === vertexB.y)) {\n // One of the test vertices is co-located\n // with one of the endpoints of this edge (this is a\n // test of the actual position of the vertices rather than\n // simply the index check performed earlier).\n // Skip this edge.\n continue;\n }\n // This edge is not connected to either of the test vertices.\n // If line segment AB intersects with this edge, then the\n // intersection is illegal.\n // I.e. New edges cannot cross existing edges.\n if (Geometry.segmentsIntersect(vertexA.x, vertexA.y, vertexB.x, vertexB.y, edgeBegin.x, edgeBegin.y, edgeEnd.x, edgeEnd.y)) {\n return true;\n }\n }\n return false;\n };\n /**\n * Check if point P is to the left of line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the left of line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isLeft = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) < 0;\n };\n /**\n * Check if point P is to the left of line AB when looking\n * from A to B or is collinear with line AB.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the left of line AB when looking\n * from A to B, or is collinear with line AB.\n */\n ConvexPolygonGenerator.isLeftOrCollinear = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) <= 0;\n };\n /**\n * Check if point P is to the right of line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the right of line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isRight = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) > 0;\n };\n /**\n * Check if point P is to the right of or on line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the right of or on line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isRightOrCollinear = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) >= 0;\n };\n /**\n * The absolute value of the returned value is two times the area of the\n * triangle defined by points (A, B, C).\n *\n * A positive value indicates:\n * - Counterclockwise wrapping of the points.\n * - Point B lies to the right of line AC, looking from A to C.\n *\n * A negative value indicates:\n * - Clockwise wrapping of the points.<\n * - Point B lies to the left of line AC, looking from A to C.\n *\n * A value of zero indicates that all points are collinear or\n * represent the same point.\n *\n * This is a fast operation.\n *\n * @param ax The x-value for point (ax, ay) for vertex A of the triangle.\n * @param ay The y-value for point (ax, ay) for vertex A of the triangle.\n * @param bx The x-value for point (bx, by) for vertex B of the triangle.\n * @param by The y-value for point (bx, by) for vertex B of the triangle.\n * @param cx The x-value for point (cx, cy) for vertex C of the triangle.\n * @param cy The y-value for point (cx, cy) for vertex C of the triangle.\n * @return The signed value of two times the area of the triangle defined\n * by the points (A, B, C).\n */\n ConvexPolygonGenerator.getSignedAreaX2 = function (ax, ay, bx, by, cx, cy) {\n // References:\n // http://softsurfer.com/Archive/algorithm_0101/algorithm_0101.htm#Modern%20Triangles\n // http://mathworld.wolfram.com/TriangleArea.html (Search for \"signed\")\n return (bx - ax) * (cy - ay) - (cx - ax) * (by - ay);\n };\n return ConvexPolygonGenerator;\n}());\n\nvar GridCoordinateConverter = /** @class */ (function () {\n function GridCoordinateConverter() {\n }\n /**\n *\n * @param gridPosition the position on the grid\n * @param position the position on the scene\n * @param scaleY for isometry\n * @returns the position on the scene\n */\n GridCoordinateConverter.prototype.convertFromGridBasis = function (grid, polygons) {\n // point can be shared so them must be copied to be scaled.\n return polygons.map(function (polygon) {\n return polygon.map(function (point) { return grid.convertFromGridBasis(point, { x: 0, y: 0 }); });\n });\n };\n return GridCoordinateConverter;\n}());\n\n/**\n * It rasterizes obstacles on a grid.\n *\n * It flags cells as obstacle to be used by {@link RegionGenerator}.\n */\nvar ObstacleRasterizer = /** @class */ (function () {\n function ObstacleRasterizer() {\n this.workingNodes = new Array(8);\n this.gridBasisIterable = new GridBasisIterable();\n }\n /**\n * Rasterize obstacles on a grid.\n * @param grid\n * @param obstacles\n */\n ObstacleRasterizer.prototype.rasterizeObstacles = function (grid, obstacles) {\n var obstaclesItr = obstacles[Symbol.iterator]();\n for (var next = obstaclesItr.next(); !next.done; next = obstaclesItr.next()) {\n var obstacle = next.value;\n this.gridBasisIterable.set(grid, obstacle);\n var vertices = this.gridBasisIterable;\n var minX = Number.MAX_VALUE;\n var maxX = -Number.MAX_VALUE;\n var minY = Number.MAX_VALUE;\n var maxY = -Number.MAX_VALUE;\n var verticesItr = vertices[Symbol.iterator]();\n for (var next_1 = verticesItr.next(); !next_1.done; next_1 = verticesItr.next()) {\n var vertex = next_1.value;\n minX = Math.min(minX, vertex.x);\n maxX = Math.max(maxX, vertex.x);\n minY = Math.min(minY, vertex.y);\n maxY = Math.max(maxY, vertex.y);\n }\n minX = Math.max(Math.floor(minX), 0);\n maxX = Math.min(Math.ceil(maxX), grid.dimX());\n minY = Math.max(Math.floor(minY), 0);\n maxY = Math.min(Math.ceil(maxY), grid.dimY());\n this.fillPolygon(vertices, minX, maxX, minY, maxY, function (x, y) { return (grid.get(x, y).distanceToObstacle = 0); });\n }\n };\n ObstacleRasterizer.prototype.fillPolygon = function (vertices, minX, maxX, minY, maxY, fill) {\n // The following implementation of the scan-line polygon fill algorithm\n // is strongly inspired from:\n // https://alienryderflex.com/polygon_fill/\n // The original implementation was under this license:\n // public-domain code by Darel Rex Finley, 2007\n // This implementation differ with the following:\n // - it handles float vertices\n // so it focus on pixels center\n // - it is conservative to thin vertical or horizontal polygons\n var fillAnyPixels = false;\n this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {\n for (var pixelX = minX; pixelX < maxX; pixelX++) {\n fillAnyPixels = true;\n fill(pixelX, pixelY);\n }\n });\n if (fillAnyPixels) {\n return;\n }\n this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {\n // conserve thin (less than one cell large) horizontal polygons\n if (minX === maxX) {\n fill(minX, pixelY);\n }\n });\n this.scanX(vertices, minX, maxX, minY, maxY, function (pixelX, minY, maxY) {\n for (var pixelY = minY; pixelY < maxY; pixelY++) {\n fill(pixelX, pixelY);\n }\n // conserve thin (less than one cell large) vertical polygons\n if (minY === maxY) {\n fill(pixelX, minY);\n }\n });\n };\n ObstacleRasterizer.prototype.scanY = function (vertices, minX, maxX, minY, maxY, checkAndFillY) {\n var workingNodes = this.workingNodes;\n // Loop through the rows of the image.\n for (var pixelY = minY; pixelY < maxY; pixelY++) {\n var pixelCenterY = pixelY + 0.5;\n // Build a list of nodes.\n workingNodes.length = 0;\n //let j = vertices.length - 1;\n var verticesItr = vertices[Symbol.iterator]();\n var next = verticesItr.next();\n var vertex = next.value;\n // The iterator always return the same instance.\n // It must be copied to be save for later.\n var firstVertexX = vertex.x;\n var firstVertexY = vertex.y;\n while (!next.done) {\n var previousVertexX = vertex.x;\n var previousVertexY = vertex.y;\n next = verticesItr.next();\n if (next.done) {\n vertex.x = firstVertexX;\n vertex.y = firstVertexY;\n }\n else {\n vertex = next.value;\n }\n if ((vertex.y <= pixelCenterY && pixelCenterY < previousVertexY) ||\n (previousVertexY < pixelCenterY && pixelCenterY <= vertex.y)) {\n workingNodes.push(Math.round(vertex.x +\n ((pixelCenterY - vertex.y) / (previousVertexY - vertex.y)) *\n (previousVertexX - vertex.x)));\n }\n }\n // Sort the nodes, via a simple “Bubble” sort.\n {\n var i = 0;\n while (i < workingNodes.length - 1) {\n if (workingNodes[i] > workingNodes[i + 1]) {\n var swap = workingNodes[i];\n workingNodes[i] = workingNodes[i + 1];\n workingNodes[i + 1] = swap;\n if (i > 0)\n i--;\n }\n else {\n i++;\n }\n }\n }\n // Fill the pixels between node pairs.\n for (var i = 0; i < workingNodes.length; i += 2) {\n if (workingNodes[i] >= maxX) {\n break;\n }\n if (workingNodes[i + 1] <= minX) {\n continue;\n }\n if (workingNodes[i] < minX) {\n workingNodes[i] = minX;\n }\n if (workingNodes[i + 1] > maxX) {\n workingNodes[i + 1] = maxX;\n }\n checkAndFillY(pixelY, workingNodes[i], workingNodes[i + 1]);\n }\n }\n };\n ObstacleRasterizer.prototype.scanX = function (vertices, minX, maxX, minY, maxY, checkAndFillX) {\n var workingNodes = this.workingNodes;\n // Loop through the columns of the image.\n for (var pixelX = minX; pixelX < maxX; pixelX++) {\n var pixelCenterX = pixelX + 0.5;\n // Build a list of nodes.\n workingNodes.length = 0;\n var verticesItr = vertices[Symbol.iterator]();\n var next = verticesItr.next();\n var vertex = next.value;\n // The iterator always return the same instance.\n // It must be copied to be save for later.\n var firstVertexX = vertex.x;\n var firstVertexY = vertex.y;\n while (!next.done) {\n var previousVertexX = vertex.x;\n var previousVertexY = vertex.y;\n next = verticesItr.next();\n if (next.done) {\n vertex.x = firstVertexX;\n vertex.y = firstVertexY;\n }\n else {\n vertex = next.value;\n }\n if ((vertex.x < pixelCenterX && pixelCenterX < previousVertexX) ||\n (previousVertexX < pixelCenterX && pixelCenterX < vertex.x)) {\n workingNodes.push(Math.round(vertex.y +\n ((pixelCenterX - vertex.x) / (previousVertexX - vertex.x)) *\n (previousVertexY - vertex.y)));\n }\n }\n // Sort the nodes, via a simple “Bubble” sort.\n {\n var i = 0;\n while (i < workingNodes.length - 1) {\n if (workingNodes[i] > workingNodes[i + 1]) {\n var swap = workingNodes[i];\n workingNodes[i] = workingNodes[i + 1];\n workingNodes[i + 1] = swap;\n if (i > 0)\n i--;\n }\n else {\n i++;\n }\n }\n }\n // Fill the pixels between node pairs.\n for (var i = 0; i < workingNodes.length; i += 2) {\n if (workingNodes[i] >= maxY) {\n break;\n }\n if (workingNodes[i + 1] <= minY) {\n continue;\n }\n if (workingNodes[i] < minY) {\n workingNodes[i] = minY;\n }\n if (workingNodes[i + 1] > maxY) {\n workingNodes[i + 1] = maxY;\n }\n checkAndFillX(pixelX, workingNodes[i], workingNodes[i + 1]);\n }\n }\n };\n return ObstacleRasterizer;\n}());\n/**\n * Iterable that converts coordinates to the grid.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar GridBasisIterable = /** @class */ (function () {\n function GridBasisIterable() {\n this.grid = null;\n this.sceneVertices = [];\n this.verticesItr = this.sceneVertices[Symbol.iterator]();\n this.result = {\n value: { x: 0, y: 0 },\n done: false,\n };\n }\n GridBasisIterable.prototype.set = function (grid, sceneVertices) {\n this.grid = grid;\n this.sceneVertices = sceneVertices;\n };\n GridBasisIterable.prototype[Symbol.iterator] = function () {\n this.verticesItr = this.sceneVertices[Symbol.iterator]();\n return this;\n };\n GridBasisIterable.prototype.next = function () {\n var next = this.verticesItr.next();\n if (next.done) {\n return next;\n }\n this.grid.convertToGridBasis(next.value, this.result.value);\n return this.result;\n };\n return GridBasisIterable;\n}());\n\n/**\n * Build cohesive regions from the non-obstacle space. It uses the data\n * from the obstacles rasterization {@link ObstacleRasterizer}.\n *\n * This implementation is strongly inspired from CritterAI class \"OpenHeightfieldBuilder\".\n *\n * Introduction to Height Fields: http://www.critterai.org/projects/nmgen_study/heightfields.html\n *\n * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html\n */\nvar RegionGenerator = /** @class */ (function () {\n function RegionGenerator() {\n this.obstacleRegionBordersCleaner = new ObstacleRegionBordersCleaner();\n this.floodedCells = new Array(1024);\n this.workingStack = new Array(1024);\n }\n //TODO implement the smoothing pass on the distance field?\n /**\n * Groups cells into cohesive regions using an watershed based algorithm.\n *\n * This operation depends on neighbor and distance field information.\n * So {@link RegionGenerator.generateDistanceField} operations must be\n * run before this operation.\n *\n * @param grid A field with cell distance information fully generated.\n * @param obstacleCellPadding a padding in cells to apply around the\n * obstacles.\n */\n RegionGenerator.prototype.generateRegions = function (grid, obstacleCellPadding) {\n // Watershed Algorithm\n //\n // Reference: http://en.wikipedia.org/wiki/Watershed_%28algorithm%29\n // A good visualization:\n // http://artis.imag.fr/Publications/2003/HDS03/ (PDF)\n //\n // Summary:\n //\n // This algorithm utilizes the cell.distanceToObstacle value, which\n // is generated by the generateDistanceField() operation.\n //\n // Using the watershed analogy, the cells which are furthest from\n // a border (highest distance to border) represent the lowest points\n // in the watershed. A border cell represents the highest possible\n // water level.\n //\n // The main loop iterates, starting at the lowest point in the\n // watershed, then incrementing with each loop until the highest\n // allowed water level is reached. This slowly \"floods\" the cells\n // starting at the lowest points.\n //\n // During each iteration of the loop, cells that are below the\n // current water level are located and an attempt is made to either\n // add them to exiting regions or create new regions from them.\n //\n // During the region expansion phase, if a newly flooded cell\n // borders on an existing region, it is usually added to the region.\n //\n // Any newly flooded cell that survives the region expansion phase\n // is used as a seed for a new region.\n //\n // At the end of the main loop, a final region expansion is\n // performed which should catch any stray cells that escaped region\n // assignment during the main loop.\n // Represents the minimum distance to an obstacle that is considered\n // traversable. I.e. Can't traverse cells closer than this distance\n // to a border. This provides a way of artificially capping the\n // height to which watershed flooding can occur.\n // I.e. Don't let the algorithm flood all the way to the actual border.\n //\n // We add the minimum border distance to take into account the\n // blurring algorithm which can result in a border cell having a\n // border distance > 0.\n var distanceMin = obstacleCellPadding * 2;\n // TODO: EVAL: Figure out why this iteration limit is needed\n // (todo from the CritterAI sources).\n var expandIterations = 4 + distanceMin * 2;\n // Zero is reserved for the obstacle-region. So initializing to 1.\n var nextRegionID = 1;\n var floodedCells = this.floodedCells;\n // Search until the current distance reaches the minimum allowed\n // distance.\n //\n // Note: This loop will not necessarily complete all region\n // assignments. This is OK since a final region assignment step\n // occurs after the loop iteration is complete.\n for (\n // This value represents the current distance from the border which\n // is to be searched. The search starts at the maximum distance then\n // moves toward zero (toward borders).\n //\n // This number will always be divisible by 2.\n var distance = grid.obstacleDistanceMax() & ~1; distance > distanceMin; distance = Math.max(distance - 2, 0)) {\n // Find all cells that are at or below the current \"water level\"\n // and are not already assigned to a region. Add these cells to\n // the flooded cell list for processing.\n floodedCells.length = 0;\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.regionID === RasterizationCell.NULL_REGION_ID &&\n cell.distanceToObstacle >= distance) {\n // The cell is not already assigned a region and is\n // below the current \"water level\". So the cell can be\n // considered for region assignment.\n floodedCells.push(cell);\n }\n }\n }\n if (nextRegionID > 1) {\n // At least one region has already been created, so first\n // try to put the newly flooded cells into existing regions.\n if (distance > 0) {\n this.expandRegions(grid, floodedCells, expandIterations);\n }\n else {\n this.expandRegions(grid, floodedCells, -1);\n }\n }\n // Create new regions for all cells that could not be added to\n // existing regions.\n for (var _i = 0, floodedCells_1 = floodedCells; _i < floodedCells_1.length; _i++) {\n var floodedCell = floodedCells_1[_i];\n if (!floodedCell ||\n floodedCell.regionID !== RasterizationCell.NULL_REGION_ID) {\n // This cell was assigned to a newly created region\n // during an earlier iteration of this loop.\n // So it can be skipped.\n continue;\n }\n // Fill to slightly more than the current \"water level\".\n // This improves efficiency of the algorithm.\n // And it is necessary with the conservative expansion to ensure that\n // more than one cell is added initially to a new regions otherwise\n // no cell could be added to it later because of the conservative\n // constraint.\n var fillTo = Math.max(distance - 2, distanceMin + 1, 1);\n if (this.floodNewRegion(grid, floodedCell, fillTo, nextRegionID)) {\n nextRegionID++;\n }\n }\n }\n // Find all cells that haven't been assigned regions by the main loop\n // (up to the minimum distance).\n floodedCells.length = 0;\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.distanceToObstacle > distanceMin &&\n cell.regionID === RasterizationCell.NULL_REGION_ID) {\n // Not a border or obstacle region cell. Should be in a region.\n floodedCells.push(cell);\n }\n }\n }\n // Perform a final expansion of existing regions.\n // Allow more iterations than normal for this last expansion.\n if (distanceMin > 0) {\n this.expandRegions(grid, floodedCells, expandIterations * 8);\n }\n else {\n this.expandRegions(grid, floodedCells, -1);\n }\n grid.regionCount = nextRegionID;\n this.obstacleRegionBordersCleaner.fixObstacleRegion(grid);\n //TODO Also port FilterOutSmallRegions?\n // The algorithm to remove vertices in the middle (added at the end of\n // ContourBuilder.buildContours) may already filter them and contour are\n // faster to process than cells.\n };\n /**\n * Attempts to find the most appropriate regions to attach cells to.\n *\n * Any cells successfully attached to a region will have their list\n * entry set to null. So any non-null entries in the list will be cells\n * for which a region could not be determined.\n *\n * @param grid\n * @param inoutCells As input, the list of cells available for formation\n * of new regions. As output, the cells that could not be assigned\n * to new regions.\n * @param maxIterations If set to -1, will iterate through completion.\n */\n RegionGenerator.prototype.expandRegions = function (grid, inoutCells, iterationMax) {\n if (inoutCells.length === 0)\n return;\n var skipped = 0;\n for (var iteration = 0; (iteration < iterationMax || iterationMax === -1) &&\n // All cells have either been processed or could not be\n // processed during the last cycle.\n skipped < inoutCells.length; iteration++) {\n // The number of cells in the working list that have been\n // successfully processed or could not be processed successfully\n // for some reason.\n // This value controls when iteration ends.\n skipped = 0;\n for (var index = 0; index < inoutCells.length; index++) {\n var cell = inoutCells[index];\n if (cell === null) {\n // The cell originally at this index location has\n // already been successfully assigned a region. Nothing\n // else to do with it.\n skipped++;\n continue;\n }\n // Default to unassigned.\n var cellRegion = RasterizationCell.NULL_REGION_ID;\n var regionCenterDist = Number.MAX_VALUE;\n for (var _i = 0, _a = RasterizationGrid.neighbor4Deltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.regionID !== RasterizationCell.NULL_REGION_ID) {\n if (neighbor.distanceToRegionCore + 2 < regionCenterDist) {\n // This neighbor is closer to its region core\n // than previously detected neighbors.\n // Conservative expansion constraint:\n // Check to ensure that this neighbor has\n // at least two other neighbors in its region.\n // This makes sure that adding this cell to\n // this neighbor's region will not result\n // in a single width line of cells.\n var sameRegionCount = 0;\n for (var neighborDirection = 0; neighborDirection < 4; neighborDirection++) {\n var nnCell = grid.getNeighbor(neighbor, neighborDirection);\n // There is a diagonal-neighbor\n if (nnCell.regionID === neighbor.regionID) {\n // This neighbor has a neighbor in\n // the same region.\n sameRegionCount++;\n }\n }\n if (sameRegionCount > 1) {\n cellRegion = neighbor.regionID;\n regionCenterDist = neighbor.distanceToRegionCore + 2;\n }\n }\n }\n }\n if (cellRegion !== RasterizationCell.NULL_REGION_ID) {\n // Found a suitable region for this cell to belong to.\n // Mark this index as having been processed.\n inoutCells[index] = null;\n cell.regionID = cellRegion;\n cell.distanceToRegionCore = regionCenterDist;\n }\n else {\n // Could not find an existing region for this cell.\n skipped++;\n }\n }\n }\n };\n /**\n * Creates a new region surrounding a cell, adding neighbor cells to the\n * new region as appropriate.\n *\n * The new region creation will fail if the root cell is on the\n * border of an existing region.\n *\n * All cells added to the new region as part of this process become\n * \"core\" cells with a distance to region core of zero.\n *\n * @param grid\n * @param rootCell The cell used to seed the new region.\n * @param fillToDist The watershed distance to flood to.\n * @param regionID The region ID to use for the new region\n * (if creation is successful).\n * @return true if a new region was created.\n */\n RegionGenerator.prototype.floodNewRegion = function (grid, rootCell, fillToDist, regionID) {\n var workingStack = this.workingStack;\n workingStack.length = 0;\n workingStack.push(rootCell);\n rootCell.regionID = regionID;\n rootCell.distanceToRegionCore = 0;\n var regionSize = 0;\n var cell;\n while ((cell = workingStack.pop())) {\n // Check regions of neighbor cells.\n //\n // If any neighbor is found to have a region assigned, then\n // the current cell can't be in the new region\n // (want standard flooding algorithm to handle deciding which\n // region this cell should go in).\n //\n // Up to 8 neighbors are checked.\n //\n // Neighbor searches:\n // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch\n var isOnRegionBorder = false;\n for (var _i = 0, _a = RasterizationGrid.neighbor8Deltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n isOnRegionBorder =\n neighbor.regionID !== RasterizationCell.NULL_REGION_ID &&\n neighbor.regionID !== regionID;\n if (isOnRegionBorder)\n break;\n }\n if (isOnRegionBorder) {\n cell.regionID = RasterizationCell.NULL_REGION_ID;\n continue;\n }\n regionSize++;\n // If got this far, we know the current cell is part of the new\n // region. Now check its neighbors to see if they should be\n // assigned to this new region.\n for (var _b = 0, _c = RasterizationGrid.neighbor4Deltas; _b < _c.length; _b++) {\n var delta = _c[_b];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.distanceToObstacle >= fillToDist &&\n neighbor.regionID === RasterizationCell.NULL_REGION_ID) {\n neighbor.regionID = regionID;\n neighbor.distanceToRegionCore = 0;\n workingStack.push(neighbor);\n }\n }\n }\n return regionSize > 0;\n };\n /**\n * Generates distance field information.\n * The {@link RasterizationCell.distanceToObstacle} information is generated\n * for all cells in the field.\n *\n * All distance values are relative and do not represent explicit\n * distance values (such as grid unit distance). The algorithm which is\n * used results in an approximation only. It is not exhaustive.\n *\n * The data generated by this operation is required by\n * {@link RegionGenerator.generateRegions}.\n *\n * @param grid A field with cells obstacle information already generated.\n */\n RegionGenerator.prototype.generateDistanceField = function (grid) {\n // close borders\n for (var x = 0; x < grid.dimX(); x++) {\n var leftCell = grid.get(x, 0);\n leftCell.distanceToObstacle = 0;\n var rightCell = grid.get(x, grid.dimY() - 1);\n rightCell.distanceToObstacle = 0;\n }\n for (var y = 1; y < grid.dimY() - 1; y++) {\n var topCell = grid.get(0, y);\n topCell.distanceToObstacle = 0;\n var bottomCell = grid.get(grid.dimX() - 1, y);\n bottomCell.distanceToObstacle = 0;\n }\n // The next two phases basically check the neighbors of a cell and\n // set the cell's distance field to be slightly greater than the\n // neighbor with the lowest border distance. Distance is increased\n // slightly more for diagonal-neighbors than for axis-neighbors.\n // 1st pass\n // During this pass, the following neighbors are checked:\n // (-1, 0) (-1, -1) (0, -1) (1, -1)\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n for (var _i = 0, _a = RegionGenerator.firstPassDeltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +\n delta.distance;\n if (cell.distanceToObstacle > distanceByNeighbor) {\n cell.distanceToObstacle = distanceByNeighbor;\n }\n }\n }\n }\n // 2nd pass\n // During this pass, the following neighbors are checked:\n // (1, 0) (1, 1) (0, 1) (-1, 1)\n //\n // Besides checking different neighbors, this pass performs its\n // grid search in reverse order.\n for (var y = grid.dimY() - 2; y >= 1; y--) {\n for (var x = grid.dimX() - 2; x >= 1; x--) {\n var cell = grid.get(x, y);\n for (var _b = 0, _c = RegionGenerator.secondPassDeltas; _b < _c.length; _b++) {\n var delta = _c[_b];\n var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +\n delta.distance;\n if (cell.distanceToObstacle > distanceByNeighbor) {\n cell.distanceToObstacle = distanceByNeighbor;\n }\n }\n }\n }\n };\n RegionGenerator.firstPassDeltas = [\n { x: -1, y: 0, distance: 2 },\n { x: -1, y: -1, distance: 3 },\n { x: 0, y: -1, distance: 2 },\n { x: 1, y: -1, distance: 3 },\n ];\n RegionGenerator.secondPassDeltas = [\n { x: 1, y: 0, distance: 2 },\n { x: 1, y: 1, distance: 3 },\n { x: 0, y: 1, distance: 2 },\n { x: -1, y: 1, distance: 3 },\n ];\n return RegionGenerator;\n}());\n/**\n * Implements three algorithms that clean up issues that can\n * develop around obstacle region boarders.\n *\n * - Detect and fix encompassed obstacle regions:\n *\n * If a obstacle region is found that is fully encompassed by a single\n * region, then the region will be split into two regions at the\n * obstacle region border.\n *\n * - Detect and fix \"short wrapping\" of obstacle regions:\n *\n * Regions can sometimes wrap slightly around the corner of a obstacle region\n * in a manner that eventually results in the formation of self-intersecting\n * polygons.\n *\n * Example: Before the algorithm is applied:\n * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_08_cornerwrapbefore.jpg\"\n *\n * Example: After the algorithm is applied:\n * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_09_cornerwrapafter.jpg\n *\n * - Detect and fix incomplete obstacle region connections:\n *\n * If a region touches obstacle region only diagonally, then contour detection\n * algorithms may not properly detect the obstacle region connection. This can\n * adversely effect other algorithms in the pipeline.\n *\n * Example: Before algorithm is applied:\n *\n * b b a a a a\n * b b a a a a\n * a a x x x x\n * a a x x x x\n *\n * Example: After algorithm is applied:\n *\n * b b a a a a\n * b b b a a a <-- Cell transferred to region B.\n * a a x x x x\n * a a x x x x\n *\n *\n * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html\n */\nvar ObstacleRegionBordersCleaner = /** @class */ (function () {\n function ObstacleRegionBordersCleaner() {\n this.workingUpLeftOpenCells = new Array(512);\n this.workingDownRightOpenCells = new Array(512);\n this.workingOpenCells = new Array(512);\n }\n /**\n * This operation utilizes {@link RasterizationCell.contourFlags}. It\n * expects the value to be zero on entry, and re-zero's the value\n * on exit.\n *\n * @param grid a grid with fully built regions.\n */\n ObstacleRegionBordersCleaner.prototype.fixObstacleRegion = function (grid) {\n var workingUpLeftOpenCells = this.workingUpLeftOpenCells;\n workingUpLeftOpenCells.length = 0;\n var workingDownRightOpenCells = this.workingDownRightOpenCells;\n workingDownRightOpenCells.length = 0;\n var workingOpenCells = this.workingOpenCells;\n workingOpenCells.length = 0;\n var extremeCells = [\n null,\n null,\n ];\n var nextRegionID = grid.regionCount;\n // Iterate over the cells, trying to find obstacle region borders.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.contourFlags !== 0)\n // Cell was processed in a previous iteration.\n // Ignore it.\n continue;\n cell.contourFlags = 1;\n var workingCell = null;\n var edgeDirection = -1;\n if (cell.regionID !== RasterizationCell.OBSTACLE_REGION_ID) {\n // Not interested in this cell.\n continue;\n }\n // This is a obstacle region cell. See if it\n // connects to a cell in a non-obstacle region.\n edgeDirection = this.getNonNullBorderDirection(grid, cell);\n if (edgeDirection === -1)\n // This cell is not a border cell. Ignore it.\n continue;\n // This is a border cell. Step into the non-null\n // region and swing the direction around 180 degrees.\n workingCell = grid.getNeighbor(cell, edgeDirection);\n edgeDirection = (edgeDirection + 2) & 0x3;\n // Process the obstacle region contour. Detect and fix\n // local issues. Determine if the region is\n // fully encompassed by a single non-obstacle region.\n var isEncompassedNullRegion = this.processNullRegion(grid, workingCell, edgeDirection, extremeCells);\n if (isEncompassedNullRegion) {\n // This cell is part of a group of obstacle region cells\n // that is encompassed within a single non-obstacle region.\n // This is not permitted. Need to fix it.\n this.partialFloodRegion(grid, extremeCells[0], extremeCells[1], nextRegionID);\n nextRegionID++;\n }\n }\n }\n grid.regionCount = nextRegionID;\n // Clear all flags.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n cell.contourFlags = 0;\n }\n }\n };\n /**\n * Partially flood a region away from the specified direction.\n *\n * {@link RasterizationCell.contourFlags}\n * is set to zero for all flooded cells.\n *\n * @param grid\n * @param startCell The cell to start the flood from.\n * @param borderDirection The hard border for flooding. No\n * cells in this direction from the startCell will be flooded.\n * @param newRegionID The region id to assign the flooded\n * cells to.\n */\n ObstacleRegionBordersCleaner.prototype.partialFloodRegion = function (grid, upLeftCell, downRightCell, newRegionID) {\n var upLeftOpenCells = this.workingUpLeftOpenCells;\n var downRightOpenCells = this.workingDownRightOpenCells;\n var workingOpenCells = this.workingOpenCells;\n // The implementation differs from CritterAI to avoid non-contiguous\n // sections. Instead of brushing in one direction, it floods from\n // 2 extremities of the encompassed obstacle region.\n var regionID = upLeftCell.regionID;\n if (regionID === newRegionID) {\n // avoid infinity loop\n console.error(\"Can't create a new region with an ID that already exist.\");\n return;\n }\n // The 1st flooding set a new the regionID\n upLeftCell.regionID = newRegionID;\n upLeftCell.distanceToRegionCore = 0; // This information is lost.\n upLeftOpenCells.length = 0;\n upLeftOpenCells.push(upLeftCell);\n // The 2nd flooding keep the regionID and mark the cell as visited.\n downRightCell.contourFlags = 2;\n downRightCell.distanceToRegionCore = 0; // This information is lost.\n downRightOpenCells.length = 0;\n downRightOpenCells.push(downRightCell);\n var swap;\n workingOpenCells.length = 0;\n while (upLeftOpenCells.length !== 0 || downRightOpenCells.length !== 0) {\n for (var _i = 0, upLeftOpenCells_1 = upLeftOpenCells; _i < upLeftOpenCells_1.length; _i++) {\n var cell = upLeftOpenCells_1[_i];\n for (var direction = 0; direction < 4; direction++) {\n var neighbor = grid.getNeighbor(cell, direction);\n if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {\n continue;\n }\n // Transfer the neighbor to the new region.\n neighbor.regionID = newRegionID;\n neighbor.distanceToRegionCore = 0; // This information is lost.\n workingOpenCells.push(neighbor);\n }\n }\n // This allows to flood the nearest cells first without needing lifo queue.\n // But a queue would take less memory.\n swap = upLeftOpenCells;\n upLeftOpenCells = workingOpenCells;\n workingOpenCells = swap;\n workingOpenCells.length = 0;\n for (var _a = 0, downRightOpenCells_1 = downRightOpenCells; _a < downRightOpenCells_1.length; _a++) {\n var cell = downRightOpenCells_1[_a];\n for (var direction = 0; direction < 4; direction++) {\n var neighbor = grid.getNeighbor(cell, direction);\n if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {\n continue;\n }\n // Keep the neighbor to the current region.\n neighbor.contourFlags = 2;\n neighbor.distanceToRegionCore = 0; // This information is lost.\n workingOpenCells.push(neighbor);\n }\n }\n swap = downRightOpenCells;\n downRightOpenCells = workingOpenCells;\n workingOpenCells = swap;\n workingOpenCells.length = 0;\n }\n };\n /**\n * Detects and fixes bad cell configurations in the vicinity of a\n * obstacle region contour (See class description for details).\n * @param grid\n * @param startCell A cell in a non-obstacle region that borders a null\n * region.\n * @param startDirection The direction of the obstacle region border.\n * @return TRUE if the start cell's region completely encompasses\n * the obstacle region.\n */\n ObstacleRegionBordersCleaner.prototype.processNullRegion = function (grid, startCell, startDirection, extremeCells) {\n // This algorithm traverses the contour. As it does so, it detects\n // and fixes various known dangerous cell configurations.\n //\n // Traversing the contour: A good way to visualize it is to think\n // of a robot sitting on the floor facing a known wall. It then\n // does the following to skirt the wall:\n // 1. If there is a wall in front of it, turn clockwise in 90 degrees\n // increments until it finds the wall is gone.\n // 2. Move forward one step.\n // 3. Turn counter-clockwise by 90 degrees.\n // 4. Repeat from step 1 until it finds itself at its original\n // location facing its original direction.\n //\n // See also: http://www.critterai.org/projects/nmgen_study/regiongen.html#robotwalk\n //\n // As the traversal occurs, the number of acute (90 degree) and\n // obtuse (270 degree) corners are monitored. If a complete contour is\n // detected and (obtuse corners > acute corners), then the null\n // region is inside the contour. Otherwise the obstacle region is\n // outside the contour, which we don't care about.\n var borderRegionID = startCell.regionID;\n // Prepare for loop.\n var cell = startCell;\n var neighbor = null;\n var direction = startDirection;\n var upLeftCell = cell;\n var downRightCell = cell;\n // Initialize monitoring variables.\n var loopCount = 0;\n var acuteCornerCount = 0;\n var obtuseCornerCount = 0;\n var stepsWithoutBorder = 0;\n var borderSeenLastLoop = false;\n var isBorder = true; // Initial value doesn't matter.\n // Assume a single region is connected to the obstacle region\n // until proven otherwise.\n var hasSingleConnection = true;\n // The loop limit exists for the sole reason of preventing\n // an infinite loop in case of bad input data.\n // It is set to a very high value because there is no way of\n // definitively determining a safe smaller value. Setting\n // the value too low can result in rescanning a contour\n // multiple times, killing performance.\n while (++loopCount < 1 << 30) {\n // Get the cell across the border.\n neighbor = grid.getNeighbor(cell, direction);\n // Detect which type of edge this direction points across.\n if (neighbor === null) {\n // It points across a obstacle region border edge.\n isBorder = true;\n }\n else {\n // We never need to perform contour detection\n // on this cell again. So mark it as processed.\n neighbor.contourFlags = 1;\n if (neighbor.regionID === RasterizationCell.OBSTACLE_REGION_ID) {\n // It points across a obstacle region border edge.\n isBorder = true;\n }\n else {\n // This isn't a obstacle region border.\n isBorder = false;\n if (neighbor.regionID !== borderRegionID)\n // It points across a border to a non-obstacle region.\n // This means the current contour can't\n // represent a fully encompassed obstacle region.\n hasSingleConnection = false;\n }\n }\n // Process the border.\n if (isBorder) {\n // It is a border edge.\n if (borderSeenLastLoop) {\n // A border was detected during the last loop as well.\n // Two detections in a row indicates we passed an acute\n // (inner) corner.\n //\n // a x\n // x x\n acuteCornerCount++;\n }\n else if (stepsWithoutBorder > 1) {\n // We have moved at least two cells before detecting\n // a border. This indicates we passed an obtuse\n // (outer) corner.\n //\n // a a\n // a x\n obtuseCornerCount++;\n stepsWithoutBorder = 0;\n // Detect and fix cell configuration issue around this\n // corner.\n if (this.processOuterCorner(grid, cell, direction))\n // A change was made and it resulted in the\n // corner area having multiple region connections.\n hasSingleConnection = false;\n }\n direction = (direction + 1) & 0x3; // Rotate in clockwise direction.\n borderSeenLastLoop = true;\n stepsWithoutBorder = 0;\n }\n else {\n // Not a obstacle region border.\n // Move to the neighbor and swing the search direction back\n // one increment (counterclockwise). By moving the direction\n // back one increment we guarantee we don't miss any edges.\n cell = neighbor;\n direction = (direction + 3) & 0x3; // Rotate counterclockwise direction.\n borderSeenLastLoop = false;\n stepsWithoutBorder++;\n if (cell.x < upLeftCell.x ||\n (cell.x === upLeftCell.x && cell.y < upLeftCell.y)) {\n upLeftCell = cell;\n }\n if (cell.x > downRightCell.x ||\n (cell.x === downRightCell.x && cell.y > downRightCell.y)) {\n downRightCell = cell;\n }\n }\n if (startCell === cell && startDirection === direction) {\n extremeCells[0] = upLeftCell;\n extremeCells[1] = downRightCell;\n // Have returned to the original cell and direction.\n // The search is complete.\n // Is the obstacle region inside the contour?\n return hasSingleConnection && obtuseCornerCount > acuteCornerCount;\n }\n }\n // If got here then the obstacle region boarder is too large to be fully\n // explored. So it can't be encompassed.\n return false;\n };\n /**\n * Detects and fixes cell configuration issues in the vicinity\n * of obtuse (outer) obstacle region corners.\n * @param grid\n * @param referenceCell The cell in a non-obstacle region that is\n * just past the outer corner.\n * @param borderDirection The direction of the obstacle region border.\n * @return TRUE if more than one region connects to the obstacle region\n * in the vicinity of the corner (this may or may not be due to\n * a change made by this operation).\n */\n ObstacleRegionBordersCleaner.prototype.processOuterCorner = function (grid, referenceCell, borderDirection) {\n var hasMultiRegions = false;\n // Get the previous two cells along the border.\n var backOne = grid.getNeighbor(referenceCell, (borderDirection + 3) & 0x3);\n var backTwo = grid.getNeighbor(backOne, borderDirection);\n var testCell;\n if (backOne.regionID !== referenceCell.regionID &&\n // This differ from the CritterAI implementation.\n // To filter vertices in the middle, this must be avoided too:\n // a x\n // b c\n backTwo.regionID !== backOne.regionID) {\n // Dangerous corner configuration.\n //\n // a x\n // b a\n //\n // Need to change to one of the following configurations:\n //\n // b x a x\n // b a b b\n //\n // Reason: During contour detection this type of configuration can\n // result in the region connection being detected as a\n // region-region portal, when it is not. The region connection\n // is actually interrupted by the obstacle region.\n //\n // This configuration has been demonstrated to result in\n // two regions being improperly merged to encompass an\n // internal obstacle region.\n //\n // Example:\n //\n // a a x x x a\n // a a x x a a\n // b b a a a a\n // b b a a a a\n //\n // During contour and connection detection for region b, at no\n // point will the obstacle region be detected. It will appear\n // as if a clean a-b portal exists.\n //\n // An investigation into fixing this issue via updates to the\n // watershed or contour detection algorithms did not turn\n // up a better way of resolving this issue.\n hasMultiRegions = true;\n // Determine how many connections backTwo has to backOne's region.\n testCell = grid.getNeighbor(backOne, (borderDirection + 3) & 0x3);\n var backTwoConnections = 0;\n if (testCell.regionID === backOne.regionID) {\n backTwoConnections++;\n testCell = grid.getNeighbor(testCell, borderDirection);\n if (testCell.regionID === backOne.regionID)\n backTwoConnections++;\n }\n // Determine how many connections the reference cell has\n // to backOne's region.\n var referenceConnections = 0;\n testCell = grid.getNeighbor(backOne, (borderDirection + 2) & 0x3);\n if (testCell.regionID === backOne.regionID) {\n referenceConnections++;\n testCell = grid.getNeighbor(testCell, (borderDirection + 2) & 0x3);\n if (testCell.regionID === backOne.regionID)\n backTwoConnections++;\n }\n // Change the region of the cell that has the most connections\n // to the target region.\n if (referenceConnections > backTwoConnections)\n referenceCell.regionID = backOne.regionID;\n else\n backTwo.regionID = backOne.regionID;\n }\n else if (backOne.regionID === referenceCell.regionID &&\n backTwo.regionID === referenceCell.regionID) {\n // Potential dangerous short wrap.\n //\n // a x\n // a a\n //\n // Example of actual problem configuration:\n //\n // b b x x\n // b a x x <- Short wrap.\n // b a a a\n //\n // In the above case, the short wrap around the corner of the\n // obstacle region has been demonstrated to cause self-intersecting\n // polygons during polygon formation.\n //\n // This algorithm detects whether or not one (and only one)\n // of the axis neighbors of the corner should be re-assigned to\n // a more appropriate region.\n //\n // In the above example, the following configuration is more\n // appropriate:\n //\n // b b x x\n // b b x x <- Change to this row.\n // b a a a\n // Check to see if backTwo should be in a different region.\n var selectedRegion = this.selectedRegionID(grid, backTwo, (borderDirection + 1) & 0x3, (borderDirection + 2) & 0x3);\n if (selectedRegion === backTwo.regionID) {\n // backTwo should not be re-assigned. How about\n // the reference cell?\n selectedRegion = this.selectedRegionID(grid, referenceCell, borderDirection, (borderDirection + 3) & 0x3);\n if (selectedRegion !== referenceCell.regionID) {\n // The reference cell should be reassigned\n // to a new region.\n referenceCell.regionID = selectedRegion;\n hasMultiRegions = true;\n }\n }\n else {\n // backTwo should be re-assigned to a new region.\n backTwo.regionID = selectedRegion;\n hasMultiRegions = true;\n }\n }\n else\n hasMultiRegions = true;\n // No dangerous configurations detected. But definitely\n // has a change in regions at the corner. We know this\n // because one of the previous checks looked for a single\n // region for all wrap cells.\n return hasMultiRegions;\n };\n /**\n * Checks the cell to see if it should be reassigned to a new region.\n *\n * @param grid\n * @param referenceCell A cell on one side of an obstacle region contour's\n * outer corner. It is expected that the all cells that wrap the\n * corner are in the same region.\n * @param borderDirection The direction of the obstacle region border.\n * @param cornerDirection The direction of the outer corner from the\n * reference cell.\n * @return The region the cell should be a member of. May be the\n * region the cell is currently a member of.\n */\n ObstacleRegionBordersCleaner.prototype.selectedRegionID = function (grid, referenceCell, borderDirection, cornerDirection) {\n // Initial example state:\n //\n // a - Known region.\n // x - Null region.\n // u - Unknown, not checked yet.\n //\n // u u u\n // u a x\n // u a a\n // The only possible alternate region id is from\n // the cell that is opposite the border. So check it first.\n var regionID = grid.getNeighbor(referenceCell, (borderDirection + 2) & 0x3)\n .regionID;\n if (regionID === referenceCell.regionID ||\n regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // The region away from the border is either a obstacle region\n // or the same region. So we keep the current region.\n //\n // u u u u u u\n // a a x or x a x <-- Potentially bad, but stuck with it.\n // u a a u a a\n return referenceCell.regionID;\n // Candidate region for re-assignment.\n var potentialRegion = regionID;\n // Next we check the region opposite from the corner direction.\n // If it is the current region, then we definitely can't\n // change the region id without risk of splitting the region.\n regionID = grid.getNeighbor(referenceCell, (cornerDirection + 2) & 0x3)\n .regionID;\n if (regionID === referenceCell.regionID ||\n regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // The region opposite from the corner direction is\n // either a obstacle region or the same region. So we\n // keep the current region.\n //\n // u a u u x u\n // b a x or b a x\n // u a a u a a\n return referenceCell.regionID;\n // We have checked the early exit special cases. Now a generalized\n // brute count is performed.\n //\n // Priority is given to the potential region. Here is why:\n // (Highly unlikely worst case scenario)\n //\n // c c c c c c\n // b a x -> b b x Select b even though b count == a count.\n // b a a b a a\n // Neighbors in potential region.\n // We know this will have a minimum value of 1.\n var potentialCount = 0;\n // Neighbors in the cell's current region.\n // We know this will have a minimum value of 2.\n var currentCount = 0;\n // Maximum edge case:\n //\n // b b b\n // b a x\n // b a a\n //\n // The maximum edge case for region A can't exist. It\n // is filtered out during one of the earlier special cases\n // handlers.\n //\n // Other cases may exist if more regions are involved.\n // Such cases will tend to favor the current region.\n for (var direction = 0; direction < 8; direction++) {\n var regionID_1 = grid.getNeighbor(referenceCell, direction).regionID;\n if (regionID_1 === referenceCell.regionID)\n currentCount++;\n else if (regionID_1 === potentialRegion)\n potentialCount++;\n }\n return potentialCount < currentCount\n ? referenceCell.regionID\n : potentialRegion;\n };\n /**\n * Returns the direction of the first neighbor in a non-obstacle region.\n * @param grid\n * @param cell The cell to check.\n * @return The direction of the first neighbor in a non-obstacle region, or\n * -1 if all neighbors are in the obstacle region.\n */\n ObstacleRegionBordersCleaner.prototype.getNonNullBorderDirection = function (grid, cell) {\n // Search axis-neighbors.\n for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {\n var delta = RasterizationGrid.neighbor4Deltas[direction];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.regionID !== RasterizationCell.OBSTACLE_REGION_ID)\n // The neighbor is a obstacle region.\n return direction;\n }\n // All neighbors are in a non-obstacle region.\n return -1;\n };\n return ObstacleRegionBordersCleaner;\n}());\n\n// This implementation is strongly inspired from a Java one\n// by Stephen A. Pratt:\n// http://www.critterai.org/projects/nmgen_study/\n//\n// Most of the comments were written by him and were adapted to fit this implementation.\n// This implementation differs a bit from the original:\n// - it's only 2D instead of 3D\n// - it has less features (see TODO) and might have lesser performance\n// - it uses objects for points instead of pointer-like in arrays of numbers\n// - the rasterization comes from other sources because of the 2d focus\n// - partialFloodRegion was rewritten to fix an issue\n// - filterNonObstacleVertices was added\n//\n// The Java implementation was also inspired from Recast that can be found here:\n// https://github.com/recastnavigation/recastnavigation\nvar NavMeshGenerator = /** @class */ (function () {\n function NavMeshGenerator(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, isometricRatio) {\n if (isometricRatio === void 0) { isometricRatio = 1; }\n this.grid = new RasterizationGrid(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, \n // make cells square in the world\n rasterizationCellSize / isometricRatio);\n this.isometricRatio = isometricRatio;\n this.obstacleRasterizer = new ObstacleRasterizer();\n this.regionGenerator = new RegionGenerator();\n this.contourBuilder = new ContourBuilder();\n this.convexPolygonGenerator = new ConvexPolygonGenerator();\n this.gridCoordinateConverter = new GridCoordinateConverter();\n }\n NavMeshGenerator.prototype.buildNavMesh = function (obstacles, obstacleCellPadding) {\n var _this = this;\n this.grid.clear();\n this.obstacleRasterizer.rasterizeObstacles(this.grid, obstacles);\n this.regionGenerator.generateDistanceField(this.grid);\n this.regionGenerator.generateRegions(this.grid, obstacleCellPadding);\n // It's probably not a good idea to expose the vectorization threshold.\n // As stated in the parameter documentation, the value 1 gives good\n // results in any situations.\n var threshold = 1;\n var contours = this.contourBuilder.buildContours(this.grid, threshold);\n var meshField = this.convexPolygonGenerator.splitToConvexPolygons(contours, 16);\n var scaledMeshField = this.gridCoordinateConverter.convertFromGridBasis(this.grid, meshField);\n if (this.isometricRatio != 1) {\n // Rescale the mesh to have the same unit length on the 2 axis for the pathfinding.\n scaledMeshField.forEach(function (polygon) {\n return polygon.forEach(function (point) {\n point.y *= _this.isometricRatio;\n });\n });\n }\n return scaledMeshField;\n };\n return NavMeshGenerator;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * PathfindingObstaclesManager manages the common objects shared by objects having a\n * pathfinding behavior: In particular, the obstacles behaviors are required to declare\n * themselves (see `PathfindingObstaclesManager.addObstacle`) to the manager of their associated scene\n * (see `gdjs.NavMeshPathfindingRuntimeBehavior.obstaclesManagers`).\n */\nvar NavMeshPathfindingObstaclesManager = /** @class */ (function () {\n function NavMeshPathfindingObstaclesManager(instanceContainer, configuration) {\n /**\n * The navigation meshes by moving object size\n * (rounded on _cellSize)\n */\n this._navMeshes = new Map();\n /**\n * Used while NavMeshes update is disabled to remember to do the update\n * when it's enable back.\n */\n this._navMeshesAreUpToDate = true;\n /**\n * This allows to continue finding paths with the old NavMeshes while\n * moving obstacles.\n */\n this._navMeshesUpdateIsEnabled = true;\n var viewpoint = configuration._getViewpoint();\n if (viewpoint === 'Isometry 2:1 (26.565°)') {\n configuration._setIsometricRatio(2);\n }\n else if (viewpoint === 'True Isometry (30°)') {\n configuration._setIsometricRatio(Math.sqrt(3));\n }\n else {\n configuration._setIsometricRatio(1);\n }\n if (configuration._getCellSize() <= 0) {\n configuration._setCellSize(10);\n }\n if (configuration._getAreaLeftBound() === 0 &&\n configuration._getAreaTopBound() === 0 &&\n configuration._getAreaRightBound() === 0 &&\n configuration._getAreaBottomBound() === 0) {\n var game = instanceContainer.getGame();\n configuration._setAreaLeftBound(0);\n configuration._setAreaTopBound(0);\n configuration._setAreaRightBound(game.getGameResolutionWidth());\n configuration._setAreaBottomBound(game.getGameResolutionHeight());\n }\n this.configuration = configuration;\n this._obstacles = new Set();\n this._polygonIterableAdapter = new PolygonIterableAdapter();\n this._navMeshGenerator = new NavMeshGenerator(configuration._getAreaLeftBound(), configuration._getAreaTopBound(), configuration._getAreaRightBound(), configuration._getAreaBottomBound(), configuration._getCellSize(), \n // make cells square in the world\n configuration._getIsometricRatio());\n }\n /**\n * Get the obstacles manager of a scene.\n */\n NavMeshPathfindingObstaclesManager.getManager = function (instanceContainer) {\n // @ts-ignore\n return instanceContainer.navMeshPathfindingObstaclesManager;\n };\n NavMeshPathfindingObstaclesManager.getManagerOrCreate = function (instanceContainer, configuration) {\n // @ts-ignore\n if (!instanceContainer.navMeshPathfindingObstaclesManager) {\n // Create the shared manager if necessary.\n // @ts-ignore\n instanceContainer.navMeshPathfindingObstaclesManager = new NavMeshPathfindingObstaclesManager(instanceContainer, configuration);\n }\n // @ts-ignore\n return instanceContainer.navMeshPathfindingObstaclesManager;\n };\n NavMeshPathfindingObstaclesManager.prototype.setNavMeshesUpdateEnabled = function (navMeshesUpdateIsEnabled) {\n this._navMeshesUpdateIsEnabled = navMeshesUpdateIsEnabled;\n if (navMeshesUpdateIsEnabled && !this._navMeshesAreUpToDate) {\n this._navMeshes.clear();\n this._navMeshesAreUpToDate = true;\n }\n };\n /**\n * Add a obstacle to the list of existing obstacles.\n */\n NavMeshPathfindingObstaclesManager.prototype.addObstacle = function (pathfindingObstacleBehavior) {\n this._obstacles.add(pathfindingObstacleBehavior.behavior.owner);\n this.invalidateNavMesh();\n };\n /**\n * Remove a obstacle from the list of existing obstacles. Be sure that the obstacle was\n * added before.\n */\n NavMeshPathfindingObstaclesManager.prototype.removeObstacle = function (pathfindingObstacleBehavior) {\n this._obstacles.delete(pathfindingObstacleBehavior.behavior.owner);\n this.invalidateNavMesh();\n };\n NavMeshPathfindingObstaclesManager.prototype.invalidateNavMesh = function () {\n if (this._navMeshesUpdateIsEnabled) {\n this._navMeshes.clear();\n this._navMeshesAreUpToDate = true;\n }\n else {\n this._navMeshesAreUpToDate = false;\n }\n };\n NavMeshPathfindingObstaclesManager.prototype.getNavMesh = function (obstacleCellPadding) {\n var navMesh = this._navMeshes.get(obstacleCellPadding);\n if (!navMesh) {\n var navMeshPolygons = this._navMeshGenerator.buildNavMesh(this._getVerticesIterable(this._obstacles), obstacleCellPadding);\n navMesh = new NavMesh(navMeshPolygons);\n this._navMeshes.set(obstacleCellPadding, navMesh);\n }\n return navMesh;\n };\n NavMeshPathfindingObstaclesManager.prototype._getVerticesIterable = function (objects) {\n this._polygonIterableAdapter.set(objects);\n return this._polygonIterableAdapter;\n };\n return NavMeshPathfindingObstaclesManager;\n}());\n/**\n * Iterable that adapts `RuntimeObject` to `Iterable<{x: float y: float}>`.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar PolygonIterableAdapter = /** @class */ (function () {\n function PolygonIterableAdapter() {\n this.objects = [];\n this.objectsItr = this.objects[Symbol.iterator]();\n this.polygonsItr = [][Symbol.iterator]();\n this.pointIterableAdapter = new PointIterableAdapter();\n this.result = {\n value: this.pointIterableAdapter,\n done: false,\n };\n }\n PolygonIterableAdapter.prototype.set = function (objects) {\n this.objects = objects;\n };\n PolygonIterableAdapter.prototype[Symbol.iterator] = function () {\n this.objectsItr = this.objects[Symbol.iterator]();\n this.polygonsItr = [][Symbol.iterator]();\n return this;\n };\n PolygonIterableAdapter.prototype.next = function () {\n var polygonNext = this.polygonsItr.next();\n while (polygonNext.done) {\n var objectNext = this.objectsItr.next();\n if (objectNext.done) {\n // IteratorReturnResult require a defined value\n // even though the spec state otherwise.\n // So, this class can't be typed as an iterable.\n this.result.value = undefined;\n this.result.done = true;\n return this.result;\n }\n this.polygonsItr = objectNext.value.getHitBoxes().values();\n polygonNext = this.polygonsItr.next();\n }\n this.pointIterableAdapter.set(polygonNext.value.vertices);\n this.result.value = this.pointIterableAdapter;\n this.result.done = false;\n return this.result;\n };\n return PolygonIterableAdapter;\n}());\n/**\n * Iterable that adapts coordinates from `[int, int]` to `{x: int, y: int}`.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar PointIterableAdapter = /** @class */ (function () {\n function PointIterableAdapter() {\n this.vertices = [];\n this.verticesItr = this.vertices[Symbol.iterator]();\n this.result = {\n value: { x: 0, y: 0 },\n done: false,\n };\n }\n PointIterableAdapter.prototype.set = function (vertices) {\n this.vertices = vertices;\n };\n PointIterableAdapter.prototype[Symbol.iterator] = function () {\n this.verticesItr = this.vertices[Symbol.iterator]();\n return this;\n };\n PointIterableAdapter.prototype.next = function () {\n var next = this.verticesItr.next();\n if (next.done) {\n return next;\n }\n this.result.value.x = next.value[0];\n this.result.value.y = next.value[1];\n return this.result;\n };\n return PointIterableAdapter;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar NavMeshRenderer = /** @class */ (function () {\n function NavMeshRenderer() {\n /** Used to draw traces for debugging */\n this._lastUsedObstacleCellPadding = null;\n }\n NavMeshRenderer.prototype.setLastUsedObstacleCellPadding = function (lastUsedObstacleCellPadding) {\n this._lastUsedObstacleCellPadding = lastUsedObstacleCellPadding;\n };\n NavMeshRenderer.prototype.render = function (instanceContainer, shapePainter) {\n if (this._lastUsedObstacleCellPadding === null) {\n return;\n }\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n // TODO find a way to rebuild drawing only when necessary.\n // Draw the navigation mesh on a shape painter object for debugging purpose\n var navMesh = manager.getNavMesh(this._lastUsedObstacleCellPadding);\n for (var _i = 0, _a = navMesh.getPolygons(); _i < _a.length; _i++) {\n var navPoly = _a[_i];\n var polygon = navPoly.getPoints();\n if (polygon.length === 0)\n continue;\n for (var index = 1; index < polygon.length; index++) {\n // It helps to spot vertices with 180° between edges.\n shapePainter.drawCircle(polygon[index].x, polygon[index].y / isometricRatio, 3);\n }\n }\n for (var _b = 0, _c = navMesh.getPolygons(); _b < _c.length; _b++) {\n var navPoly = _c[_b];\n var polygon = navPoly.getPoints();\n if (polygon.length === 0)\n continue;\n shapePainter.beginFillPath(polygon[0].x, polygon[0].y / isometricRatio);\n for (var index = 1; index < polygon.length; index++) {\n shapePainter.drawPathLineTo(polygon[index].x, polygon[index].y / isometricRatio);\n }\n shapePainter.closePath();\n shapePainter.endFillPath();\n }\n };\n return NavMeshRenderer;\n}());\n\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar PathFollower = /** @class */ (function () {\n function PathFollower(configuration) {\n // Attributes used for traveling on the path:\n this._path = [];\n this._speed = 0;\n this._distanceOnSegment = 0;\n this._totalSegmentDistance = 0;\n this._currentSegment = 0;\n this._movementAngle = 0;\n this.configuration = configuration;\n }\n PathFollower.prototype.setSpeed = function (speed) {\n this._speed = speed;\n };\n PathFollower.prototype.getSpeed = function () {\n return this._speed;\n };\n PathFollower.prototype.getMovementAngle = function () {\n return this._movementAngle;\n };\n PathFollower.prototype.movementAngleIsAround = function (degreeAngle, tolerance) {\n return (Math.abs(gdjs.evtTools.common.angleDifference(this._movementAngle, degreeAngle)) <= tolerance);\n };\n PathFollower.prototype.getNodeX = function (index) {\n if (index < this._path.length) {\n return this._path[index][0];\n }\n return 0;\n };\n PathFollower.prototype.getNodeY = function (index) {\n if (index < this._path.length) {\n return this._path[index][1];\n }\n return 0;\n };\n PathFollower.prototype.getNextNodeIndex = function () {\n return Math.min(this._currentSegment + 1, this._path.length - 1);\n };\n PathFollower.prototype.getNodeCount = function () {\n return this._path.length;\n };\n PathFollower.prototype.getNextNodeX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);\n return this._path[nextIndex][0];\n };\n PathFollower.prototype.getNextNodeY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);\n return this._path[nextIndex][1];\n };\n PathFollower.prototype.getPreviousNodeX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var previousIndex = Math.min(this._currentSegment, this._path.length - 1);\n return this._path[previousIndex][0];\n };\n PathFollower.prototype.getPreviousNodeY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var previousIndex = Math.min(this._currentSegment, this._path.length - 1);\n return this._path[previousIndex][1];\n };\n PathFollower.prototype.getDestinationX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n return this._path[this._path.length - 1][0];\n };\n PathFollower.prototype.getDestinationY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n return (this._path[this._path.length - 1][1]);\n };\n /**\n * Return true if the object reached its destination.\n */\n PathFollower.prototype.destinationReached = function () {\n return this._currentSegment >= this._path.length - 1;\n };\n /**\n * Compute and move on the path to the specified destination.\n */\n PathFollower.prototype.setPath = function (path) {\n this._path = path;\n this._enterSegment(0);\n };\n PathFollower.prototype._enterSegment = function (segmentNumber) {\n if (this._path.length === 0) {\n return;\n }\n this._currentSegment = segmentNumber;\n if (this._currentSegment < this._path.length - 1) {\n var pathX = this._path[this._currentSegment + 1][0] -\n this._path[this._currentSegment][0];\n var pathY = this._path[this._currentSegment + 1][1] -\n this._path[this._currentSegment][1];\n this._totalSegmentDistance = Math.sqrt(pathX * pathX + pathY * pathY);\n this._distanceOnSegment = 0;\n this._movementAngle =\n (gdjs.toDegrees(Math.atan2(pathY, pathX)) + 360) % 360;\n }\n else {\n this._speed = 0;\n }\n };\n PathFollower.prototype.isMoving = function () {\n return !(this._path.length === 0 || this.destinationReached());\n };\n PathFollower.prototype.step = function (timeDelta) {\n if (this._path.length === 0 || this.destinationReached()) {\n return;\n }\n // Update the speed of the object\n var previousSpeed = this._speed;\n var maxSpeed = this.configuration._getMaxSpeed();\n if (this._speed !== maxSpeed) {\n this._speed += this.configuration._getAcceleration() * timeDelta;\n if (this._speed > maxSpeed) {\n this._speed = maxSpeed;\n }\n }\n // Update the time on the segment and change segment if needed\n // Use a Verlet integration to be frame rate independent.\n this._distanceOnSegment +=\n ((this._speed + previousSpeed) / 2) * timeDelta;\n var remainingDistanceOnSegment = this._totalSegmentDistance - this._distanceOnSegment;\n if (remainingDistanceOnSegment <= 0 &&\n this._currentSegment < this._path.length) {\n this._enterSegment(this._currentSegment + 1);\n this._distanceOnSegment = -remainingDistanceOnSegment;\n }\n };\n PathFollower.prototype.getX = function () {\n return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][0], this._path[this._currentSegment + 1][0], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][0];\n };\n PathFollower.prototype.getY = function () {\n return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][1], this._path[this._currentSegment + 1][1], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][1];\n };\n return PathFollower;\n}());\n\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar NavMeshPathfindingBehavior = /** @class */ (function () {\n function NavMeshPathfindingBehavior(behavior) {\n // Attributes used for traveling on the path:\n this._pathFound = false;\n this.behavior = behavior;\n this.pathFollower = new PathFollower(behavior);\n this.navMeshRenderer = new NavMeshRenderer();\n }\n /**\n * Return true if the latest call to moveTo succeeded.\n */\n NavMeshPathfindingBehavior.prototype.pathFound = function () {\n return this._pathFound;\n };\n /**\n * Compute and move on the path to the specified destination.\n */\n NavMeshPathfindingBehavior.prototype.moveTo = function (instanceContainer, x, y) {\n var owner = this.behavior.owner;\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n this._pathFound = true;\n this.pathFollower.setPath([[owner.getX(), owner.getY()], [x, y]]);\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n var cellSize = manager.configuration._getCellSize();\n var collisionShape = this.behavior._getCollisionShape();\n var extraBorder = this.behavior._getExtraBorder();\n var radiusSqMax = 0;\n if (collisionShape !== 'Dot at center') {\n var centerX = owner.getCenterXInScene();\n var centerY = owner.getCenterYInScene();\n for (var _i = 0, _a = owner.getHitBoxes(); _i < _a.length; _i++) {\n var hitBox = _a[_i];\n for (var _b = 0, _c = hitBox.vertices; _b < _c.length; _b++) {\n var vertex = _c[_b];\n var deltaX = vertex[0] - centerX;\n // to have the same unit on x and y\n var deltaY = (vertex[1] - centerY) * isometricRatio;\n var radiusSq = deltaX * deltaX + deltaY * deltaY;\n radiusSqMax = Math.max(radiusSq, radiusSqMax);\n }\n }\n }\n // Round to avoid to flicker between 2 NavMesh\n // because of trigonometry rounding errors.\n // Round the padding on cellSize to avoid almost identical NavMesh\n var obstacleCellPadding = Math.max(0, Math.round((Math.sqrt(radiusSqMax) + extraBorder) / cellSize));\n this.navMeshRenderer.setLastUsedObstacleCellPadding(obstacleCellPadding);\n var navMesh = manager.getNavMesh(obstacleCellPadding);\n // TODO avoid the path allocation\n var path = navMesh.findPath({\n x: owner.getX(),\n y: owner.getY() * isometricRatio,\n }, { x: x, y: y * isometricRatio }) || [];\n this._pathFound = path.length > 0;\n this.pathFollower.setPath(path.map(function (_a) {\n var x = _a.x, y = _a.y;\n return [x, y];\n }));\n };\n NavMeshPathfindingBehavior.prototype.doStepPreEvents = function (instanceContainer) {\n if (this.pathFollower.destinationReached()) {\n return;\n }\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n var owner = this.behavior.owner;\n var angleOffset = this.behavior._getAngleOffset();\n var angularMaxSpeed = this.behavior._getMaxSpeed();\n var rotateObject = this.behavior._getRotateObject();\n var timeDelta = owner.getElapsedTime(instanceContainer) / 1000;\n this.pathFollower.step(timeDelta);\n // Position object on the segment and update its angle\n var movementAngle = this.pathFollower.getMovementAngle();\n if (rotateObject &&\n owner.getAngle() !== movementAngle + angleOffset) {\n owner.rotateTowardAngle(movementAngle + angleOffset, angularMaxSpeed, instanceContainer);\n }\n owner.setX(this.pathFollower.getX());\n // In case of isometry, convert coords back in screen.\n owner.setY(this.pathFollower.getY() / isometricRatio);\n };\n return NavMeshPathfindingBehavior;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * NavMeshPathfindingObstacleRuntimeBehavior represents a behavior allowing objects to be\n * considered as a obstacle by objects having Pathfinding Behavior.\n */\nvar NavMeshPathfindingObstacleBehavior = /** @class */ (function () {\n function NavMeshPathfindingObstacleBehavior(instanceContainer, behavior) {\n this._oldX = 0;\n this._oldY = 0;\n this._oldWidth = 0;\n this._oldHeight = 0;\n this._registeredInManager = false;\n this.behavior = behavior;\n this._manager = NavMeshPathfindingObstaclesManager.getManagerOrCreate(instanceContainer, \n // @ts-ignore\n behavior._sharedData);\n //Note that we can't use getX(), getWidth()... of owner here:\n //The owner is not yet fully constructed.\n }\n NavMeshPathfindingObstacleBehavior.prototype.onDestroy = function () {\n if (this._manager && this._registeredInManager) {\n this._manager.removeObstacle(this);\n }\n };\n NavMeshPathfindingObstacleBehavior.prototype.doStepPreEvents = function (instanceContainer) {\n var owner = this.behavior.owner;\n //Make sure the obstacle is or is not in the obstacles manager.\n if (!this.behavior.activated() && this._registeredInManager) {\n this._manager.removeObstacle(this);\n this._registeredInManager = false;\n }\n else {\n if (this.behavior.activated() && !this._registeredInManager) {\n this._manager.addObstacle(this);\n this._registeredInManager = true;\n }\n }\n //Track changes in size or position\n if (this._oldX !== owner.getX() ||\n this._oldY !== owner.getY() ||\n this._oldWidth !== owner.getWidth() ||\n this._oldHeight !== owner.getHeight()) {\n if (this._registeredInManager) {\n this._manager.removeObstacle(this);\n this._manager.addObstacle(this);\n }\n this._oldX = owner.getX();\n this._oldY = owner.getY();\n this._oldWidth = owner.getWidth();\n this._oldHeight = owner.getHeight();\n }\n };\n NavMeshPathfindingObstacleBehavior.prototype.doStepPostEvents = function (instanceContainer) { };\n NavMeshPathfindingObstacleBehavior.prototype.onActivate = function () {\n if (this._registeredInManager) {\n return;\n }\n this._manager.addObstacle(this);\n this._registeredInManager = true;\n };\n NavMeshPathfindingObstacleBehavior.prototype.onDeActivate = function () {\n if (!this._registeredInManager) {\n return;\n }\n this._manager.removeObstacle(this);\n this._registeredInManager = false;\n };\n return NavMeshPathfindingObstacleBehavior;\n}());\n\ngdjs.__NavMeshPathfinding = gdjs.__NavMeshPathfinding || {};\ngdjs.__NavMeshPathfinding.NavMeshPathfindingBehavior = NavMeshPathfindingBehavior;\ngdjs.__NavMeshPathfinding.NavMeshPathfindingObstacleBehavior = NavMeshPathfindingObstacleBehavior;\n", "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -28216,6 +28217,492 @@ } ], "objectGroups": [] + }, + { + "description": "the cell size for obstacle collision mask rasterization. While an object is needed, this will apply to all objects using the behavior.", + "fullName": "Cell size", + "functionType": "ExpressionAndCondition", + "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "name": "CellSize", + "sentence": "the cell size", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::SharedPropertyCellSize()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "CellSize", + "name": "SetCellSize", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::SetSharedPropertyCellSize" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::InvalidateNavMesh" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the area left bound. The left bound of the area where objects can go. While an object is needed, this will apply to all objects using the behavior.", + "fullName": "Area left bound", + "functionType": "ExpressionAndCondition", + "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "name": "AreaLeftBound", + "sentence": "the area left bound", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::SharedPropertyAreaLeftBound()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "AreaLeftBound", + "name": "SetAreaLeftBound", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::SetSharedPropertyAreaLeftBound" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::InvalidateNavMesh" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the area top bound. The top bound of the area where objects can go. While an object is needed, this will apply to all objects using the behavior.", + "fullName": "Area top bound", + "functionType": "ExpressionAndCondition", + "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "name": "AreaTopBound", + "sentence": "the area top bound", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::SharedPropertyAreaTopBound()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "AreaTopBound", + "name": "SetAreaTopBound", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::SetSharedPropertyAreaTopBound" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::InvalidateNavMesh" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the area right bound. The right bound of the area where objects can go (default to the game resolution). While an object is needed, this will apply to all objects using the behavior.", + "fullName": "Area right bound", + "functionType": "ExpressionAndCondition", + "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "name": "AreaRightBound", + "sentence": "the area right bound", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::SharedPropertyAreaRightBound()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "AreaRightBound", + "name": "SetAreaRightBound", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::SetSharedPropertyAreaRightBound" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::InvalidateNavMesh" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "the area bottom bound. The bottom bound of the area where objects can go (default to the game resolution). While an object is needed, this will apply to all objects using the behavior.", + "fullName": "Area bottom bound", + "functionType": "ExpressionAndCondition", + "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "name": "AreaBottomBound", + "sentence": "the area bottom bound", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::SharedPropertyAreaBottomBound()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "ActionWithOperator", + "getterName": "AreaBottomBound", + "name": "SetAreaBottomBound", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::SetSharedPropertyAreaBottomBound" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"Value\")" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior::InvalidateNavMesh" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Invalidate navigation mesh.", + "fullName": "Invalidate navigation mesh", + "functionType": "Action", + "name": "InvalidateNavMesh", + "private": true, + "sentence": "Invalidate navigation mesh", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.obstacle._manager.invalidateNavMesh();\n", + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingObstacleBehavior", + "type": "behavior" + } + ], + "objectGroups": [] } ], "propertyDescriptors": [], From f036b0a57229d8660d09fd6f17e54c994397afb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sat, 26 Nov 2022 12:45:23 +0100 Subject: [PATCH 03/11] Wording --- examples/isometric-game/isometric-game.json | 50 ++++++++++----------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index bd96c447b..bbf3fdcb0 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -26653,7 +26653,7 @@ ], "eventsBasedBehaviors": [ { - "description": "Move the object to a target in straight lines while avoiding all objects that are flagged as obstacles.", + "description": "Move objects to a target in straight lines while avoiding all objects that are flagged as obstacles.", "fullName": "Navigation mesh pathfinding (experimental)", "name": "NavMeshPathfindingBehavior", "objectType": "", @@ -27400,7 +27400,7 @@ { "fullName": "Draw navigation mesh", "functionType": "Action", - "group": "Debug (navigation mesh)", + "group": "Debug", "name": "DrawNavMesh", "sentence": "Draw the navigation mesh used for _PARAM0_ on _PARAM2_", "events": [ @@ -27515,12 +27515,12 @@ "objectGroups": [] }, { - "description": "the max. speed of the object.", - "fullName": "Max. speed", + "description": "the maximum speed of the object.", + "fullName": "Maximum speed", "functionType": "ExpressionAndCondition", "group": "Pathfinding configuration (navigation mesh)", "name": "MaxSpeed", - "sentence": "the max. speed", + "sentence": "the maximum speed", "events": [ { "type": "BuiltinCommonInstructions::Standard", @@ -27596,12 +27596,12 @@ "objectGroups": [] }, { - "description": "the rotate speed of the object.", - "fullName": "Rotate speed", + "description": "the rotation speed of the object.", + "fullName": "Rotation speed", "functionType": "ExpressionAndCondition", "group": "Pathfinding configuration (navigation mesh)", "name": "AngularMaxSpeed", - "sentence": "the rotate speed", + "sentence": "the rotation speed", "events": [ { "type": "BuiltinCommonInstructions::Standard", @@ -28063,7 +28063,7 @@ { "value": "200", "type": "Number", - "label": "Max. speed", + "label": "Maximum speed", "description": "", "group": "", "extraInformation": [], @@ -28073,7 +28073,7 @@ { "value": "180", "type": "Number", - "label": "Rotate speed", + "label": "Rotation speed", "description": "", "group": "Rotation", "extraInformation": [], @@ -28127,8 +28127,8 @@ "sharedPropertyDescriptors": [] }, { - "description": "Flag the object as being an obstacle for pathfinding.", - "fullName": "Obstacle for pathfinding (navigation mesh, experimental)", + "description": "Flag objects as being an obstacle for pathfinding.", + "fullName": "Obstacle for navigation mesh pathfinding (experimental)", "name": "NavMeshPathfindingObstacleBehavior", "objectType": "", "eventsFunctions": [ @@ -28222,7 +28222,7 @@ "description": "the cell size for obstacle collision mask rasterization. While an object is needed, this will apply to all objects using the behavior.", "fullName": "Cell size", "functionType": "ExpressionAndCondition", - "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "group": "Navigation mesh configuration", "name": "CellSize", "sentence": "the cell size", "events": [ @@ -28310,10 +28310,10 @@ "objectGroups": [] }, { - "description": "the area left bound. The left bound of the area where objects can go. While an object is needed, this will apply to all objects using the behavior.", + "description": "the area left bound. The left bound of the area where objects can go in the scene. While an object is needed, this will apply to all objects using the behavior.", "fullName": "Area left bound", "functionType": "ExpressionAndCondition", - "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "group": "Navigation mesh configuration", "name": "AreaLeftBound", "sentence": "the area left bound", "events": [ @@ -28401,10 +28401,10 @@ "objectGroups": [] }, { - "description": "the area top bound. The top bound of the area where objects can go. While an object is needed, this will apply to all objects using the behavior.", + "description": "the area top bound. The top bound of the area where objects can go in the scene. While an object is needed, this will apply to all objects using the behavior.", "fullName": "Area top bound", "functionType": "ExpressionAndCondition", - "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "group": "Navigation mesh configuration", "name": "AreaTopBound", "sentence": "the area top bound", "events": [ @@ -28492,10 +28492,10 @@ "objectGroups": [] }, { - "description": "the area right bound. The right bound of the area where objects can go (default to the game resolution). While an object is needed, this will apply to all objects using the behavior.", + "description": "the area right bound. The right bound of the area where objects can go in the scene (default to the game resolution). While an object is needed, this will apply to all objects using the behavior.", "fullName": "Area right bound", "functionType": "ExpressionAndCondition", - "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "group": "Navigation mesh configuration", "name": "AreaRightBound", "sentence": "the area right bound", "events": [ @@ -28583,10 +28583,10 @@ "objectGroups": [] }, { - "description": "the area bottom bound. The bottom bound of the area where objects can go (default to the game resolution). While an object is needed, this will apply to all objects using the behavior.", + "description": "the area bottom bound. The bottom bound of the area where objects can go in the scene (default to the game resolution). While an object is needed, this will apply to all objects using the behavior.", "fullName": "Area bottom bound", "functionType": "ExpressionAndCondition", - "group": "Obstacle for pathfinding (navigation mesh, experimental) configuration", + "group": "Navigation mesh configuration", "name": "AreaBottomBound", "sentence": "the area bottom bound", "events": [ @@ -28735,7 +28735,7 @@ "value": "", "type": "Number", "label": "Area left bound", - "description": "The left bound of the area where objects can go.", + "description": "The left bound of the area where objects can go in the scene.", "group": "", "extraInformation": [], "hidden": false, @@ -28745,7 +28745,7 @@ "value": "", "type": "Number", "label": "Area top bound", - "description": "The top bound of the area where objects can go.", + "description": "The top bound of the area where objects can go in the scene.", "group": "", "extraInformation": [], "hidden": false, @@ -28755,7 +28755,7 @@ "value": "", "type": "Number", "label": "Area right bound", - "description": "The right bound of the area where objects can go (default to the game resolution).", + "description": "The right bound of the area where objects can go in the scene (default to the game resolution).", "group": "", "extraInformation": [], "hidden": false, @@ -28765,7 +28765,7 @@ "value": "", "type": "Number", "label": "Area bottom bound", - "description": "The bottom bound of the area where objects can go (default to the game resolution).", + "description": "The bottom bound of the area where objects can go in the scene (default to the game resolution).", "group": "", "extraInformation": [], "hidden": false, From 5156037f103d1519971177445f1ca491e0fbfd56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 2 Dec 2022 14:49:57 +0100 Subject: [PATCH 04/11] Use multi-line descriptions and JS events. --- examples/isometric-game/isometric-game.json | 5644 ++++++++++++++++++- 1 file changed, 5602 insertions(+), 42 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index bbf3fdcb0..d279a4940 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -26594,7 +26594,12 @@ "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/Line Hero Pack/Master/SVG/Maps and Navigation/Maps and Navigation_map_find_search.svg", "shortDescription": "Pathfinding allows to compute an efficient path for objects, avoiding obstacles on the way.", "version": "0.1.0", - "description": "Compare to the built-in pathfinding behavior, this one aims to:\n- better respect obstacle shapes\n- find pathes faster if obstacles don't move\n", + "description": [ + "In comparison to the built-in pathfinding behavior, this one aims to:", + "- better respect obstacle shapes", + "- find pathes faster if obstacles don't move", + "" + ], "tags": [ "navmesh", "pathfinding" @@ -26639,7 +26644,4419 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "\n// This code has been built from https://github.com/D8H/NavMesh-GDevelop-Extension\n// If you need to make any modification, please open a PR on github.\n\nvar extendStatics = function(d, b) {\n extendStatics = Object.setPrototypeOf ||\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\n return extendStatics(d, b);\n};\n\nfunction __extends(d, b) {\n if (typeof b !== \"function\" && b !== null)\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\n extendStatics(d, b);\n function __() { this.constructor = d; }\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\n}\n\n/**\n * Stripped down version of Phaser's Vector2 with just the functionality needed for navmeshes.\n *\n * @export\n * @class Vector2\n */\nvar Vector2 = /** @class */ (function () {\n function Vector2(x, y) {\n if (x === void 0) { x = 0; }\n if (y === void 0) { y = 0; }\n this.x = x;\n this.y = y;\n }\n Vector2.prototype.equals = function (v) {\n return this.x === v.x && this.y === v.y;\n };\n Vector2.prototype.angle = function (v) {\n return Math.atan2(v.y - this.y, v.x - this.x);\n };\n Vector2.prototype.distance = function (v) {\n var dx = v.x - this.x;\n var dy = v.y - this.y;\n return Math.sqrt(dx * dx + dy * dy);\n };\n Vector2.prototype.add = function (v) {\n this.x += v.x;\n this.y += v.y;\n };\n Vector2.prototype.subtract = function (v) {\n this.x -= v.x;\n this.y -= v.y;\n };\n Vector2.prototype.clone = function () {\n return new Vector2(this.x, this.y);\n };\n return Vector2;\n}());\n\nvar GridNode = /** @class */ (function () {\n function GridNode(weight) {\n this.h = 0;\n this.g = 0;\n this.f = 0;\n this.closed = false;\n this.visited = false;\n this.parent = null;\n this.weight = weight;\n }\n GridNode.prototype.isWall = function () {\n return this.weight === 0;\n };\n GridNode.prototype.clean = function () {\n this.f = 0;\n this.g = 0;\n this.h = 0;\n this.visited = false;\n this.closed = false;\n this.parent = null;\n };\n return GridNode;\n}());\n\n/**\n * A class that represents a navigable polygon with a navmesh. It is built on top of a\n * {@link Polygon}. It implements the properties and fields that javascript-astar needs - weight,\n * toString, isWall and getCost. See GPS test from astar repo for structure:\n * https://github.com/bgrins/javascript-astar/blob/master/test/tests.js\n */\nvar NavPoly = /** @class */ (function (_super) {\n __extends(NavPoly, _super);\n /**\n * Creates an instance of NavPoly.\n */\n function NavPoly(id, polygon) {\n var _this = _super.call(this, 1) || this;\n _this.id = id;\n _this.polygon = polygon;\n _this.edges = polygon.edges;\n _this.neighbors = [];\n _this.portals = [];\n _this.centroid = _this.calculateCentroid();\n _this.boundingRadius = _this.calculateRadius();\n return _this;\n }\n /**\n * Returns an array of points that form the polygon.\n */\n NavPoly.prototype.getPoints = function () {\n return this.polygon.points;\n };\n /**\n * Check if the given point-like object is within the polygon.\n */\n NavPoly.prototype.contains = function (point) {\n // Phaser's polygon check doesn't handle when a point is on one of the edges of the line. Note:\n // check numerical stability here. It would also be good to optimize this for different shapes.\n return this.polygon.contains(point.x, point.y) || this.isPointOnEdge(point);\n };\n /**\n * Only rectangles are supported, so this calculation works, but this is not actually the centroid\n * calculation for a polygon. This is just the average of the vertices - proper centroid of a\n * polygon factors in the area.\n */\n NavPoly.prototype.calculateCentroid = function () {\n var centroid = new Vector2(0, 0);\n var length = this.polygon.points.length;\n this.polygon.points.forEach(function (p) { return centroid.add(p); });\n centroid.x /= length;\n centroid.y /= length;\n return centroid;\n };\n /**\n * Calculate the radius of a circle that circumscribes the polygon.\n */\n NavPoly.prototype.calculateRadius = function () {\n var boundingRadius = 0;\n for (var _i = 0, _a = this.polygon.points; _i < _a.length; _i++) {\n var point = _a[_i];\n var d = this.centroid.distance(point);\n if (d > boundingRadius)\n boundingRadius = d;\n }\n return boundingRadius;\n };\n /**\n * Check if the given point-like object is on one of the edges of the polygon.\n */\n NavPoly.prototype.isPointOnEdge = function (_a) {\n var x = _a.x, y = _a.y;\n for (var _i = 0, _b = this.edges; _i < _b.length; _i++) {\n var edge = _b[_i];\n if (edge.pointOnSegment(x, y))\n return true;\n }\n return false;\n };\n NavPoly.prototype.destroy = function () {\n this.neighbors = [];\n this.portals = [];\n };\n // === jsastar methods ===\n NavPoly.prototype.toString = function () {\n return \"NavPoly(id: \" + this.id + \" at: \" + this.centroid + \")\";\n };\n NavPoly.prototype.isWall = function () {\n return false;\n };\n NavPoly.prototype.centroidDistance = function (navPolygon) {\n return this.centroid.distance(navPolygon.centroid);\n };\n NavPoly.prototype.getCost = function (navPolygon) {\n //TODO the cost method should not be in the Node\n return this.centroidDistance(navPolygon);\n };\n return NavPoly;\n}(GridNode));\n\n/**\n * A graph memory structure\n */\nvar Graph = /** @class */ (function () {\n /**\n * A graph memory structure\n * @param {Array} gridIn 2D array of input weights\n * @param {Object} [options]\n * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed\n */\n function Graph(nodes, options) {\n this.dirtyNodes = [];\n options = options || {};\n this.nodes = nodes;\n this.diagonal = !!options.diagonal;\n this.init();\n }\n Graph.prototype.init = function () {\n this.dirtyNodes = [];\n for (var i = 0; i < this.nodes.length; i++) {\n this.nodes[i].clean();\n }\n };\n Graph.prototype.cleanDirty = function () {\n for (var i = 0; i < this.dirtyNodes.length; i++) {\n this.dirtyNodes[i].clean();\n }\n this.dirtyNodes = [];\n };\n Graph.prototype.markDirty = function (node) {\n this.dirtyNodes.push(node);\n };\n return Graph;\n}());\n\n/**\n * Graph for javascript-astar. It implements the functionality for astar. See GPS test from astar\n * repo for structure: https://github.com/bgrins/javascript-astar/blob/master/test/tests.js\n *\n * @class NavGraph\n * @private\n */\nvar NavGraph = /** @class */ (function (_super) {\n __extends(NavGraph, _super);\n function NavGraph(navPolygons) {\n var _this = _super.call(this, navPolygons) || this;\n _this.nodes = navPolygons;\n _this.init();\n return _this;\n }\n NavGraph.prototype.neighbors = function (navPolygon) {\n return navPolygon.neighbors;\n };\n NavGraph.prototype.navHeuristic = function (navPolygon1, navPolygon2) {\n return navPolygon1.centroidDistance(navPolygon2);\n };\n NavGraph.prototype.destroy = function () {\n this.cleanDirty();\n this.nodes = [];\n };\n return NavGraph;\n}(Graph));\n\n/**\n * Calculate the distance squared between two points. This is an optimization to a square root when\n * you just need to compare relative distances without needing to know the specific distance.\n * @param a\n * @param b\n */\nfunction distanceSquared(a, b) {\n var dx = b.x - a.x;\n var dy = b.y - a.y;\n return dx * dx + dy * dy;\n}\n/**\n * Project a point onto a line segment.\n * JS Source: http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment\n * @param point\n * @param line\n */\nfunction projectPointToEdge(point, line) {\n var a = line.start;\n var b = line.end;\n // Consider the parametric equation for the edge's line, p = a + t (b - a). We want to find\n // where our point lies on the line by solving for t:\n // t = [(p-a) . (b-a)] / |b-a|^2\n var l2 = distanceSquared(a, b);\n var t = ((point.x - a.x) * (b.x - a.x) + (point.y - a.y) * (b.y - a.y)) / l2;\n // We clamp t from [0,1] to handle points outside the segment vw.\n t = clamp(t, 0, 1);\n // Project onto the segment\n var p = new Vector2(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y));\n return p;\n}\n/**\n * Twice the area of the triangle formed by a, b and c.\n */\nfunction triarea2(a, b, c) {\n var ax = b.x - a.x;\n var ay = b.y - a.y;\n var bx = c.x - a.x;\n var by = c.y - a.y;\n return bx * ay - ax * by;\n}\n/**\n * Clamp the given value between min and max.\n */\nfunction clamp(value, min, max) {\n if (value < min)\n value = min;\n if (value > max)\n value = max;\n return value;\n}\n/**\n * Check if two values are within a small margin of one another.\n */\nfunction almostEqual(value1, value2, errorMargin) {\n if (errorMargin === void 0) { errorMargin = 0.0001; }\n if (Math.abs(value1 - value2) <= errorMargin)\n return true;\n else\n return false;\n}\n/**\n * Find the smallest angle difference between two angles\n * https://gist.github.com/Aaronduino/4068b058f8dbc34b4d3a9eedc8b2cbe0\n */\nfunction angleDifference(x, y) {\n var a = x - y;\n var i = a + Math.PI;\n var j = Math.PI * 2;\n a = i - Math.floor(i / j) * j; // (a+180) % 360; this ensures the correct sign\n a -= Math.PI;\n return a;\n}\n/**\n * Check if two lines are collinear (within a small error margin).\n */\nfunction areCollinear(line1, line2, errorMargin) {\n if (errorMargin === void 0) { errorMargin = 0.0001; }\n // Figure out if the two lines are equal by looking at the area of the triangle formed\n // by their points\n var area1 = triarea2(line1.start, line1.end, line2.start);\n var area2 = triarea2(line1.start, line1.end, line2.end);\n if (almostEqual(area1, 0, errorMargin) && almostEqual(area2, 0, errorMargin)) {\n return true;\n }\n else\n return false;\n}\n\n// Mostly sourced from PatrolJS at the moment. TODO: come back and reimplement this as an incomplete\n/**\n * @private\n */\nvar Channel = /** @class */ (function () {\n function Channel() {\n this.portals = [];\n this.path = [];\n }\n Channel.prototype.push = function (p1, p2) {\n if (p2 === undefined)\n p2 = p1;\n this.portals.push({\n left: p1,\n right: p2,\n });\n };\n Channel.prototype.stringPull = function () {\n var portals = this.portals;\n var pts = [];\n // Init scan state\n var apexIndex = 0;\n var leftIndex = 0;\n var rightIndex = 0;\n var portalApex = portals[0].left;\n var portalLeft = portals[0].left;\n var portalRight = portals[0].right;\n // Add start point.\n pts.push(portalApex);\n for (var i = 1; i < portals.length; i++) {\n // Find the next portal vertices\n var left = portals[i].left;\n var right = portals[i].right;\n // Update right vertex.\n if (triarea2(portalApex, portalRight, right) <= 0.0) {\n if (portalApex.equals(portalRight) || triarea2(portalApex, portalLeft, right) > 0.0) {\n // Tighten the funnel.\n portalRight = right;\n rightIndex = i;\n }\n else {\n // Right vertex just crossed over the left vertex, so the left vertex should\n // now be part of the path.\n pts.push(portalLeft);\n // Restart scan from portal left point.\n // Make current left the new apex.\n portalApex = portalLeft;\n apexIndex = leftIndex;\n // Reset portal\n portalLeft = portalApex;\n portalRight = portalApex;\n leftIndex = apexIndex;\n rightIndex = apexIndex;\n // Restart scan\n i = apexIndex;\n continue;\n }\n }\n // Update left vertex.\n if (triarea2(portalApex, portalLeft, left) >= 0.0) {\n if (portalApex.equals(portalLeft) || triarea2(portalApex, portalRight, left) < 0.0) {\n // Tighten the funnel.\n portalLeft = left;\n leftIndex = i;\n }\n else {\n // Left vertex just crossed over the right vertex, so the right vertex should\n // now be part of the path\n pts.push(portalRight);\n // Restart scan from portal right point.\n // Make current right the new apex.\n portalApex = portalRight;\n apexIndex = rightIndex;\n // Reset portal\n portalLeft = portalApex;\n portalRight = portalApex;\n leftIndex = apexIndex;\n rightIndex = apexIndex;\n // Restart scan\n i = apexIndex;\n continue;\n }\n }\n }\n if (pts.length === 0 || !pts[pts.length - 1].equals(portals[portals.length - 1].left)) {\n // Append last point to path.\n pts.push(portals[portals.length - 1].left);\n }\n this.path = pts;\n return pts;\n };\n return Channel;\n}());\n\n/**\n * Stripped down version of Phaser's Line with just the functionality needed for navmeshes.\n *\n * @export\n * @class Line\n */\nvar Line = /** @class */ (function () {\n function Line(x1, y1, x2, y2) {\n this.start = new Vector2(x1, y1);\n this.end = new Vector2(x2, y2);\n this.left = Math.min(x1, x2);\n this.right = Math.max(x1, x2);\n this.top = Math.min(y1, y2);\n this.bottom = Math.max(y1, y2);\n }\n Line.prototype.pointOnSegment = function (x, y) {\n return (x >= this.left &&\n x <= this.right &&\n y >= this.top &&\n y <= this.bottom &&\n this.pointOnLine(x, y));\n };\n Line.prototype.pointOnLine = function (x, y) {\n // Compare slope of line start -> xy to line start -> line end\n return (x - this.left) * (this.bottom - this.top) === (this.right - this.left) * (y - this.top);\n };\n return Line;\n}());\n\n/**\n * Stripped down version of Phaser's Polygon with just the functionality needed for navmeshes.\n *\n * @export\n * @class Polygon\n */\nvar Polygon = /** @class */ (function () {\n function Polygon(points, closed) {\n if (closed === void 0) { closed = true; }\n this.isClosed = closed;\n this.points = points;\n this.edges = [];\n for (var i = 1; i < points.length; i++) {\n var p1 = points[i - 1];\n var p2 = points[i];\n this.edges.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n if (this.isClosed) {\n var first = points[0];\n var last = points[points.length - 1];\n this.edges.push(new Line(first.x, first.y, last.x, last.y));\n }\n }\n Polygon.prototype.contains = function (x, y) {\n var inside = false;\n for (var i = -1, j = this.points.length - 1; ++i < this.points.length; j = i) {\n var ix = this.points[i].x;\n var iy = this.points[i].y;\n var jx = this.points[j].x;\n var jy = this.points[j].y;\n if (((iy <= y && y < jy) || (jy <= y && y < iy)) &&\n x < ((jx - ix) * (y - iy)) / (jy - iy) + ix) {\n inside = !inside;\n }\n }\n return inside;\n };\n return Polygon;\n}());\n\nvar BinaryHeap = /** @class */ (function () {\n function BinaryHeap(scoreFunction) {\n this.content = new Array();\n this.scoreFunction = scoreFunction;\n }\n BinaryHeap.prototype.push = function (element) {\n // Add the new element to the end of the array.\n this.content.push(element);\n // Allow it to sink down.\n this.sinkDown(this.content.length - 1);\n };\n BinaryHeap.prototype.pop = function () {\n // Store the first element so we can return it later.\n var result = this.content[0];\n // Get the element at the end of the array.\n var end = this.content.pop();\n if (!end)\n return;\n // If there are any elements left, put the end element at the\n // start, and let it bubble up.\n if (this.content.length > 0) {\n this.content[0] = end;\n this.bubbleUp(0);\n }\n return result;\n };\n BinaryHeap.prototype.remove = function (node) {\n var i = this.content.indexOf(node);\n // When it is found, the process seen in 'pop' is repeated\n // to fill up the hole.\n var end = this.content.pop();\n if (!end)\n return;\n if (i !== this.content.length - 1) {\n this.content[i] = end;\n if (this.scoreFunction(end) < this.scoreFunction(node)) {\n this.sinkDown(i);\n }\n else {\n this.bubbleUp(i);\n }\n }\n };\n BinaryHeap.prototype.size = function () {\n return this.content.length;\n };\n BinaryHeap.prototype.rescoreElement = function (node) {\n this.sinkDown(this.content.indexOf(node));\n };\n BinaryHeap.prototype.sinkDown = function (n) {\n // Fetch the element that has to be sunk.\n var element = this.content[n];\n // When at 0, an element can not sink any further.\n while (n > 0) {\n // Compute the parent element's index, and fetch it.\n var parentN = ((n + 1) >> 1) - 1;\n var parent = this.content[parentN];\n // Swap the elements if the parent is greater.\n if (this.scoreFunction(element) < this.scoreFunction(parent)) {\n this.content[parentN] = element;\n this.content[n] = parent;\n // Update 'n' to continue at the new position.\n n = parentN;\n }\n // Found a parent that is less, no need to sink any further.\n else {\n break;\n }\n }\n };\n BinaryHeap.prototype.bubbleUp = function (n) {\n // Look up the target element and its score.\n var length = this.content.length;\n var element = this.content[n];\n var elemScore = this.scoreFunction(element);\n while (true) {\n // Compute the indices of the child elements.\n var child2N = (n + 1) << 1;\n var child1N = child2N - 1;\n // This is used to store the new position of the element, if any.\n var swap = null;\n var child1Score = 0;\n // If the first child exists (is inside the array)...\n if (child1N < length) {\n // Look it up and compute its score.\n var child1 = this.content[child1N];\n child1Score = this.scoreFunction(child1);\n // If the score is less than our element's, we need to swap.\n if (child1Score < elemScore) {\n swap = child1N;\n }\n }\n // Do the same checks for the other child.\n if (child2N < length) {\n var child2 = this.content[child2N];\n var child2Score = this.scoreFunction(child2);\n if (child2Score < (swap === null ? elemScore : child1Score)) {\n swap = child2N;\n }\n }\n // If the element needs to be moved, swap it, and continue.\n if (swap !== null) {\n this.content[n] = this.content[swap];\n this.content[swap] = element;\n n = swap;\n }\n // Otherwise, we are done.\n else {\n break;\n }\n }\n };\n return BinaryHeap;\n}());\n\n// The following implementation of the A* algorithm is from:\nvar AStar = /** @class */ (function () {\n function AStar() {\n }\n /**\n * Perform an A* Search on a graph given a start and end node.\n * @param {Graph} graph\n * @param {GridNode} start\n * @param {GridNode} end\n * @param {Object} [options]\n * @param {bool} [options.closest] Specifies whether to return the\n path to the closest node if the target is unreachable.\n * @param {Function} [options.heuristic] Heuristic function (see\n * astar.heuristics).\n */\n AStar.prototype.search = function (graph, start, end, \n // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html\n heuristic, closest) {\n if (closest === void 0) { closest = false; }\n graph.cleanDirty();\n var openHeap = this.getHeap();\n var closestNode = start; // set the start node to be the closest if required\n start.h = heuristic(start, end);\n graph.markDirty(start);\n openHeap.push(start);\n while (openHeap.size() > 0) {\n // Grab the lowest f(x) to process next. Heap keeps this sorted for us.\n var currentNode = openHeap.pop();\n // never happen\n if (!currentNode)\n return [];\n // End case -- result has been found, return the traced path.\n if (currentNode === end) {\n return this.pathTo(currentNode);\n }\n // Normal case -- move currentNode from open to closed, process each of its neighbors.\n currentNode.closed = true;\n // Find all neighbors for the current node.\n var neighbors = graph.neighbors(currentNode);\n for (var i = 0, il = neighbors.length; i < il; ++i) {\n var neighbor = neighbors[i];\n if (neighbor.closed || neighbor.isWall()) {\n // Not a valid node to process, skip to next neighbor.\n continue;\n }\n // The g score is the shortest distance from start to current node.\n // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.\n var gScore = currentNode.g + neighbor.getCost(currentNode);\n var beenVisited = neighbor.visited;\n if (!beenVisited || gScore < neighbor.g) {\n // Found an optimal (so far) path to this node. Take score for node to see how good it is.\n neighbor.visited = true;\n neighbor.parent = currentNode;\n neighbor.h = neighbor.h || heuristic(neighbor, end);\n neighbor.g = gScore;\n neighbor.f = neighbor.g + neighbor.h;\n graph.markDirty(neighbor);\n if (closest) {\n // If the neighbor is closer than the current closestNode or if it's equally close but has\n // a cheaper path than the current closest node then it becomes the closest node\n if (neighbor.h < closestNode.h ||\n (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) {\n closestNode = neighbor;\n }\n }\n if (!beenVisited) {\n // Pushing to heap will put it in proper place based on the 'f' value.\n openHeap.push(neighbor);\n }\n else {\n // Already seen the node, but since it has been rescored we need to reorder it in the heap\n openHeap.rescoreElement(neighbor);\n }\n }\n }\n }\n if (closest) {\n return this.pathTo(closestNode);\n }\n // No result was found - empty array signifies failure to find path.\n return [];\n };\n AStar.prototype.pathTo = function (node) {\n var curr = node;\n var path = new Array();\n while (curr.parent) {\n path.unshift(curr);\n curr = curr.parent;\n }\n return path;\n };\n AStar.prototype.getHeap = function () {\n return new BinaryHeap(function (node) {\n return node.f;\n });\n };\n return AStar;\n}());\n\n/**\n * The `NavMesh` class is the workhorse that represents a navigation mesh built from a series of\n * polygons. Once built, the mesh can be asked for a path from one point to another point. Some\n * internal terminology usage:\n * - neighbor: a polygon that shares part of an edge with another polygon\n * - portal: when two neighbor's have edges that overlap, the portal is the overlapping line segment\n * - channel: the path of polygons from starting point to end point\n * - pull the string: run the funnel algorithm on the channel so that the path hugs the edges of the\n * channel. Equivalent to having a string snaking through a hallway and then pulling it taut.\n */\nvar NavMesh = /** @class */ (function () {\n /**\n * @param meshPolygonPoints Array where each element is an array of point-like objects that\n * defines a polygon.\n * @param meshShrinkAmount The amount (in pixels) that the navmesh has been shrunk around\n * obstacles (a.k.a the amount obstacles have been expanded).\n */\n function NavMesh(meshPolygonPoints, meshShrinkAmount) {\n if (meshShrinkAmount === void 0) { meshShrinkAmount = 0; }\n this.meshShrinkAmount = meshShrinkAmount;\n // Convert the PolyPoints[] into NavPoly instances.\n this.navPolygons = meshPolygonPoints.map(function (polyPoints, i) { return new NavPoly(i, new Polygon(polyPoints)); });\n this.calculateNeighbors();\n // Astar graph of connections between polygons\n this.graph = new NavGraph(this.navPolygons);\n }\n /**\n * Get the NavPolys that are in this navmesh.\n */\n NavMesh.prototype.getPolygons = function () {\n return this.navPolygons;\n };\n /**\n * Cleanup method to remove references.\n */\n NavMesh.prototype.destroy = function () {\n this.graph.destroy();\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var poly = _a[_i];\n poly.destroy();\n }\n this.navPolygons = [];\n };\n /**\n * Find if the given point is within any of the polygons in the mesh.\n * @param point\n */\n NavMesh.prototype.isPointInMesh = function (point) {\n return this.navPolygons.some(function (navPoly) { return navPoly.contains(point); });\n };\n /**\n * Find the closest point in the mesh to the given point. If the point is already in the mesh,\n * this will give you that point. If the point is outside of the mesh, this will attempt to\n * project this point into the mesh (up to the given maxAllowableDist). This returns an object\n * with:\n * - distance - from the given point to the mesh\n * - polygon - the one the point is closest to, or null\n * - point - the point inside the mesh, or null\n * @param point\n * @param maxAllowableDist\n */\n NavMesh.prototype.findClosestMeshPoint = function (point, maxAllowableDist) {\n if (maxAllowableDist === void 0) { maxAllowableDist = Number.POSITIVE_INFINITY; }\n var minDistance = maxAllowableDist;\n var closestPoly = null;\n var pointOnClosestPoly = null;\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var navPoly = _a[_i];\n // If we are inside a poly, we've got the closest.\n if (navPoly.contains(point)) {\n minDistance = 0;\n closestPoly = navPoly;\n pointOnClosestPoly = point;\n break;\n }\n // Is the poly close enough to warrant a more accurate check? Point is definitely outside of\n // the polygon. Distance - Radius is the smallest possible distance to an edge of the poly.\n // This will underestimate distance, but that's perfectly fine.\n var r = navPoly.boundingRadius;\n var d = navPoly.centroid.distance(point);\n if (d - r < minDistance) {\n var result = this.projectPointToPolygon(point, navPoly);\n if (result.distance < minDistance) {\n minDistance = result.distance;\n closestPoly = navPoly;\n pointOnClosestPoly = result.point;\n }\n }\n }\n return { distance: minDistance, polygon: closestPoly, point: pointOnClosestPoly };\n };\n /**\n * Find a path from the start point to the end point using this nav mesh.\n * @param startPoint A point-like object in the form {x, y}\n * @param endPoint A point-like object in the form {x, y}\n * @returns An array of points if a path is found, or null if no path\n */\n NavMesh.prototype.findPath = function (startPoint, endPoint) {\n var startPoly = null;\n var endPoly = null;\n var startDistance = Number.MAX_VALUE;\n var endDistance = Number.MAX_VALUE;\n var d, r;\n var startVector = new Vector2(startPoint.x, startPoint.y);\n var endVector = new Vector2(endPoint.x, endPoint.y);\n // Find the closest poly for the starting and ending point\n for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {\n var navPoly = _a[_i];\n r = navPoly.boundingRadius;\n // Start\n d = navPoly.centroid.distance(startVector);\n if (d <= startDistance && d <= r && navPoly.contains(startVector)) {\n startPoly = navPoly;\n startDistance = d;\n }\n // End\n d = navPoly.centroid.distance(endVector);\n if (d <= endDistance && d <= r && navPoly.contains(endVector)) {\n endPoly = navPoly;\n endDistance = d;\n }\n }\n // If the end point wasn't inside a polygon, run a more liberal check that allows a point\n // to be within meshShrinkAmount radius of a polygon\n if (!endPoly && this.meshShrinkAmount > 0) {\n for (var _b = 0, _c = this.navPolygons; _b < _c.length; _b++) {\n var navPoly = _c[_b];\n r = navPoly.boundingRadius + this.meshShrinkAmount;\n d = navPoly.centroid.distance(endVector);\n if (d <= r) {\n var distance = this.projectPointToPolygon(endVector, navPoly).distance;\n if (distance <= this.meshShrinkAmount && distance < endDistance) {\n endPoly = navPoly;\n endDistance = distance;\n }\n }\n }\n }\n // No matching polygons locations for the end, so no path found\n // because start point is valid normally, check end point first\n if (!endPoly)\n return null;\n // Same check as above, but for the start point\n if (!startPoly && this.meshShrinkAmount > 0) {\n for (var _d = 0, _e = this.navPolygons; _d < _e.length; _d++) {\n var navPoly = _e[_d];\n // Check if point is within bounding circle to avoid extra projection calculations\n r = navPoly.boundingRadius + this.meshShrinkAmount;\n d = navPoly.centroid.distance(startVector);\n if (d <= r) {\n // Check if projected point is within range of a polygon and is closer than the\n // previous point\n var distance = this.projectPointToPolygon(startVector, navPoly).distance;\n if (distance <= this.meshShrinkAmount && distance < startDistance) {\n startPoly = navPoly;\n startDistance = distance;\n }\n }\n }\n }\n // No matching polygons locations for the start, so no path found\n if (!startPoly)\n return null;\n // If the start and end polygons are the same, return a direct path\n if (startPoly === endPoly)\n return [startVector, endVector];\n // Search!\n var astarPath = new AStar().search(this.graph, startPoly, endPoly, this.graph.navHeuristic);\n // While the start and end polygons may be valid, no path between them\n if (astarPath.length === 0)\n return null;\n // jsastar drops the first point from the path, but the funnel algorithm needs it\n astarPath.unshift(startPoly);\n // We have a path, so now time for the funnel algorithm\n var channel = new Channel();\n channel.push(startVector);\n for (var i = 0; i < astarPath.length - 1; i++) {\n var navPolygon = astarPath[i];\n var nextNavPolygon = astarPath[i + 1];\n // Find the portal\n var portal = null;\n for (var i_1 = 0; i_1 < navPolygon.neighbors.length; i_1++) {\n if (navPolygon.neighbors[i_1].id === nextNavPolygon.id) {\n portal = navPolygon.portals[i_1];\n }\n }\n if (!portal)\n throw new Error(\"Path was supposed to be found, but portal is missing!\");\n // Push the portal vertices into the channel\n channel.push(portal.start, portal.end);\n }\n channel.push(endVector);\n // Pull a string along the channel to run the funnel\n channel.stringPull();\n // Clone path, excluding duplicates\n var lastPoint = null;\n var phaserPath = new Array();\n for (var _f = 0, _g = channel.path; _f < _g.length; _f++) {\n var p = _g[_f];\n var newPoint = p.clone();\n if (!lastPoint || !newPoint.equals(lastPoint))\n phaserPath.push(newPoint);\n lastPoint = newPoint;\n }\n return phaserPath;\n };\n NavMesh.prototype.calculateNeighbors = function () {\n // Fill out the neighbor information for each navpoly\n for (var i = 0; i < this.navPolygons.length; i++) {\n var navPoly = this.navPolygons[i];\n for (var j = i + 1; j < this.navPolygons.length; j++) {\n var otherNavPoly = this.navPolygons[j];\n // Check if the other navpoly is within range to touch\n var d = navPoly.centroid.distance(otherNavPoly.centroid);\n if (d > navPoly.boundingRadius + otherNavPoly.boundingRadius)\n continue;\n // The are in range, so check each edge pairing\n for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {\n var edge = _a[_i];\n for (var _b = 0, _c = otherNavPoly.edges; _b < _c.length; _b++) {\n var otherEdge = _c[_b];\n // If edges aren't collinear, not an option for connecting navpolys\n if (!areCollinear(edge, otherEdge))\n continue;\n // If they are collinear, check if they overlap\n var overlap = this.getSegmentOverlap(edge, otherEdge);\n if (!overlap)\n continue;\n // Connections are symmetric!\n navPoly.neighbors.push(otherNavPoly);\n otherNavPoly.neighbors.push(navPoly);\n // Calculate the portal between the two polygons - this needs to be in\n // counter-clockwise order, relative to each polygon\n var p1 = overlap[0], p2 = overlap[1];\n var edgeStartAngle = navPoly.centroid.angle(edge.start);\n var a1 = navPoly.centroid.angle(overlap[0]);\n var a2 = navPoly.centroid.angle(overlap[1]);\n var d1 = angleDifference(edgeStartAngle, a1);\n var d2 = angleDifference(edgeStartAngle, a2);\n if (d1 < d2) {\n navPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n else {\n navPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));\n }\n edgeStartAngle = otherNavPoly.centroid.angle(otherEdge.start);\n a1 = otherNavPoly.centroid.angle(overlap[0]);\n a2 = otherNavPoly.centroid.angle(overlap[1]);\n d1 = angleDifference(edgeStartAngle, a1);\n d2 = angleDifference(edgeStartAngle, a2);\n if (d1 < d2) {\n otherNavPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));\n }\n else {\n otherNavPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));\n }\n // Two convex polygons shouldn't be connected more than once! (Unless\n // there are unnecessary vertices...)\n }\n }\n }\n }\n };\n // Check two collinear line segments to see if they overlap by sorting the points.\n // Algorithm source: http://stackoverflow.com/a/17152247\n NavMesh.prototype.getSegmentOverlap = function (line1, line2) {\n var points = [\n { line: line1, point: line1.start },\n { line: line1, point: line1.end },\n { line: line2, point: line2.start },\n { line: line2, point: line2.end },\n ];\n points.sort(function (a, b) {\n if (a.point.x < b.point.x)\n return -1;\n else if (a.point.x > b.point.x)\n return 1;\n else {\n if (a.point.y < b.point.y)\n return -1;\n else if (a.point.y > b.point.y)\n return 1;\n else\n return 0;\n }\n });\n // If the first two points in the array come from the same line, no overlap\n var noOverlap = points[0].line === points[1].line;\n // If the two middle points in the array are the same coordinates, then there is a\n // single point of overlap.\n var singlePointOverlap = points[1].point.equals(points[2].point);\n if (noOverlap || singlePointOverlap)\n return null;\n else\n return [points[1].point, points[2].point];\n };\n /**\n * Project a point onto a polygon in the shortest distance possible.\n *\n * @param {Phaser.Point} point The point to project\n * @param {NavPoly} navPoly The navigation polygon to test against\n * @returns {{point: Phaser.Point, distance: number}}\n */\n NavMesh.prototype.projectPointToPolygon = function (point, navPoly) {\n var closestProjection = null;\n var closestDistance = Number.MAX_VALUE;\n for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {\n var edge = _a[_i];\n var projectedPoint = projectPointToEdge(point, edge);\n var d = point.distance(projectedPoint);\n if (closestProjection === null || d < closestDistance) {\n closestDistance = d;\n closestProjection = projectedPoint;\n }\n }\n return { point: closestProjection, distance: closestDistance };\n };\n return NavMesh;\n}());\n\n/**\n * This implementation is strongly inspired from CritterAI class \"Geometry\".\n */\nvar Geometry = /** @class */ (function () {\n function Geometry() {\n }\n /**\n * Returns TRUE if line segment AB intersects with line segment CD in any\n * manner. Either collinear or at a single point.\n * @param ax The x-value for point (ax, ay) in line segment AB.\n * @param ay The y-value for point (ax, ay) in line segment AB.\n * @param bx The x-value for point (bx, by) in line segment AB.\n * @param by The y-value for point (bx, by) in line segment AB.\n * @param cx The x-value for point (cx, cy) in line segment CD.\n * @param cy The y-value for point (cx, cy) in line segment CD.\n * @param dx The x-value for point (dx, dy) in line segment CD.\n * @param dy The y-value for point (dx, dy) in line segment CD.\n * @return TRUE if line segment AB intersects with line segment CD in any\n * manner.\n */\n Geometry.segmentsIntersect = function (ax, ay, bx, by, cx, cy, dx, dy) {\n // This is modified 2D line-line intersection/segment-segment\n // intersection test.\n var deltaABx = bx - ax;\n var deltaABy = by - ay;\n var deltaCAx = ax - cx;\n var deltaCAy = ay - cy;\n var deltaCDx = dx - cx;\n var deltaCDy = dy - cy;\n var numerator = deltaCAy * deltaCDx - deltaCAx * deltaCDy;\n var denominator = deltaABx * deltaCDy - deltaABy * deltaCDx;\n // Perform early exit tests.\n if (denominator === 0 && numerator !== 0) {\n // If numerator is zero, then the lines are colinear.\n // Since it isn't, then the lines must be parallel.\n return false;\n }\n // Lines intersect. But do the segments intersect?\n // Forcing float division on both of these via casting of the\n // denominator.\n var factorAB = numerator / denominator;\n var factorCD = (deltaCAy * deltaABx - deltaCAx * deltaABy) / denominator;\n // Determine the type of intersection\n if (factorAB >= 0.0 &&\n factorAB <= 1.0 &&\n factorCD >= 0.0 &&\n factorCD <= 1.0) {\n return true; // The two segments intersect.\n }\n // The lines intersect, but segments to not.\n return false;\n };\n /**\n * Returns the distance squared from the point to the line segment.\n *\n * Behavior is undefined if the the closest distance is outside the\n * line segment.\n *\n * @param px The x-value of point (px, py).\n * @param py The y-value of point (px, py)\n * @param ax The x-value of the line segment's vertex A.\n * @param ay The y-value of the line segment's vertex A.\n * @param bx The x-value of the line segment's vertex B.\n * @param by The y-value of the line segment's vertex B.\n * @return The distance squared from the point (px, py) to line segment AB.\n */\n Geometry.getPointSegmentDistanceSq = function (px, py, ax, ay, bx, by) {\n // Reference: http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/\n //\n // The goal of the algorithm is to find the point on line segment AB\n // that is closest to P and then calculate the distance between P\n // and that point.\n var deltaABx = bx - ax;\n var deltaABy = by - ay;\n var deltaAPx = px - ax;\n var deltaAPy = py - ay;\n var segmentABLengthSq = deltaABx * deltaABx + deltaABy * deltaABy;\n if (segmentABLengthSq === 0) {\n // AB is not a line segment. So just return\n // distanceSq from P to A\n return deltaAPx * deltaAPx + deltaAPy * deltaAPy;\n }\n var u = (deltaAPx * deltaABx + deltaAPy * deltaABy) / segmentABLengthSq;\n if (u < 0) {\n // Closest point on line AB is outside outside segment AB and\n // closer to A. So return distanceSq from P to A.\n return deltaAPx * deltaAPx + deltaAPy * deltaAPy;\n }\n else if (u > 1) {\n // Closest point on line AB is outside segment AB and closer to B.\n // So return distanceSq from P to B.\n return (px - bx) * (px - bx) + (py - by) * (py - by);\n }\n // Closest point on lineAB is inside segment AB. So find the exact\n // point on AB and calculate the distanceSq from it to P.\n // The calculation in parenthesis is the location of the point on\n // the line segment.\n var deltaX = ax + u * deltaABx - px;\n var deltaY = ay + u * deltaABy - py;\n return deltaX * deltaX + deltaY * deltaY;\n };\n return Geometry;\n}());\n\n/**\n * A cell that holds data needed by the 1st steps of the NavMesh generation.\n */\nvar RasterizationCell = /** @class */ (function () {\n function RasterizationCell(x, y) {\n /**\n * 0 means there is an obstacle in the cell.\n * See {@link RegionGenerator}\n */\n this.distanceToObstacle = Number.MAX_VALUE;\n this.regionID = RasterizationCell.NULL_REGION_ID;\n this.distanceToRegionCore = 0;\n /**\n * If a cell is connected to one or more external regions then the\n * flag will be a 4 bit value where connections are recorded as\n * follows:\n * - bit1 = neighbor0\n * - bit2 = neighbor1\n * - bit3 = neighbor2\n * - bit4 = neighbor3\n * With the meaning of the bits as follows:\n * - 0 = neighbor in same region.\n * - 1 = neighbor not in same region (neighbor may be the obstacle\n * region or a real region).\n *\n * See {@link ContourBuilder}\n */\n this.contourFlags = 0;\n this.x = x;\n this.y = y;\n this.clear();\n }\n RasterizationCell.prototype.clear = function () {\n this.distanceToObstacle = Number.MAX_VALUE;\n this.regionID = RasterizationCell.NULL_REGION_ID;\n this.distanceToRegionCore = 0;\n this.contourFlags = 0;\n };\n /** A cell that has not been assigned to any region yet */\n RasterizationCell.NULL_REGION_ID = 0;\n /**\n * A cell that contains an obstacle.\n *\n * The value is the same as NULL_REGION_ID because the cells that are\n * not assigned to any region at the end of the flooding algorithm are\n * the obstacle cells.\n */\n RasterizationCell.OBSTACLE_REGION_ID = 0;\n return RasterizationCell;\n}());\n\nvar RasterizationGrid = /** @class */ (function () {\n function RasterizationGrid(left, top, right, bottom, cellWidth, cellHeight) {\n this.regionCount = 0;\n this.cellWidth = cellWidth;\n this.cellHeight = cellHeight;\n this.originX = left - cellWidth;\n this.originY = top - cellHeight;\n var dimX = 2 + Math.ceil((right - left) / cellWidth);\n var dimY = 2 + Math.ceil((bottom - top) / cellHeight);\n this.cells = [];\n for (var y = 0; y < dimY; y++) {\n this.cells[y] = [];\n for (var x = 0; x < dimX; x++) {\n this.cells[y][x] = new RasterizationCell(x, y);\n }\n }\n }\n RasterizationGrid.prototype.clear = function () {\n for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {\n var row = _a[_i];\n for (var _b = 0, row_1 = row; _b < row_1.length; _b++) {\n var cell = row_1[_b];\n cell.clear();\n }\n }\n this.regionCount = 0;\n };\n /**\n *\n * @param position the position on the scene\n * @param gridPosition the position on the grid\n * @returns the position on the grid\n */\n RasterizationGrid.prototype.convertToGridBasis = function (position, gridPosition) {\n gridPosition.x = (position.x - this.originX) / this.cellWidth;\n gridPosition.y = (position.y - this.originY) / this.cellHeight;\n return gridPosition;\n };\n /**\n *\n * @param gridPosition the position on the grid\n * @param position the position on the scene\n * @returns the position on the scene\n */\n RasterizationGrid.prototype.convertFromGridBasis = function (gridPosition, position) {\n position.x = gridPosition.x * this.cellWidth + this.originX;\n position.y = gridPosition.y * this.cellHeight + this.originY;\n return position;\n };\n RasterizationGrid.prototype.get = function (x, y) {\n return this.cells[y][x];\n };\n RasterizationGrid.prototype.getNeighbor = function (cell, direction) {\n var delta = RasterizationGrid.neighbor8Deltas[direction];\n return this.cells[cell.y + delta.y][cell.x + delta.x];\n };\n RasterizationGrid.prototype.dimY = function () {\n return this.cells.length;\n };\n RasterizationGrid.prototype.dimX = function () {\n var firstColumn = this.cells[0];\n return firstColumn ? firstColumn.length : 0;\n };\n RasterizationGrid.prototype.obstacleDistanceMax = function () {\n var max = 0;\n for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {\n var cellRow = _a[_i];\n for (var _b = 0, cellRow_1 = cellRow; _b < cellRow_1.length; _b++) {\n var cell = cellRow_1[_b];\n if (cell.distanceToObstacle > max) {\n max = cell.distanceToObstacle;\n }\n }\n }\n return max;\n };\n RasterizationGrid.neighbor4Deltas = [\n { x: -1, y: 0 },\n { x: 0, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: -1 },\n ];\n RasterizationGrid.neighbor8Deltas = [\n { x: -1, y: 0 },\n { x: 0, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: -1 },\n { x: 1, y: 1 },\n { x: -1, y: 1 },\n { x: -1, y: -1 },\n { x: 1, y: -1 },\n ];\n return RasterizationGrid;\n}());\n\n/**\n * Builds a set of contours from the region information contained in\n * {@link RasterizationCell}. It does this by locating and \"walking\" the edges.\n *\n * This implementation is strongly inspired from CritterAI class \"ContourSetBuilder\".\n * http://www.critterai.org/projects/nmgen_study/contourgen.html\n */\nvar ContourBuilder = /** @class */ (function () {\n function ContourBuilder() {\n // These are working lists whose content changes with each iteration\n // of the up coming loop. They represent the detailed and simple\n // contour vertices.\n // Initial sizing is arbitrary.\n this.workingRawVertices = new Array(256);\n this.workingSimplifiedVertices = new Array(64);\n }\n /**\n * Generates a contour set from the provided {@link RasterizationGrid}\n *\n * The provided field is expected to contain region information.\n * Behavior is undefined if the provided field is malformed or incomplete.\n *\n * This operation overwrites the flag fields for all cells in the\n * provided field. So the flags must be saved and restored if they are\n * important.\n *\n * @param grid A fully generated field.\n * @param threshold The maximum distance (in cells) the edge of the contour\n * may deviate from the source geometry when the rastered obstacles are\n * vectorized.\n *\n * Setting it to:\n * - 1 ensure that an aliased edge won't be split to more edges.\n * - more that 1 will reduce the number of edges but the obstacles edges\n * will be followed with less accuracy.\n * - less that 1 might be more accurate but it may try to follow the\n * aliasing and be a lot less accurate.\n *\n * Values under 1 can be useful in specific cases:\n * - when edges are horizontal or vertical, there is no aliasing so value\n * near 0 can do better results.\n * - when edges are 45° multiples, aliased vertex won't be farther than\n * sqrt(2)/2 so values over 0.71 should give good results but not\n * necessarily better than 1.\n *\n * @return The contours generated from the field.\n */\n ContourBuilder.prototype.buildContours = function (grid, threshold) {\n var contours = new Array(grid.regionCount);\n contours.length = 0;\n var contoursByRegion = new Array(grid.regionCount);\n var discardedContours = 0;\n // Set the flags on all cells in non-obstacle regions to indicate which\n // edges are connected to external regions.\n //\n // Reference: Neighbor search and nomenclature.\n // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch\n //\n // If a cell has no connections to external regions or is\n // completely surrounded by other regions (a single cell island),\n // its flag will be zero.\n //\n // If a cell is connected to one or more external regions then the\n // flag will be a 4 bit value where connections are recorded as\n // follows:\n // bit1 = neighbor0\n // bit2 = neighbor1\n // bit3 = neighbor2\n // bit4 = neighbor3\n // With the meaning of the bits as follows:\n // 0 = neighbor in same region.\n // 1 = neighbor not in same region (neighbor may be the obstacle\n // region or a real region).\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n // Note: This algorithm first sets the flag bits such that\n // 1 = \"neighbor is in the same region\". At the end it inverts\n // the bits so flags are as expected.\n // Default to \"not connected to any external region\".\n cell.contourFlags = 0;\n if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // Don't care about cells in the obstacle region.\n continue;\n for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {\n var delta = RasterizationGrid.neighbor4Deltas[direction];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (cell.regionID === neighbor.regionID) {\n // Neighbor is in same region as this cell.\n // Set the bit for this neighbor to 1 (Will be inverted later).\n cell.contourFlags |= 1 << direction;\n }\n }\n // Invert the bits so a bit value of 1 indicates neighbor NOT in\n // same region.\n cell.contourFlags ^= 0xf;\n if (cell.contourFlags === 0xf) {\n // This is an island cell (All neighbors are from other regions)\n // Get rid of flags.\n cell.contourFlags = 0;\n console.warn(\"Discarded contour: Island cell. Can't form a contour. Region: \" +\n cell.regionID);\n discardedContours++;\n }\n }\n }\n // Loop through all cells looking for cells on the edge of a region.\n //\n // At this point, only cells with flags != 0 are edge cells that\n // are part of a region contour.\n //\n // The process of building a contour will clear the flags on all cells\n // that make up the contour to ensure they are only processed once.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID ||\n cell.contourFlags === 0) {\n // cell is either: Part of the obstacle region, does not\n // represent an edge cell, or was already processed during\n // an earlier iteration.\n continue;\n }\n this.workingRawVertices.length = 0;\n this.workingSimplifiedVertices.length = 0;\n // The cell is part of an unprocessed region's contour.\n // Locate a direction of the cell's edge which points toward\n // another region (there is at least one).\n var startDirection = 0;\n while ((cell.contourFlags & (1 << startDirection)) === 0) {\n startDirection++;\n }\n // We now have a cell that is part of a contour and a direction\n // that points to a different region (obstacle or real).\n // Build the contour.\n this.buildRawContours(grid, cell, startDirection, this.workingRawVertices);\n // Perform post processing on the contour in order to\n // create the final, simplified contour.\n this.generateSimplifiedContour(cell.regionID, this.workingRawVertices, this.workingSimplifiedVertices, threshold);\n // The CritterAI implementation filters polygons with less than\n // 3 vertices, but they are needed to filter vertices in the middle\n // (not on an obstacle region border).\n var contour = Array.from(this.workingSimplifiedVertices);\n contours.push(contour);\n contoursByRegion[cell.regionID] = contour;\n }\n }\n if (contours.length + discardedContours !== grid.regionCount - 1) {\n // The only valid state is one contour per region.\n //\n // The only time this should occur is if an invalid contour\n // was formed or if a region resulted in multiple\n // contours (bad region data).\n //\n // IMPORTANT: While a mismatch may not be a fatal error,\n // it should be addressed since it can result in odd,\n // hard to spot anomalies later in the pipeline.\n //\n // A known cause is if a region fully encompasses another\n // region. In such a case, two contours will be formed.\n // The normal outer contour and an inner contour.\n // The CleanNullRegionBorders algorithm protects\n // against internal encompassed obstacle regions.\n console.error(\"Contour generation failed: Detected contours does\" +\n \" not match the number of regions. Regions: \" +\n (grid.regionCount - 1) +\n \", Detected contours: \" +\n (contours.length + discardedContours) +\n \" (Actual: \" +\n contours.length +\n \", Discarded: \" +\n discardedContours +\n \")\");\n // The CritterAI implementation has more detailed logs.\n // They can be interesting for debugging.\n }\n this.filterNonObstacleVertices(contours, contoursByRegion);\n return contours;\n };\n /**\n * Search vertices that are not shared with the obstacle region and\n * remove them.\n *\n * Some contours will have no vertex left.\n *\n * @param contours\n * @param contoursByRegion Some regions may have been discarded\n * so contours index can't be used.\n */\n ContourBuilder.prototype.filterNonObstacleVertices = function (contours, contoursByRegion) {\n // This was not part of the CritterAI implementation.\n // The removed vertex is merged on the nearest of the edges other extremity\n // that is on an obstacle border.\n var commonVertexContours = new Array(5);\n var commonVertexIndexes = new Array(5);\n // Each pass only filter vertex that have an edge other extremity on an obstacle.\n // Vertex depth (in number of edges to reach an obstacle) is reduces by\n // at least one by each pass.\n var movedAnyVertex = false;\n do {\n movedAnyVertex = false;\n for (var _i = 0, contours_1 = contours; _i < contours_1.length; _i++) {\n var contour = contours_1[_i];\n for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {\n var vertex = contour[vertexIndex];\n var nextVertex = contour[(vertexIndex + 1) % contour.length];\n if (vertex.region !== RasterizationCell.OBSTACLE_REGION_ID &&\n nextVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n // This is a vertex in the middle. It must be removed.\n // Search the contours around the vertex.\n //\n // Typically a contour point to its neighbor and it form a cycle.\n //\n // \\ C /\n // \\ /\n // A | B\n // |\n //\n // C -> B -> A -> C\n //\n // There can be more than 3 contours even if it's rare.\n commonVertexContours.length = 0;\n commonVertexIndexes.length = 0;\n commonVertexContours.push(contour);\n commonVertexIndexes.push(vertexIndex);\n var errorFound = false;\n var commonVertex = vertex;\n do {\n var neighborContour = contoursByRegion[commonVertex.region];\n if (!neighborContour) {\n errorFound = true;\n if (commonVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n console.warn(\"contour already discarded: \" + commonVertex.region);\n }\n break;\n }\n var foundVertex = false;\n for (var neighborVertexIndex = 0; neighborVertexIndex < neighborContour.length; neighborVertexIndex++) {\n var neighborVertex = neighborContour[neighborVertexIndex];\n if (neighborVertex.x === commonVertex.x &&\n neighborVertex.y === commonVertex.y) {\n commonVertexContours.push(neighborContour);\n commonVertexIndexes.push(neighborVertexIndex);\n commonVertex = neighborVertex;\n foundVertex = true;\n break;\n }\n }\n if (!foundVertex) {\n errorFound = true;\n console.error(\"Can't find a common vertex with a neighbor contour. There is probably a superposition.\");\n break;\n }\n } while (commonVertex !== vertex);\n if (errorFound) {\n continue;\n }\n if (commonVertexContours.length < 3) {\n console.error(\"The vertex is shared by only \" + commonVertexContours.length + \" regions.\");\n }\n var shorterEdgeContourIndex = -1;\n var edgeLengthMin = Number.MAX_VALUE;\n for (var index = 0; index < commonVertexContours.length; index++) {\n var vertexContour = commonVertexContours[index];\n var vertexIndex_1 = commonVertexIndexes[index];\n var previousVertex = vertexContour[(vertexIndex_1 - 1 + vertexContour.length) %\n vertexContour.length];\n if (previousVertex.region === RasterizationCell.OBSTACLE_REGION_ID) {\n var deltaX = previousVertex.x - vertex.x;\n var deltaY = previousVertex.y - vertex.y;\n var lengthSq = deltaX * deltaX + deltaY * deltaY;\n if (lengthSq < edgeLengthMin) {\n edgeLengthMin = lengthSq;\n shorterEdgeContourIndex = index;\n }\n }\n }\n if (shorterEdgeContourIndex === -1) {\n // A vertex has no neighbor on an obstacle.\n // It will be solved in next iterations.\n continue;\n }\n // Merge the vertex on the other extremity of the smallest of the 3 edges.\n //\n // \\ C /\n // \\ /\n // A | B\n // |\n //\n // - the shortest edge is between A and B\n // - the Y will become a V\n // - vertices are store clockwise\n // - there can be more than one C (it's rare)\n // This is B\n var shorterEdgeContour = commonVertexContours[shorterEdgeContourIndex];\n var shorterEdgeVertexIndex = commonVertexIndexes[shorterEdgeContourIndex];\n var shorterEdgeExtremityVertex = shorterEdgeContour[(shorterEdgeVertexIndex - 1 + shorterEdgeContour.length) %\n shorterEdgeContour.length];\n // This is A\n var shorterEdgeOtherContourIndex = (shorterEdgeContourIndex + 1) % commonVertexContours.length;\n var shorterEdgeOtherContour = commonVertexContours[shorterEdgeOtherContourIndex];\n var shorterEdgeOtherVertexIndex = commonVertexIndexes[shorterEdgeOtherContourIndex];\n for (var index = 0; index < commonVertexContours.length; index++) {\n if (index === shorterEdgeContourIndex ||\n index === shorterEdgeOtherContourIndex) {\n continue;\n }\n // These are C\n var commonVertexContour = commonVertexContours[index];\n var commonVertexIndex = commonVertexIndexes[index];\n // Move the vertex to an obstacle border\n var movedVertex = commonVertexContour[commonVertexIndex];\n movedVertex.x = shorterEdgeExtremityVertex.x;\n movedVertex.y = shorterEdgeExtremityVertex.y;\n movedVertex.region = RasterizationCell.NULL_REGION_ID;\n }\n // There is no more border between A and B,\n // update the region from B to C.\n shorterEdgeOtherContour[(shorterEdgeOtherVertexIndex + 1) % shorterEdgeOtherContour.length].region =\n shorterEdgeOtherContour[shorterEdgeOtherVertexIndex].region;\n // Remove in A and B the vertex that's been move in C.\n shorterEdgeContour.splice(shorterEdgeVertexIndex, 1);\n shorterEdgeOtherContour.splice(shorterEdgeOtherVertexIndex, 1);\n movedAnyVertex = true;\n }\n }\n }\n } while (movedAnyVertex);\n // Clean the polygons from identical vertices.\n //\n // This can happen with 2 vertices regions.\n // 2 edges are superposed and there extremity is the same.\n // One is move over the other.\n // I could observe this with a region between 2 regions\n // where one of one of these 2 regions were also encompassed.\n // A bit like a rainbow, 2 big regions: the land, the sky\n // and 2 regions for the colors.\n //\n // The vertex can't be removed during the process because\n // they hold data used by other merging.\n //\n // Some contour will have no vertex left.\n // It more efficient to let the next step ignore them.\n for (var _a = 0, contours_2 = contours; _a < contours_2.length; _a++) {\n var contour = contours_2[_a];\n for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {\n var vertex = contour[vertexIndex];\n var nextVertexIndex = (vertexIndex + 1) % contour.length;\n var nextVertex = contour[nextVertexIndex];\n if (vertex.x === nextVertex.x && vertex.y === nextVertex.y) {\n contour.splice(nextVertexIndex, 1);\n vertexIndex--;\n }\n }\n }\n };\n /**\n * Walk around the edge of this cell's region gathering vertices that\n * represent the corners of each cell on the sides that are external facing.\n *\n * There will be two or three vertices for each edge cell:\n * Two for cells that don't represent a change in edge direction. Three\n * for cells that represent a change in edge direction.\n *\n * The output array will contain vertices ordered as follows:\n * (x, y, z, regionID) where regionID is the region (obstacle or real) that\n * this vertex is considered to be connected to.\n *\n * WARNING: Only run this operation on cells that are already known\n * to be on a region edge. The direction must also be pointing to a\n * valid edge. Otherwise behavior will be undefined.\n *\n * @param grid the grid of cells\n * @param startCell A cell that is known to be on the edge of a region\n * (part of a region contour).\n * @param startDirection The direction of the edge of the cell that is\n * known to point\n * across the region edge.\n * @param outContourVertices The list of vertices that represent the edge\n * of the region.\n */\n ContourBuilder.prototype.buildRawContours = function (grid, startCell, startDirection, outContourVertices) {\n // Flaw in Algorithm:\n //\n // This method of contour generation can result in an inappropriate\n // impassable seam between two adjacent regions in the following case:\n //\n // 1. One region connects to another region on two sides in an\n // uninterrupted manner (visualize one region wrapping in an L\n // shape around the corner of another).\n // 2. At the corner shared by the two regions, a change in height\n // occurs.\n //\n // In this case, the two regions should share a corner vertex\n // (an obtuse corner vertex for one region and an acute corner\n // vertex for the other region).\n //\n // In reality, though this algorithm will select the same (x, z)\n // coordinates for each region's corner vertex, the vertex heights\n // may differ, eventually resulting in an impassable seam.\n // It is a bit hard to describe the stepping portion of this algorithm.\n // One way to visualize it is to think of a robot sitting on the\n // floor facing a known wall. It then does the following to skirt\n // the wall:\n // 1. If there is a wall in front of it, turn clockwise in 90 degrees\n // increments until it finds the wall is gone.\n // 2. Move forward one step.\n // 3. Turn counter-clockwise by 90 degrees.\n // 4. Repeat from step 1 until it finds itself at its original\n // location facing its original direction.\n //\n // See also: http://www.critterai.org/projects/nmgen_study/contourgen.html#robotwalk\n var cell = startCell;\n var direction = startDirection;\n var loopCount = 0;\n do {\n // Note: The design of this loop is such that the cell variable\n // will always reference an edge cell from the same region as\n // the start cell.\n if ((cell.contourFlags & (1 << direction)) !== 0) {\n // The current direction is pointing toward an edge.\n // Get this edge's vertex.\n var delta = ContourBuilder.leftVertexOfFacingCellBorderDeltas[direction];\n var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);\n outContourVertices.push({\n x: cell.x + delta.x,\n y: cell.y + delta.y,\n region: neighbor.regionID,\n });\n // Remove the flag for this edge. We never need to consider\n // it again since we have a vertex for this edge.\n cell.contourFlags &= ~(1 << direction);\n // Rotate in clockwise direction.\n direction = (direction + 1) & 0x3;\n }\n else {\n // The current direction does not point to an edge. So it\n // must point to a neighbor cell in the same region as the\n // current cell. Move to the neighbor and swing the search\n // direction back one increment (counterclockwise).\n // By moving the direction back one increment we guarantee we\n // don't miss any edges.\n var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);\n cell = neighbor;\n direction = (direction + 3) & 0x3; // Rotate counterclockwise.\n }\n // The loop limit is arbitrary. It exists only to guarantee that\n // bad input data doesn't result in an infinite loop.\n // The only down side of this loop limit is that it limits the\n // number of detectable edge vertices (the longer the region edge\n // and the higher the number of \"turns\" in a region's edge, the less\n // edge vertices can be detected for that region).\n } while (!(cell === startCell && direction === startDirection) &&\n ++loopCount < 65535);\n return outContourVertices;\n };\n /**\n * Takes a group of vertices that represent a region contour and changes\n * it in the following manner:\n * - For any edges that connect to non-obstacle regions, remove all\n * vertices except the start and end vertices for that edge (this\n * smooths the edges between non-obstacle regions into a straight line).\n * - Runs an algorithm's against the contour to follow the edge more closely.\n *\n * @param regionID The region the contour was derived from.\n * @param sourceVertices The source vertices that represent the complex\n * contour.\n * @param outVertices The simplified contour vertices.\n * @param threshold The maximum distance the edge of the contour may deviate\n * from the source geometry.\n */\n ContourBuilder.prototype.generateSimplifiedContour = function (regionID, sourceVertices, outVertices, threshold) {\n var noConnections = true;\n for (var _i = 0, sourceVertices_1 = sourceVertices; _i < sourceVertices_1.length; _i++) {\n var sourceVertex = sourceVertices_1[_i];\n if (sourceVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {\n noConnections = false;\n break;\n }\n }\n // Seed the simplified contour with the mandatory edges\n // (At least one edge).\n if (noConnections) {\n // This contour represents an island region surrounded only by the\n // obstacle region. Seed the simplified contour with the source's\n // lower left (ll) and upper right (ur) vertices.\n var lowerLeftX = sourceVertices[0].x;\n var lowerLeftY = sourceVertices[0].y;\n var lowerLeftIndex = 0;\n var upperRightX = sourceVertices[0].x;\n var upperRightY = sourceVertices[0].y;\n var upperRightIndex = 0;\n for (var index = 0; index < sourceVertices.length; index++) {\n var sourceVertex = sourceVertices[index];\n var x = sourceVertex.x;\n var y = sourceVertex.y;\n if (x < lowerLeftX || (x === lowerLeftX && y < lowerLeftY)) {\n lowerLeftX = x;\n lowerLeftY = y;\n lowerLeftIndex = index;\n }\n if (x >= upperRightX || (x === upperRightX && y > upperRightY)) {\n upperRightX = x;\n upperRightY = y;\n upperRightIndex = index;\n }\n }\n // The region attribute is used to store an index locally in this function.\n // TODO Maybe there is a way to do this cleanly and keep no memory footprint.\n // Seed the simplified contour with this edge.\n outVertices.push({\n x: lowerLeftX,\n y: lowerLeftY,\n region: lowerLeftIndex,\n });\n outVertices.push({\n x: upperRightX,\n y: upperRightY,\n region: upperRightIndex,\n });\n }\n else {\n // The contour shares edges with other non-obstacle regions.\n // Seed the simplified contour with a new vertex for every\n // location where the region connection changes. These are\n // vertices that are important because they represent portals\n // to other regions.\n for (var index = 0; index < sourceVertices.length; index++) {\n var sourceVert = sourceVertices[index];\n if (sourceVert.region !==\n sourceVertices[(index + 1) % sourceVertices.length].region) {\n // The current vertex has a different region than the\n // next vertex. So there is a change in vertex region.\n outVertices.push({\n x: sourceVert.x,\n y: sourceVert.y,\n region: index,\n });\n }\n }\n }\n this.matchObstacleRegionEdges(sourceVertices, outVertices, threshold);\n if (outVertices.length < 2) {\n // It will be ignored by the triangulation.\n // It should be rare enough not to handle it now.\n console.warn(\"A region is encompassed in another region. It will be ignored.\");\n }\n // There can be polygons with only 2 vertices when a region is between\n // 2 non-obstacles regions. It's still a useful information to filter\n // vertices in the middle (not on an obstacle region border).\n // In this case, the CritterAI implementation adds a 3rd point to avoid\n // invisible polygons, but it makes it difficult to filter it later.\n // Replace the index pointers in the output list with region IDs.\n for (var _a = 0, outVertices_1 = outVertices; _a < outVertices_1.length; _a++) {\n var outVertex = outVertices_1[_a];\n outVertex.region = sourceVertices[outVertex.region].region;\n }\n };\n /**\n * Applies an algorithm to contours which results in obstacle-region edges\n * following the original detail source geometry edge more closely.\n * http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple\n *\n * Adds vertices from the source list to the result list such that\n * if any obstacle region vertices are compared against the result list,\n * none of the vertices will be further from the obstacle region edges than\n * the allowed threshold.\n *\n * Only obstacle-region edges are operated on. All other edges are\n * ignored.\n *\n * The result vertices is expected to be seeded with at least two\n * source vertices.\n *\n * @param sourceVertices\n * @param inoutResultVertices\n * @param threshold The maximum distance the edge of the contour may deviate\n * from the source geometry.\n */\n ContourBuilder.prototype.matchObstacleRegionEdges = function (sourceVertices, inoutResultVertices, threshold) {\n // This implementation is strongly inspired from CritterAI class \"MatchNullRegionEdges\".\n // Loop through all edges in this contour.\n //\n // NOTE: The simplifiedVertCount in the loop condition\n // increases over iterations. That is what keeps the loop going beyond\n // the initial vertex count.\n var resultIndexA = 0;\n while (resultIndexA < inoutResultVertices.length) {\n var resultIndexB = (resultIndexA + 1) % inoutResultVertices.length;\n // The line segment's beginning vertex.\n var ax = inoutResultVertices[resultIndexA].x;\n var az = inoutResultVertices[resultIndexA].y;\n var sourceIndexA = inoutResultVertices[resultIndexA].region;\n // The line segment's ending vertex.\n var bx = inoutResultVertices[resultIndexB].x;\n var bz = inoutResultVertices[resultIndexB].y;\n var sourceIndexB = inoutResultVertices[resultIndexB].region;\n // The source index of the next vertex to test (the vertex just\n // after the current vertex in the source vertex list).\n var testedSourceIndex = (sourceIndexA + 1) % sourceVertices.length;\n var maxDeviation = 0;\n // Default to no index. No new vert to add.\n var toInsertSourceIndex = -1;\n if (sourceVertices[testedSourceIndex].region ===\n RasterizationCell.OBSTACLE_REGION_ID) {\n // This test vertex is part of a obstacle region edge.\n // Loop through the source vertices until the end vertex\n // is found, searching for the vertex that is farthest from\n // the line segment formed by the begin/end vertices.\n //\n // Visualizations:\n // http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple\n while (testedSourceIndex !== sourceIndexB) {\n var deviation = Geometry.getPointSegmentDistanceSq(sourceVertices[testedSourceIndex].x, sourceVertices[testedSourceIndex].y, ax, az, bx, bz);\n if (deviation > maxDeviation) {\n // A new maximum deviation was detected.\n maxDeviation = deviation;\n toInsertSourceIndex = testedSourceIndex;\n }\n // Move to the next vertex.\n testedSourceIndex = (testedSourceIndex + 1) % sourceVertices.length;\n }\n }\n if (toInsertSourceIndex !== -1 && maxDeviation > threshold * threshold) {\n // A vertex was found that is further than allowed from the\n // current edge. Add this vertex to the contour.\n inoutResultVertices.splice(resultIndexA + 1, 0, {\n x: sourceVertices[toInsertSourceIndex].x,\n y: sourceVertices[toInsertSourceIndex].y,\n region: toInsertSourceIndex,\n });\n // Not incrementing the vertex since we need to test the edge\n // formed by vertA and this this new vertex on the next\n // iteration of the loop.\n }\n // This edge segment does not need to be altered. Move to\n // the next vertex.\n else\n resultIndexA++;\n }\n };\n ContourBuilder.leftVertexOfFacingCellBorderDeltas = [\n { x: 0, y: 1 },\n { x: 1, y: 1 },\n { x: 1, y: 0 },\n { x: 0, y: 0 },\n ];\n return ContourBuilder;\n}());\n\n/**\n * Builds convex polygons from the provided polygons.\n *\n * This implementation is strongly inspired from CritterAI class \"PolyMeshFieldBuilder\".\n * http://www.critterai.org/projects/nmgen_study/polygen.html\n */\nvar ConvexPolygonGenerator = /** @class */ (function () {\n function ConvexPolygonGenerator() {\n }\n /**\n * Builds convex polygons from the provided polygons.\n * @param concavePolygons The content is manipulated during the operation\n * and it will be left in an undefined state at the end of\n * the operation.\n * @param maxVerticesPerPolygon cap the vertex number in return polygons.\n * @return convex polygons.\n */\n ConvexPolygonGenerator.prototype.splitToConvexPolygons = function (concavePolygons, maxVerticesPerPolygon) {\n // The maximum possible number of polygons assuming that all will\n // be triangles.\n var maxPossiblePolygons = 0;\n // The maximum vertices found in a single contour.\n var maxVerticesPerContour = 0;\n for (var _i = 0, concavePolygons_1 = concavePolygons; _i < concavePolygons_1.length; _i++) {\n var contour = concavePolygons_1[_i];\n var count = contour.length;\n maxPossiblePolygons += count - 2;\n maxVerticesPerContour = Math.max(maxVerticesPerContour, count);\n }\n // Each list is initialized to a size that will minimize resizing.\n var convexPolygons = new Array(maxPossiblePolygons);\n convexPolygons.length = 0;\n // Various working variables.\n // (Values are meaningless outside of the iteration)\n var workingContourFlags = new Array(maxVerticesPerContour);\n workingContourFlags.length = 0;\n var workingPolygons = new Array(maxVerticesPerContour + 1);\n workingPolygons.length = 0;\n var workingMergeInfo = {\n lengthSq: -1,\n polygonAVertexIndex: -1,\n polygonBVertexIndex: -1,\n };\n var workingMergedPolygon = new Array(maxVerticesPerPolygon);\n workingMergedPolygon.length = 0;\n var _loop_1 = function (contour) {\n if (contour.length < 3) {\n return \"continue\";\n }\n // Initialize the working polygon array.\n workingPolygons.length = 0;\n // Triangulate the contour.\n var foundAnyTriangle = false;\n this_1.triangulate(contour, workingContourFlags, function (p1, p2, p3) {\n var workingPolygon = new Array(maxVerticesPerPolygon);\n workingPolygon.length = 0;\n workingPolygon.push(p1);\n workingPolygon.push(p2);\n workingPolygon.push(p3);\n workingPolygons.push(workingPolygon);\n foundAnyTriangle = true;\n });\n if (!foundAnyTriangle) {\n /*\n * Failure of the triangulation.\n * This is known to occur if the source polygon is\n * self-intersecting or the source region contains internal\n * holes. In both cases, the problem is likely due to bad\n * region formation.\n */\n console.error(\"Polygon generation failure: Could not triangulate contour.\");\n console.error(\"contour:\" +\n contour.map(function (point) { return point.x + \" \" + point.y; }).join(\" ; \"));\n return \"continue\";\n }\n if (maxVerticesPerPolygon > 3) {\n // Merging of triangles into larger polygons is permitted.\n // Continue until no polygons can be found to merge.\n // http://www.critterai.org/nmgen_polygen#mergepolys\n while (true) {\n var longestMergeEdge = -1;\n var bestPolygonA = [];\n var polygonAVertexIndex = -1; // Start of the shared edge.\n var bestPolygonB = [];\n var polygonBVertexIndex = -1; // Start of the shared edge.\n var bestPolygonBIndex = -1;\n // Loop through all but the last polygon looking for the\n // best polygons to merge in this iteration.\n for (var indexA = 0; indexA < workingPolygons.length - 1; indexA++) {\n var polygonA = workingPolygons[indexA];\n for (var indexB = indexA + 1; indexB < workingPolygons.length; indexB++) {\n var polygonB = workingPolygons[indexB];\n // Can polyB merge with polyA?\n this_1.getPolyMergeInfo(polygonA, polygonB, maxVerticesPerPolygon, workingMergeInfo);\n if (workingMergeInfo.lengthSq > longestMergeEdge) {\n // polyB has the longest shared edge with\n // polyA found so far. Save the merge\n // information.\n longestMergeEdge = workingMergeInfo.lengthSq;\n bestPolygonA = polygonA;\n polygonAVertexIndex = workingMergeInfo.polygonAVertexIndex;\n bestPolygonB = polygonB;\n polygonBVertexIndex = workingMergeInfo.polygonBVertexIndex;\n bestPolygonBIndex = indexB;\n }\n }\n }\n if (longestMergeEdge <= 0)\n // No valid merges found during this iteration.\n break;\n // Found polygons to merge. Perform the merge.\n /*\n * Fill the mergedPoly array.\n * Start the vertex at the end of polygon A's shared edge.\n * Add all vertices until looping back to the vertex just\n * before the start of the shared edge. Repeat for\n * polygon B.\n *\n * Duplicate vertices are avoided, while ensuring we get\n * all vertices, since each loop drops the vertex that\n * starts its polygon's shared edge and:\n *\n * PolyAStartVert == PolyBEndVert and\n * PolyAEndVert == PolyBStartVert.\n */\n var vertCountA = bestPolygonA.length;\n var vertCountB = bestPolygonB.length;\n workingMergedPolygon.length = 0;\n for (var i = 0; i < vertCountA - 1; i++)\n workingMergedPolygon.push(bestPolygonA[(polygonAVertexIndex + 1 + i) % vertCountA]);\n for (var i = 0; i < vertCountB - 1; i++)\n workingMergedPolygon.push(bestPolygonB[(polygonBVertexIndex + 1 + i) % vertCountB]);\n // Copy the merged polygon over the top of polygon A.\n bestPolygonA.length = 0;\n Array.prototype.push.apply(bestPolygonA, workingMergedPolygon);\n // Remove polygon B\n workingPolygons.splice(bestPolygonBIndex, 1);\n }\n }\n // Polygon creation for this contour is complete.\n // Add polygons to the global polygon array\n Array.prototype.push.apply(convexPolygons, workingPolygons);\n };\n var this_1 = this;\n // Split every concave polygon into convex polygons.\n for (var _a = 0, concavePolygons_2 = concavePolygons; _a < concavePolygons_2.length; _a++) {\n var contour = concavePolygons_2[_a];\n _loop_1(contour);\n }\n // The original implementation builds polygon adjacency information.\n // but the library for the pathfinding already does it.\n return convexPolygons;\n };\n /**\n * Checks two polygons to see if they can be merged. If a merge is\n * allowed, provides data via the outResult argument (see {@link PolyMergeResult}).\n *\n * @param polygonA The polygon A\n * @param polygonB The polygon B\n * @param maxVerticesPerPolygon cap the vertex number in return polygons.\n * @param outResult contains merge information.\n */\n ConvexPolygonGenerator.prototype.getPolyMergeInfo = function (polygonA, polygonB, maxVerticesPerPolygon, outResult) {\n outResult.lengthSq = -1; // Default to invalid merge\n outResult.polygonAVertexIndex = -1;\n outResult.polygonBVertexIndex = -1;\n var vertexCountA = polygonA.length;\n var vertexCountB = polygonB.length;\n // If the merged polygon would would have to many vertices, do not\n // merge. Subtracting two since to take into account the effect of\n // a merge.\n if (vertexCountA + vertexCountB - 2 > maxVerticesPerPolygon)\n return;\n // Check if the polygons share an edge.\n for (var indexA = 0; indexA < vertexCountA; indexA++) {\n // Get the vertex indices for the polygonA edge\n var vertexA = polygonA[indexA];\n var nextVertexA = polygonA[(indexA + 1) % vertexCountA];\n // Search polygonB for matches.\n for (var indexB = 0; indexB < vertexCountB; indexB++) {\n // Get the vertex indices for the polygonB edge.\n var vertexB = polygonB[indexB];\n var nextVertexB = polygonB[(indexB + 1) % vertexCountB];\n // === can be used because vertices comme from the same concave polygon.\n if (vertexA === nextVertexB && nextVertexA === vertexB) {\n // The vertex indices for this edge are the same and\n // sequenced in opposite order. So the edge is shared.\n outResult.polygonAVertexIndex = indexA;\n outResult.polygonBVertexIndex = indexB;\n }\n }\n }\n if (outResult.polygonAVertexIndex === -1)\n // No common edge, cannot merge.\n return;\n // Check to see if the merged polygon would be convex.\n //\n // Gets the vertices near the section where the merge would occur.\n // Do they form a concave section? If so, the merge is invalid.\n //\n // Note that the following algorithm is only valid for clockwise\n // wrapped convex polygons.\n var sharedVertMinus = polygonA[(outResult.polygonAVertexIndex - 1 + vertexCountA) % vertexCountA];\n var sharedVert = polygonA[outResult.polygonAVertexIndex];\n var sharedVertPlus = polygonB[(outResult.polygonBVertexIndex + 2) % vertexCountB];\n if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {\n // The shared vertex (center) is not to the left of segment\n // vertMinus->vertPlus. For a clockwise wrapped polygon, this\n // indicates a concave section. Merged polygon would be concave.\n // Invalid merge.\n return;\n }\n sharedVertMinus =\n polygonB[(outResult.polygonBVertexIndex - 1 + vertexCountB) % vertexCountB];\n sharedVert = polygonB[outResult.polygonBVertexIndex];\n sharedVertPlus =\n polygonA[(outResult.polygonAVertexIndex + 2) % vertexCountA];\n if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {\n // The shared vertex (center) is not to the left of segment\n // vertMinus->vertPlus. For a clockwise wrapped polygon, this\n // indicates a concave section. Merged polygon would be concave.\n // Invalid merge.\n return;\n }\n // Get the vertex indices that form the shared edge.\n sharedVertMinus = polygonA[outResult.polygonAVertexIndex];\n sharedVert = polygonA[(outResult.polygonAVertexIndex + 1) % vertexCountA];\n // Store the lengthSq of the shared edge.\n var deltaX = sharedVertMinus.x - sharedVert.x;\n var deltaZ = sharedVertMinus.y - sharedVert.y;\n outResult.lengthSq = deltaX * deltaX + deltaZ * deltaZ;\n };\n /**\n * Attempts to triangulate a polygon.\n *\n * @param vertices the polygon to be triangulate.\n * The content is manipulated during the operation\n * and it will be left in an undefined state at the end of\n * the operation.\n * @param vertexFlags only used internally\n * @param outTriangles is called for each triangle derived\n * from the original polygon.\n * @return The number of triangles generated. Or, if triangulation\n * failed, a negative number.\n */\n ConvexPolygonGenerator.prototype.triangulate = function (vertices, vertexFlags, outTriangles) {\n // Terminology, concepts and such:\n //\n // This algorithm loops around the edges of a polygon looking for\n // new internal edges to add that will partition the polygon into a\n // new valid triangle internal to the starting polygon. During each\n // iteration the shortest potential new edge is selected to form that\n // iteration's new triangle.\n //\n // Triangles will only be formed if a single new edge will create\n // a triangle. Two new edges will never be added during a single\n // iteration. This means that the triangulated portions of the\n // original polygon will only contain triangles and the only\n // non-triangle polygon will exist in the untriangulated portion\n // of the original polygon.\n //\n // \"Partition edge\" refers to a potential new edge that will form a\n // new valid triangle.\n //\n // \"Center\" vertex refers to the vertex in a potential new triangle\n // which, if the triangle is formed, will be external to the\n // remaining untriangulated portion of the polygon. Since it\n // is now external to the polygon, it can't be used to form any\n // new triangles.\n //\n // Some documentation refers to \"iPlus2\" even though the variable is\n // not in scope or does not exist for that section of code. For\n // documentation purposes, iPlus2 refers to the 2nd vertex after the\n // primary vertex.\n // E.g.: i, iPlus1, and iPlus2.\n //\n // Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#triangulation\n // Loop through all vertices, flagging all indices that represent\n // a center vertex of a valid new triangle.\n vertexFlags.length = vertices.length;\n for (var i = 0; i < vertices.length; i++) {\n var iPlus1 = (i + 1) % vertices.length;\n var iPlus2 = (i + 2) % vertices.length;\n // A triangle formed by i, iPlus1, and iPlus2 will result\n // in a valid internal triangle.\n // Flag the center vertex (iPlus1) to indicate a valid triangle\n // location.\n vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, iPlus2, vertices);\n }\n // Loop through the vertices creating triangles. When there is only a\n // single triangle left, the operation is complete.\n //\n // When a valid triangle is formed, remove its center vertex. So for\n // each loop, a single vertex will be removed.\n //\n // At the start of each iteration the indices list is in the following\n // state:\n // - Represents a simple polygon representing the un-triangulated\n // portion of the original polygon.\n // - All valid center vertices are flagged.\n while (vertices.length > 3) {\n // Find the shortest new valid edge.\n // NOTE: i and iPlus1 are defined in two different scopes in\n // this section. So be careful.\n // Loop through all indices in the remaining polygon.\n var minLengthSq = Number.MAX_VALUE;\n var minLengthSqVertexIndex = -1;\n for (var i_1 = 0; i_1 < vertices.length; i_1++) {\n if (vertexFlags[(i_1 + 1) % vertices.length]) {\n // Indices i, iPlus1, and iPlus2 are known to form a\n // valid triangle.\n var vert = vertices[i_1];\n var vertPlus2 = vertices[(i_1 + 2) % vertices.length];\n // Determine the length of the partition edge.\n // (i -> iPlus2)\n var deltaX = vertPlus2.x - vert.x;\n var deltaY = vertPlus2.y - vert.y;\n var lengthSq = deltaX * deltaX + deltaY * deltaY;\n if (lengthSq < minLengthSq) {\n minLengthSq = lengthSq;\n minLengthSqVertexIndex = i_1;\n }\n }\n }\n if (minLengthSqVertexIndex === -1)\n // Could not find a new triangle. Triangulation failed.\n // This happens if there are three or more vertices\n // left, but none of them are flagged as being a\n // potential center vertex.\n return;\n var i = minLengthSqVertexIndex;\n var iPlus1 = (i + 1) % vertices.length;\n // Add the new triangle to the output.\n outTriangles(vertices[i], vertices[iPlus1], vertices[(i + 2) % vertices.length]);\n // iPlus1, the \"center\" vert in the new triangle, is now external\n // to the untriangulated portion of the polygon. Remove it from\n // the vertices list since it cannot be a member of any new\n // triangles.\n vertices.splice(iPlus1, 1);\n vertexFlags.splice(iPlus1, 1);\n if (iPlus1 === 0 || iPlus1 >= vertices.length) {\n // The vertex removal has invalidated iPlus1 and/or i. So\n // force a wrap, fixing the indices so they reference the\n // correct indices again. This only occurs when the new\n // triangle is formed across the wrap location of the polygon.\n // Case 1: i = 14, iPlus1 = 15, iPlus2 = 0\n // Case 2: i = 15, iPlus1 = 0, iPlus2 = 1;\n i = vertices.length - 1;\n iPlus1 = 0;\n }\n // At this point i and iPlus1 refer to the two indices from a\n // successful triangulation that will be part of another new\n // triangle. We now need to re-check these indices to see if they\n // can now be the center index in a potential new partition.\n vertexFlags[i] = ConvexPolygonGenerator.isValidPartition((i - 1 + vertices.length) % vertices.length, iPlus1, vertices);\n vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, (i + 2) % vertices.length, vertices);\n }\n // Only 3 vertices remain.\n // Add their triangle to the output list.\n outTriangles(vertices[0], vertices[1], vertices[2]);\n };\n /**\n * Check if the line segment formed by vertex A and vertex B will\n * form a valid partition of the polygon.\n *\n * I.e. the line segment AB is internal to the polygon and will not\n * cross existing line segments.\n *\n * Assumptions:\n * - The vertices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * - indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if the line segment formed by vertex A and vertex B will\n * form a valid partition of the polygon.\n */\n ConvexPolygonGenerator.isValidPartition = function (indexA, indexB, vertices) {\n // First check whether the segment AB lies within the internal\n // angle formed at A (this is the faster check).\n // If it does, then perform the more costly check.\n return (ConvexPolygonGenerator.liesWithinInternalAngle(indexA, indexB, vertices) &&\n !ConvexPolygonGenerator.hasIllegalEdgeIntersection(indexA, indexB, vertices));\n };\n /**\n * Check if vertex B lies within the internal angle of the polygon\n * at vertex A.\n *\n * Vertex B does not have to be within the polygon border. It just has\n * be be within the area encompassed by the internal angle formed at\n * vertex A.\n *\n * This operation is a fast way of determining whether a line segment\n * can possibly form a valid polygon partition. If this test returns\n * FALSE, then more expensive checks can be skipped.\n *\n * Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#anglecheck\n *\n * Special case:\n * FALSE is returned if vertex B lies directly on either of the rays\n * cast from vertex A along its associated polygon edges. So the test\n * on vertex B is exclusive of the polygon edges.\n *\n * Assumptions:\n * - The vertices and indices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * -indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if vertex B lies within the internal angle of\n * the polygon at vertex A.\n */\n ConvexPolygonGenerator.liesWithinInternalAngle = function (indexA, indexB, vertices) {\n // Get pointers to the main vertices being tested.\n var vertexA = vertices[indexA];\n var vertexB = vertices[indexB];\n // Get pointers to the vertices just before and just after vertA.\n var vertexAMinus = vertices[(indexA - 1 + vertices.length) % vertices.length];\n var vertexAPlus = vertices[(indexA + 1) % vertices.length];\n // First, find which of the two angles formed by the line segments\n // AMinus->A->APlus is internal to (pointing towards) the polygon.\n // Then test to see if B lies within the area formed by that angle.\n // TRUE if A is left of or on line AMinus->APlus\n if (ConvexPolygonGenerator.isLeftOrCollinear(vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y, vertexAPlus.x, vertexAPlus.y))\n // The angle internal to the polygon is <= 180 degrees\n // (non-reflex angle).\n // Test to see if B lies within this angle.\n return (ConvexPolygonGenerator.isLeft(\n // TRUE if B is left of line A->AMinus\n vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y) &&\n // TRUE if B is right of line A->APlus\n ConvexPolygonGenerator.isRight(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y));\n // The angle internal to the polygon is > 180 degrees (reflex angle).\n // Test to see if B lies within the external (<= 180 degree) angle and\n // flip the result. (If B lies within the external angle, it can't\n // lie within the internal angle)\n return !(\n // TRUE if B is left of or on line A->APlus\n (ConvexPolygonGenerator.isLeftOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y) &&\n // TRUE if B is right of or on line A->AMinus\n ConvexPolygonGenerator.isRightOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y)));\n };\n /**\n * Check if the line segment AB intersects any edges not already\n * connected to one of the two vertices.\n *\n * Assumptions:\n * - The vertices and indices arguments define a valid simple polygon\n * with vertices wrapped clockwise.\n * - indexA != indexB\n *\n * Behavior is undefined if the arguments to not meet these\n * assumptions\n *\n * @param indexA the index of the vertex that will form the segment AB.\n * @param indexB the index of the vertex that will form the segment AB.\n * @param vertices a polygon wrapped clockwise.\n * @return true if the line segment AB intersects any edges not already\n * connected to one of the two vertices.\n */\n ConvexPolygonGenerator.hasIllegalEdgeIntersection = function (indexA, indexB, vertices) {\n // Get pointers to the primary vertices being tested.\n var vertexA = vertices[indexA];\n var vertexB = vertices[indexB];\n // Loop through the polygon edges.\n for (var edgeBeginIndex = 0; edgeBeginIndex < vertices.length; edgeBeginIndex++) {\n var edgeEndIndex = (edgeBeginIndex + 1) % vertices.length;\n if (edgeBeginIndex === indexA ||\n edgeBeginIndex === indexB ||\n edgeEndIndex === indexA ||\n edgeEndIndex === indexB) {\n continue;\n }\n // Neither of the test indices are endpoints of this edge.\n // Get this edge's vertices.\n var edgeBegin = vertices[edgeBeginIndex];\n var edgeEnd = vertices[edgeEndIndex];\n if ((edgeBegin.x === vertexA.x && edgeBegin.y === vertexA.y) ||\n (edgeBegin.x === vertexB.x && edgeBegin.y === vertexB.y) ||\n (edgeEnd.x === vertexA.x && edgeEnd.y === vertexA.y) ||\n (edgeEnd.x === vertexB.x && edgeEnd.y === vertexB.y)) {\n // One of the test vertices is co-located\n // with one of the endpoints of this edge (this is a\n // test of the actual position of the vertices rather than\n // simply the index check performed earlier).\n // Skip this edge.\n continue;\n }\n // This edge is not connected to either of the test vertices.\n // If line segment AB intersects with this edge, then the\n // intersection is illegal.\n // I.e. New edges cannot cross existing edges.\n if (Geometry.segmentsIntersect(vertexA.x, vertexA.y, vertexB.x, vertexB.y, edgeBegin.x, edgeBegin.y, edgeEnd.x, edgeEnd.y)) {\n return true;\n }\n }\n return false;\n };\n /**\n * Check if point P is to the left of line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the left of line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isLeft = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) < 0;\n };\n /**\n * Check if point P is to the left of line AB when looking\n * from A to B or is collinear with line AB.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the left of line AB when looking\n * from A to B, or is collinear with line AB.\n */\n ConvexPolygonGenerator.isLeftOrCollinear = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) <= 0;\n };\n /**\n * Check if point P is to the right of line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the right of line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isRight = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) > 0;\n };\n /**\n * Check if point P is to the right of or on line AB when looking\n * from A to B.\n * @param px The x-value of the point to test.\n * @param py The y-value of the point to test.\n * @param ax The x-value of the point (ax, ay) that is point A on line AB.\n * @param ay The y-value of the point (ax, ay) that is point A on line AB.\n * @param bx The x-value of the point (bx, by) that is point B on line AB.\n * @param by The y-value of the point (bx, by) that is point B on line AB.\n * @return TRUE if point P is to the right of or on line AB when looking\n * from A to B.\n */\n ConvexPolygonGenerator.isRightOrCollinear = function (px, py, ax, ay, bx, by) {\n return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) >= 0;\n };\n /**\n * The absolute value of the returned value is two times the area of the\n * triangle defined by points (A, B, C).\n *\n * A positive value indicates:\n * - Counterclockwise wrapping of the points.\n * - Point B lies to the right of line AC, looking from A to C.\n *\n * A negative value indicates:\n * - Clockwise wrapping of the points.<\n * - Point B lies to the left of line AC, looking from A to C.\n *\n * A value of zero indicates that all points are collinear or\n * represent the same point.\n *\n * This is a fast operation.\n *\n * @param ax The x-value for point (ax, ay) for vertex A of the triangle.\n * @param ay The y-value for point (ax, ay) for vertex A of the triangle.\n * @param bx The x-value for point (bx, by) for vertex B of the triangle.\n * @param by The y-value for point (bx, by) for vertex B of the triangle.\n * @param cx The x-value for point (cx, cy) for vertex C of the triangle.\n * @param cy The y-value for point (cx, cy) for vertex C of the triangle.\n * @return The signed value of two times the area of the triangle defined\n * by the points (A, B, C).\n */\n ConvexPolygonGenerator.getSignedAreaX2 = function (ax, ay, bx, by, cx, cy) {\n // References:\n // http://softsurfer.com/Archive/algorithm_0101/algorithm_0101.htm#Modern%20Triangles\n // http://mathworld.wolfram.com/TriangleArea.html (Search for \"signed\")\n return (bx - ax) * (cy - ay) - (cx - ax) * (by - ay);\n };\n return ConvexPolygonGenerator;\n}());\n\nvar GridCoordinateConverter = /** @class */ (function () {\n function GridCoordinateConverter() {\n }\n /**\n *\n * @param gridPosition the position on the grid\n * @param position the position on the scene\n * @param scaleY for isometry\n * @returns the position on the scene\n */\n GridCoordinateConverter.prototype.convertFromGridBasis = function (grid, polygons) {\n // point can be shared so them must be copied to be scaled.\n return polygons.map(function (polygon) {\n return polygon.map(function (point) { return grid.convertFromGridBasis(point, { x: 0, y: 0 }); });\n });\n };\n return GridCoordinateConverter;\n}());\n\n/**\n * It rasterizes obstacles on a grid.\n *\n * It flags cells as obstacle to be used by {@link RegionGenerator}.\n */\nvar ObstacleRasterizer = /** @class */ (function () {\n function ObstacleRasterizer() {\n this.workingNodes = new Array(8);\n this.gridBasisIterable = new GridBasisIterable();\n }\n /**\n * Rasterize obstacles on a grid.\n * @param grid\n * @param obstacles\n */\n ObstacleRasterizer.prototype.rasterizeObstacles = function (grid, obstacles) {\n var obstaclesItr = obstacles[Symbol.iterator]();\n for (var next = obstaclesItr.next(); !next.done; next = obstaclesItr.next()) {\n var obstacle = next.value;\n this.gridBasisIterable.set(grid, obstacle);\n var vertices = this.gridBasisIterable;\n var minX = Number.MAX_VALUE;\n var maxX = -Number.MAX_VALUE;\n var minY = Number.MAX_VALUE;\n var maxY = -Number.MAX_VALUE;\n var verticesItr = vertices[Symbol.iterator]();\n for (var next_1 = verticesItr.next(); !next_1.done; next_1 = verticesItr.next()) {\n var vertex = next_1.value;\n minX = Math.min(minX, vertex.x);\n maxX = Math.max(maxX, vertex.x);\n minY = Math.min(minY, vertex.y);\n maxY = Math.max(maxY, vertex.y);\n }\n minX = Math.max(Math.floor(minX), 0);\n maxX = Math.min(Math.ceil(maxX), grid.dimX());\n minY = Math.max(Math.floor(minY), 0);\n maxY = Math.min(Math.ceil(maxY), grid.dimY());\n this.fillPolygon(vertices, minX, maxX, minY, maxY, function (x, y) { return (grid.get(x, y).distanceToObstacle = 0); });\n }\n };\n ObstacleRasterizer.prototype.fillPolygon = function (vertices, minX, maxX, minY, maxY, fill) {\n // The following implementation of the scan-line polygon fill algorithm\n // is strongly inspired from:\n // https://alienryderflex.com/polygon_fill/\n // The original implementation was under this license:\n // public-domain code by Darel Rex Finley, 2007\n // This implementation differ with the following:\n // - it handles float vertices\n // so it focus on pixels center\n // - it is conservative to thin vertical or horizontal polygons\n var fillAnyPixels = false;\n this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {\n for (var pixelX = minX; pixelX < maxX; pixelX++) {\n fillAnyPixels = true;\n fill(pixelX, pixelY);\n }\n });\n if (fillAnyPixels) {\n return;\n }\n this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {\n // conserve thin (less than one cell large) horizontal polygons\n if (minX === maxX) {\n fill(minX, pixelY);\n }\n });\n this.scanX(vertices, minX, maxX, minY, maxY, function (pixelX, minY, maxY) {\n for (var pixelY = minY; pixelY < maxY; pixelY++) {\n fill(pixelX, pixelY);\n }\n // conserve thin (less than one cell large) vertical polygons\n if (minY === maxY) {\n fill(pixelX, minY);\n }\n });\n };\n ObstacleRasterizer.prototype.scanY = function (vertices, minX, maxX, minY, maxY, checkAndFillY) {\n var workingNodes = this.workingNodes;\n // Loop through the rows of the image.\n for (var pixelY = minY; pixelY < maxY; pixelY++) {\n var pixelCenterY = pixelY + 0.5;\n // Build a list of nodes.\n workingNodes.length = 0;\n //let j = vertices.length - 1;\n var verticesItr = vertices[Symbol.iterator]();\n var next = verticesItr.next();\n var vertex = next.value;\n // The iterator always return the same instance.\n // It must be copied to be save for later.\n var firstVertexX = vertex.x;\n var firstVertexY = vertex.y;\n while (!next.done) {\n var previousVertexX = vertex.x;\n var previousVertexY = vertex.y;\n next = verticesItr.next();\n if (next.done) {\n vertex.x = firstVertexX;\n vertex.y = firstVertexY;\n }\n else {\n vertex = next.value;\n }\n if ((vertex.y <= pixelCenterY && pixelCenterY < previousVertexY) ||\n (previousVertexY < pixelCenterY && pixelCenterY <= vertex.y)) {\n workingNodes.push(Math.round(vertex.x +\n ((pixelCenterY - vertex.y) / (previousVertexY - vertex.y)) *\n (previousVertexX - vertex.x)));\n }\n }\n // Sort the nodes, via a simple “Bubble” sort.\n {\n var i = 0;\n while (i < workingNodes.length - 1) {\n if (workingNodes[i] > workingNodes[i + 1]) {\n var swap = workingNodes[i];\n workingNodes[i] = workingNodes[i + 1];\n workingNodes[i + 1] = swap;\n if (i > 0)\n i--;\n }\n else {\n i++;\n }\n }\n }\n // Fill the pixels between node pairs.\n for (var i = 0; i < workingNodes.length; i += 2) {\n if (workingNodes[i] >= maxX) {\n break;\n }\n if (workingNodes[i + 1] <= minX) {\n continue;\n }\n if (workingNodes[i] < minX) {\n workingNodes[i] = minX;\n }\n if (workingNodes[i + 1] > maxX) {\n workingNodes[i + 1] = maxX;\n }\n checkAndFillY(pixelY, workingNodes[i], workingNodes[i + 1]);\n }\n }\n };\n ObstacleRasterizer.prototype.scanX = function (vertices, minX, maxX, minY, maxY, checkAndFillX) {\n var workingNodes = this.workingNodes;\n // Loop through the columns of the image.\n for (var pixelX = minX; pixelX < maxX; pixelX++) {\n var pixelCenterX = pixelX + 0.5;\n // Build a list of nodes.\n workingNodes.length = 0;\n var verticesItr = vertices[Symbol.iterator]();\n var next = verticesItr.next();\n var vertex = next.value;\n // The iterator always return the same instance.\n // It must be copied to be save for later.\n var firstVertexX = vertex.x;\n var firstVertexY = vertex.y;\n while (!next.done) {\n var previousVertexX = vertex.x;\n var previousVertexY = vertex.y;\n next = verticesItr.next();\n if (next.done) {\n vertex.x = firstVertexX;\n vertex.y = firstVertexY;\n }\n else {\n vertex = next.value;\n }\n if ((vertex.x < pixelCenterX && pixelCenterX < previousVertexX) ||\n (previousVertexX < pixelCenterX && pixelCenterX < vertex.x)) {\n workingNodes.push(Math.round(vertex.y +\n ((pixelCenterX - vertex.x) / (previousVertexX - vertex.x)) *\n (previousVertexY - vertex.y)));\n }\n }\n // Sort the nodes, via a simple “Bubble” sort.\n {\n var i = 0;\n while (i < workingNodes.length - 1) {\n if (workingNodes[i] > workingNodes[i + 1]) {\n var swap = workingNodes[i];\n workingNodes[i] = workingNodes[i + 1];\n workingNodes[i + 1] = swap;\n if (i > 0)\n i--;\n }\n else {\n i++;\n }\n }\n }\n // Fill the pixels between node pairs.\n for (var i = 0; i < workingNodes.length; i += 2) {\n if (workingNodes[i] >= maxY) {\n break;\n }\n if (workingNodes[i + 1] <= minY) {\n continue;\n }\n if (workingNodes[i] < minY) {\n workingNodes[i] = minY;\n }\n if (workingNodes[i + 1] > maxY) {\n workingNodes[i + 1] = maxY;\n }\n checkAndFillX(pixelX, workingNodes[i], workingNodes[i + 1]);\n }\n }\n };\n return ObstacleRasterizer;\n}());\n/**\n * Iterable that converts coordinates to the grid.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar GridBasisIterable = /** @class */ (function () {\n function GridBasisIterable() {\n this.grid = null;\n this.sceneVertices = [];\n this.verticesItr = this.sceneVertices[Symbol.iterator]();\n this.result = {\n value: { x: 0, y: 0 },\n done: false,\n };\n }\n GridBasisIterable.prototype.set = function (grid, sceneVertices) {\n this.grid = grid;\n this.sceneVertices = sceneVertices;\n };\n GridBasisIterable.prototype[Symbol.iterator] = function () {\n this.verticesItr = this.sceneVertices[Symbol.iterator]();\n return this;\n };\n GridBasisIterable.prototype.next = function () {\n var next = this.verticesItr.next();\n if (next.done) {\n return next;\n }\n this.grid.convertToGridBasis(next.value, this.result.value);\n return this.result;\n };\n return GridBasisIterable;\n}());\n\n/**\n * Build cohesive regions from the non-obstacle space. It uses the data\n * from the obstacles rasterization {@link ObstacleRasterizer}.\n *\n * This implementation is strongly inspired from CritterAI class \"OpenHeightfieldBuilder\".\n *\n * Introduction to Height Fields: http://www.critterai.org/projects/nmgen_study/heightfields.html\n *\n * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html\n */\nvar RegionGenerator = /** @class */ (function () {\n function RegionGenerator() {\n this.obstacleRegionBordersCleaner = new ObstacleRegionBordersCleaner();\n this.floodedCells = new Array(1024);\n this.workingStack = new Array(1024);\n }\n //TODO implement the smoothing pass on the distance field?\n /**\n * Groups cells into cohesive regions using an watershed based algorithm.\n *\n * This operation depends on neighbor and distance field information.\n * So {@link RegionGenerator.generateDistanceField} operations must be\n * run before this operation.\n *\n * @param grid A field with cell distance information fully generated.\n * @param obstacleCellPadding a padding in cells to apply around the\n * obstacles.\n */\n RegionGenerator.prototype.generateRegions = function (grid, obstacleCellPadding) {\n // Watershed Algorithm\n //\n // Reference: http://en.wikipedia.org/wiki/Watershed_%28algorithm%29\n // A good visualization:\n // http://artis.imag.fr/Publications/2003/HDS03/ (PDF)\n //\n // Summary:\n //\n // This algorithm utilizes the cell.distanceToObstacle value, which\n // is generated by the generateDistanceField() operation.\n //\n // Using the watershed analogy, the cells which are furthest from\n // a border (highest distance to border) represent the lowest points\n // in the watershed. A border cell represents the highest possible\n // water level.\n //\n // The main loop iterates, starting at the lowest point in the\n // watershed, then incrementing with each loop until the highest\n // allowed water level is reached. This slowly \"floods\" the cells\n // starting at the lowest points.\n //\n // During each iteration of the loop, cells that are below the\n // current water level are located and an attempt is made to either\n // add them to exiting regions or create new regions from them.\n //\n // During the region expansion phase, if a newly flooded cell\n // borders on an existing region, it is usually added to the region.\n //\n // Any newly flooded cell that survives the region expansion phase\n // is used as a seed for a new region.\n //\n // At the end of the main loop, a final region expansion is\n // performed which should catch any stray cells that escaped region\n // assignment during the main loop.\n // Represents the minimum distance to an obstacle that is considered\n // traversable. I.e. Can't traverse cells closer than this distance\n // to a border. This provides a way of artificially capping the\n // height to which watershed flooding can occur.\n // I.e. Don't let the algorithm flood all the way to the actual border.\n //\n // We add the minimum border distance to take into account the\n // blurring algorithm which can result in a border cell having a\n // border distance > 0.\n var distanceMin = obstacleCellPadding * 2;\n // TODO: EVAL: Figure out why this iteration limit is needed\n // (todo from the CritterAI sources).\n var expandIterations = 4 + distanceMin * 2;\n // Zero is reserved for the obstacle-region. So initializing to 1.\n var nextRegionID = 1;\n var floodedCells = this.floodedCells;\n // Search until the current distance reaches the minimum allowed\n // distance.\n //\n // Note: This loop will not necessarily complete all region\n // assignments. This is OK since a final region assignment step\n // occurs after the loop iteration is complete.\n for (\n // This value represents the current distance from the border which\n // is to be searched. The search starts at the maximum distance then\n // moves toward zero (toward borders).\n //\n // This number will always be divisible by 2.\n var distance = grid.obstacleDistanceMax() & ~1; distance > distanceMin; distance = Math.max(distance - 2, 0)) {\n // Find all cells that are at or below the current \"water level\"\n // and are not already assigned to a region. Add these cells to\n // the flooded cell list for processing.\n floodedCells.length = 0;\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.regionID === RasterizationCell.NULL_REGION_ID &&\n cell.distanceToObstacle >= distance) {\n // The cell is not already assigned a region and is\n // below the current \"water level\". So the cell can be\n // considered for region assignment.\n floodedCells.push(cell);\n }\n }\n }\n if (nextRegionID > 1) {\n // At least one region has already been created, so first\n // try to put the newly flooded cells into existing regions.\n if (distance > 0) {\n this.expandRegions(grid, floodedCells, expandIterations);\n }\n else {\n this.expandRegions(grid, floodedCells, -1);\n }\n }\n // Create new regions for all cells that could not be added to\n // existing regions.\n for (var _i = 0, floodedCells_1 = floodedCells; _i < floodedCells_1.length; _i++) {\n var floodedCell = floodedCells_1[_i];\n if (!floodedCell ||\n floodedCell.regionID !== RasterizationCell.NULL_REGION_ID) {\n // This cell was assigned to a newly created region\n // during an earlier iteration of this loop.\n // So it can be skipped.\n continue;\n }\n // Fill to slightly more than the current \"water level\".\n // This improves efficiency of the algorithm.\n // And it is necessary with the conservative expansion to ensure that\n // more than one cell is added initially to a new regions otherwise\n // no cell could be added to it later because of the conservative\n // constraint.\n var fillTo = Math.max(distance - 2, distanceMin + 1, 1);\n if (this.floodNewRegion(grid, floodedCell, fillTo, nextRegionID)) {\n nextRegionID++;\n }\n }\n }\n // Find all cells that haven't been assigned regions by the main loop\n // (up to the minimum distance).\n floodedCells.length = 0;\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.distanceToObstacle > distanceMin &&\n cell.regionID === RasterizationCell.NULL_REGION_ID) {\n // Not a border or obstacle region cell. Should be in a region.\n floodedCells.push(cell);\n }\n }\n }\n // Perform a final expansion of existing regions.\n // Allow more iterations than normal for this last expansion.\n if (distanceMin > 0) {\n this.expandRegions(grid, floodedCells, expandIterations * 8);\n }\n else {\n this.expandRegions(grid, floodedCells, -1);\n }\n grid.regionCount = nextRegionID;\n this.obstacleRegionBordersCleaner.fixObstacleRegion(grid);\n //TODO Also port FilterOutSmallRegions?\n // The algorithm to remove vertices in the middle (added at the end of\n // ContourBuilder.buildContours) may already filter them and contour are\n // faster to process than cells.\n };\n /**\n * Attempts to find the most appropriate regions to attach cells to.\n *\n * Any cells successfully attached to a region will have their list\n * entry set to null. So any non-null entries in the list will be cells\n * for which a region could not be determined.\n *\n * @param grid\n * @param inoutCells As input, the list of cells available for formation\n * of new regions. As output, the cells that could not be assigned\n * to new regions.\n * @param maxIterations If set to -1, will iterate through completion.\n */\n RegionGenerator.prototype.expandRegions = function (grid, inoutCells, iterationMax) {\n if (inoutCells.length === 0)\n return;\n var skipped = 0;\n for (var iteration = 0; (iteration < iterationMax || iterationMax === -1) &&\n // All cells have either been processed or could not be\n // processed during the last cycle.\n skipped < inoutCells.length; iteration++) {\n // The number of cells in the working list that have been\n // successfully processed or could not be processed successfully\n // for some reason.\n // This value controls when iteration ends.\n skipped = 0;\n for (var index = 0; index < inoutCells.length; index++) {\n var cell = inoutCells[index];\n if (cell === null) {\n // The cell originally at this index location has\n // already been successfully assigned a region. Nothing\n // else to do with it.\n skipped++;\n continue;\n }\n // Default to unassigned.\n var cellRegion = RasterizationCell.NULL_REGION_ID;\n var regionCenterDist = Number.MAX_VALUE;\n for (var _i = 0, _a = RasterizationGrid.neighbor4Deltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.regionID !== RasterizationCell.NULL_REGION_ID) {\n if (neighbor.distanceToRegionCore + 2 < regionCenterDist) {\n // This neighbor is closer to its region core\n // than previously detected neighbors.\n // Conservative expansion constraint:\n // Check to ensure that this neighbor has\n // at least two other neighbors in its region.\n // This makes sure that adding this cell to\n // this neighbor's region will not result\n // in a single width line of cells.\n var sameRegionCount = 0;\n for (var neighborDirection = 0; neighborDirection < 4; neighborDirection++) {\n var nnCell = grid.getNeighbor(neighbor, neighborDirection);\n // There is a diagonal-neighbor\n if (nnCell.regionID === neighbor.regionID) {\n // This neighbor has a neighbor in\n // the same region.\n sameRegionCount++;\n }\n }\n if (sameRegionCount > 1) {\n cellRegion = neighbor.regionID;\n regionCenterDist = neighbor.distanceToRegionCore + 2;\n }\n }\n }\n }\n if (cellRegion !== RasterizationCell.NULL_REGION_ID) {\n // Found a suitable region for this cell to belong to.\n // Mark this index as having been processed.\n inoutCells[index] = null;\n cell.regionID = cellRegion;\n cell.distanceToRegionCore = regionCenterDist;\n }\n else {\n // Could not find an existing region for this cell.\n skipped++;\n }\n }\n }\n };\n /**\n * Creates a new region surrounding a cell, adding neighbor cells to the\n * new region as appropriate.\n *\n * The new region creation will fail if the root cell is on the\n * border of an existing region.\n *\n * All cells added to the new region as part of this process become\n * \"core\" cells with a distance to region core of zero.\n *\n * @param grid\n * @param rootCell The cell used to seed the new region.\n * @param fillToDist The watershed distance to flood to.\n * @param regionID The region ID to use for the new region\n * (if creation is successful).\n * @return true if a new region was created.\n */\n RegionGenerator.prototype.floodNewRegion = function (grid, rootCell, fillToDist, regionID) {\n var workingStack = this.workingStack;\n workingStack.length = 0;\n workingStack.push(rootCell);\n rootCell.regionID = regionID;\n rootCell.distanceToRegionCore = 0;\n var regionSize = 0;\n var cell;\n while ((cell = workingStack.pop())) {\n // Check regions of neighbor cells.\n //\n // If any neighbor is found to have a region assigned, then\n // the current cell can't be in the new region\n // (want standard flooding algorithm to handle deciding which\n // region this cell should go in).\n //\n // Up to 8 neighbors are checked.\n //\n // Neighbor searches:\n // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch\n var isOnRegionBorder = false;\n for (var _i = 0, _a = RasterizationGrid.neighbor8Deltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n isOnRegionBorder =\n neighbor.regionID !== RasterizationCell.NULL_REGION_ID &&\n neighbor.regionID !== regionID;\n if (isOnRegionBorder)\n break;\n }\n if (isOnRegionBorder) {\n cell.regionID = RasterizationCell.NULL_REGION_ID;\n continue;\n }\n regionSize++;\n // If got this far, we know the current cell is part of the new\n // region. Now check its neighbors to see if they should be\n // assigned to this new region.\n for (var _b = 0, _c = RasterizationGrid.neighbor4Deltas; _b < _c.length; _b++) {\n var delta = _c[_b];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.distanceToObstacle >= fillToDist &&\n neighbor.regionID === RasterizationCell.NULL_REGION_ID) {\n neighbor.regionID = regionID;\n neighbor.distanceToRegionCore = 0;\n workingStack.push(neighbor);\n }\n }\n }\n return regionSize > 0;\n };\n /**\n * Generates distance field information.\n * The {@link RasterizationCell.distanceToObstacle} information is generated\n * for all cells in the field.\n *\n * All distance values are relative and do not represent explicit\n * distance values (such as grid unit distance). The algorithm which is\n * used results in an approximation only. It is not exhaustive.\n *\n * The data generated by this operation is required by\n * {@link RegionGenerator.generateRegions}.\n *\n * @param grid A field with cells obstacle information already generated.\n */\n RegionGenerator.prototype.generateDistanceField = function (grid) {\n // close borders\n for (var x = 0; x < grid.dimX(); x++) {\n var leftCell = grid.get(x, 0);\n leftCell.distanceToObstacle = 0;\n var rightCell = grid.get(x, grid.dimY() - 1);\n rightCell.distanceToObstacle = 0;\n }\n for (var y = 1; y < grid.dimY() - 1; y++) {\n var topCell = grid.get(0, y);\n topCell.distanceToObstacle = 0;\n var bottomCell = grid.get(grid.dimX() - 1, y);\n bottomCell.distanceToObstacle = 0;\n }\n // The next two phases basically check the neighbors of a cell and\n // set the cell's distance field to be slightly greater than the\n // neighbor with the lowest border distance. Distance is increased\n // slightly more for diagonal-neighbors than for axis-neighbors.\n // 1st pass\n // During this pass, the following neighbors are checked:\n // (-1, 0) (-1, -1) (0, -1) (1, -1)\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n for (var _i = 0, _a = RegionGenerator.firstPassDeltas; _i < _a.length; _i++) {\n var delta = _a[_i];\n var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +\n delta.distance;\n if (cell.distanceToObstacle > distanceByNeighbor) {\n cell.distanceToObstacle = distanceByNeighbor;\n }\n }\n }\n }\n // 2nd pass\n // During this pass, the following neighbors are checked:\n // (1, 0) (1, 1) (0, 1) (-1, 1)\n //\n // Besides checking different neighbors, this pass performs its\n // grid search in reverse order.\n for (var y = grid.dimY() - 2; y >= 1; y--) {\n for (var x = grid.dimX() - 2; x >= 1; x--) {\n var cell = grid.get(x, y);\n for (var _b = 0, _c = RegionGenerator.secondPassDeltas; _b < _c.length; _b++) {\n var delta = _c[_b];\n var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +\n delta.distance;\n if (cell.distanceToObstacle > distanceByNeighbor) {\n cell.distanceToObstacle = distanceByNeighbor;\n }\n }\n }\n }\n };\n RegionGenerator.firstPassDeltas = [\n { x: -1, y: 0, distance: 2 },\n { x: -1, y: -1, distance: 3 },\n { x: 0, y: -1, distance: 2 },\n { x: 1, y: -1, distance: 3 },\n ];\n RegionGenerator.secondPassDeltas = [\n { x: 1, y: 0, distance: 2 },\n { x: 1, y: 1, distance: 3 },\n { x: 0, y: 1, distance: 2 },\n { x: -1, y: 1, distance: 3 },\n ];\n return RegionGenerator;\n}());\n/**\n * Implements three algorithms that clean up issues that can\n * develop around obstacle region boarders.\n *\n * - Detect and fix encompassed obstacle regions:\n *\n * If a obstacle region is found that is fully encompassed by a single\n * region, then the region will be split into two regions at the\n * obstacle region border.\n *\n * - Detect and fix \"short wrapping\" of obstacle regions:\n *\n * Regions can sometimes wrap slightly around the corner of a obstacle region\n * in a manner that eventually results in the formation of self-intersecting\n * polygons.\n *\n * Example: Before the algorithm is applied:\n * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_08_cornerwrapbefore.jpg\"\n *\n * Example: After the algorithm is applied:\n * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_09_cornerwrapafter.jpg\n *\n * - Detect and fix incomplete obstacle region connections:\n *\n * If a region touches obstacle region only diagonally, then contour detection\n * algorithms may not properly detect the obstacle region connection. This can\n * adversely effect other algorithms in the pipeline.\n *\n * Example: Before algorithm is applied:\n *\n * b b a a a a\n * b b a a a a\n * a a x x x x\n * a a x x x x\n *\n * Example: After algorithm is applied:\n *\n * b b a a a a\n * b b b a a a <-- Cell transferred to region B.\n * a a x x x x\n * a a x x x x\n *\n *\n * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html\n */\nvar ObstacleRegionBordersCleaner = /** @class */ (function () {\n function ObstacleRegionBordersCleaner() {\n this.workingUpLeftOpenCells = new Array(512);\n this.workingDownRightOpenCells = new Array(512);\n this.workingOpenCells = new Array(512);\n }\n /**\n * This operation utilizes {@link RasterizationCell.contourFlags}. It\n * expects the value to be zero on entry, and re-zero's the value\n * on exit.\n *\n * @param grid a grid with fully built regions.\n */\n ObstacleRegionBordersCleaner.prototype.fixObstacleRegion = function (grid) {\n var workingUpLeftOpenCells = this.workingUpLeftOpenCells;\n workingUpLeftOpenCells.length = 0;\n var workingDownRightOpenCells = this.workingDownRightOpenCells;\n workingDownRightOpenCells.length = 0;\n var workingOpenCells = this.workingOpenCells;\n workingOpenCells.length = 0;\n var extremeCells = [\n null,\n null,\n ];\n var nextRegionID = grid.regionCount;\n // Iterate over the cells, trying to find obstacle region borders.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n if (cell.contourFlags !== 0)\n // Cell was processed in a previous iteration.\n // Ignore it.\n continue;\n cell.contourFlags = 1;\n var workingCell = null;\n var edgeDirection = -1;\n if (cell.regionID !== RasterizationCell.OBSTACLE_REGION_ID) {\n // Not interested in this cell.\n continue;\n }\n // This is a obstacle region cell. See if it\n // connects to a cell in a non-obstacle region.\n edgeDirection = this.getNonNullBorderDirection(grid, cell);\n if (edgeDirection === -1)\n // This cell is not a border cell. Ignore it.\n continue;\n // This is a border cell. Step into the non-null\n // region and swing the direction around 180 degrees.\n workingCell = grid.getNeighbor(cell, edgeDirection);\n edgeDirection = (edgeDirection + 2) & 0x3;\n // Process the obstacle region contour. Detect and fix\n // local issues. Determine if the region is\n // fully encompassed by a single non-obstacle region.\n var isEncompassedNullRegion = this.processNullRegion(grid, workingCell, edgeDirection, extremeCells);\n if (isEncompassedNullRegion) {\n // This cell is part of a group of obstacle region cells\n // that is encompassed within a single non-obstacle region.\n // This is not permitted. Need to fix it.\n this.partialFloodRegion(grid, extremeCells[0], extremeCells[1], nextRegionID);\n nextRegionID++;\n }\n }\n }\n grid.regionCount = nextRegionID;\n // Clear all flags.\n for (var y = 1; y < grid.dimY() - 1; y++) {\n for (var x = 1; x < grid.dimX() - 1; x++) {\n var cell = grid.get(x, y);\n cell.contourFlags = 0;\n }\n }\n };\n /**\n * Partially flood a region away from the specified direction.\n *\n * {@link RasterizationCell.contourFlags}\n * is set to zero for all flooded cells.\n *\n * @param grid\n * @param startCell The cell to start the flood from.\n * @param borderDirection The hard border for flooding. No\n * cells in this direction from the startCell will be flooded.\n * @param newRegionID The region id to assign the flooded\n * cells to.\n */\n ObstacleRegionBordersCleaner.prototype.partialFloodRegion = function (grid, upLeftCell, downRightCell, newRegionID) {\n var upLeftOpenCells = this.workingUpLeftOpenCells;\n var downRightOpenCells = this.workingDownRightOpenCells;\n var workingOpenCells = this.workingOpenCells;\n // The implementation differs from CritterAI to avoid non-contiguous\n // sections. Instead of brushing in one direction, it floods from\n // 2 extremities of the encompassed obstacle region.\n var regionID = upLeftCell.regionID;\n if (regionID === newRegionID) {\n // avoid infinity loop\n console.error(\"Can't create a new region with an ID that already exist.\");\n return;\n }\n // The 1st flooding set a new the regionID\n upLeftCell.regionID = newRegionID;\n upLeftCell.distanceToRegionCore = 0; // This information is lost.\n upLeftOpenCells.length = 0;\n upLeftOpenCells.push(upLeftCell);\n // The 2nd flooding keep the regionID and mark the cell as visited.\n downRightCell.contourFlags = 2;\n downRightCell.distanceToRegionCore = 0; // This information is lost.\n downRightOpenCells.length = 0;\n downRightOpenCells.push(downRightCell);\n var swap;\n workingOpenCells.length = 0;\n while (upLeftOpenCells.length !== 0 || downRightOpenCells.length !== 0) {\n for (var _i = 0, upLeftOpenCells_1 = upLeftOpenCells; _i < upLeftOpenCells_1.length; _i++) {\n var cell = upLeftOpenCells_1[_i];\n for (var direction = 0; direction < 4; direction++) {\n var neighbor = grid.getNeighbor(cell, direction);\n if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {\n continue;\n }\n // Transfer the neighbor to the new region.\n neighbor.regionID = newRegionID;\n neighbor.distanceToRegionCore = 0; // This information is lost.\n workingOpenCells.push(neighbor);\n }\n }\n // This allows to flood the nearest cells first without needing lifo queue.\n // But a queue would take less memory.\n swap = upLeftOpenCells;\n upLeftOpenCells = workingOpenCells;\n workingOpenCells = swap;\n workingOpenCells.length = 0;\n for (var _a = 0, downRightOpenCells_1 = downRightOpenCells; _a < downRightOpenCells_1.length; _a++) {\n var cell = downRightOpenCells_1[_a];\n for (var direction = 0; direction < 4; direction++) {\n var neighbor = grid.getNeighbor(cell, direction);\n if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {\n continue;\n }\n // Keep the neighbor to the current region.\n neighbor.contourFlags = 2;\n neighbor.distanceToRegionCore = 0; // This information is lost.\n workingOpenCells.push(neighbor);\n }\n }\n swap = downRightOpenCells;\n downRightOpenCells = workingOpenCells;\n workingOpenCells = swap;\n workingOpenCells.length = 0;\n }\n };\n /**\n * Detects and fixes bad cell configurations in the vicinity of a\n * obstacle region contour (See class description for details).\n * @param grid\n * @param startCell A cell in a non-obstacle region that borders a null\n * region.\n * @param startDirection The direction of the obstacle region border.\n * @return TRUE if the start cell's region completely encompasses\n * the obstacle region.\n */\n ObstacleRegionBordersCleaner.prototype.processNullRegion = function (grid, startCell, startDirection, extremeCells) {\n // This algorithm traverses the contour. As it does so, it detects\n // and fixes various known dangerous cell configurations.\n //\n // Traversing the contour: A good way to visualize it is to think\n // of a robot sitting on the floor facing a known wall. It then\n // does the following to skirt the wall:\n // 1. If there is a wall in front of it, turn clockwise in 90 degrees\n // increments until it finds the wall is gone.\n // 2. Move forward one step.\n // 3. Turn counter-clockwise by 90 degrees.\n // 4. Repeat from step 1 until it finds itself at its original\n // location facing its original direction.\n //\n // See also: http://www.critterai.org/projects/nmgen_study/regiongen.html#robotwalk\n //\n // As the traversal occurs, the number of acute (90 degree) and\n // obtuse (270 degree) corners are monitored. If a complete contour is\n // detected and (obtuse corners > acute corners), then the null\n // region is inside the contour. Otherwise the obstacle region is\n // outside the contour, which we don't care about.\n var borderRegionID = startCell.regionID;\n // Prepare for loop.\n var cell = startCell;\n var neighbor = null;\n var direction = startDirection;\n var upLeftCell = cell;\n var downRightCell = cell;\n // Initialize monitoring variables.\n var loopCount = 0;\n var acuteCornerCount = 0;\n var obtuseCornerCount = 0;\n var stepsWithoutBorder = 0;\n var borderSeenLastLoop = false;\n var isBorder = true; // Initial value doesn't matter.\n // Assume a single region is connected to the obstacle region\n // until proven otherwise.\n var hasSingleConnection = true;\n // The loop limit exists for the sole reason of preventing\n // an infinite loop in case of bad input data.\n // It is set to a very high value because there is no way of\n // definitively determining a safe smaller value. Setting\n // the value too low can result in rescanning a contour\n // multiple times, killing performance.\n while (++loopCount < 1 << 30) {\n // Get the cell across the border.\n neighbor = grid.getNeighbor(cell, direction);\n // Detect which type of edge this direction points across.\n if (neighbor === null) {\n // It points across a obstacle region border edge.\n isBorder = true;\n }\n else {\n // We never need to perform contour detection\n // on this cell again. So mark it as processed.\n neighbor.contourFlags = 1;\n if (neighbor.regionID === RasterizationCell.OBSTACLE_REGION_ID) {\n // It points across a obstacle region border edge.\n isBorder = true;\n }\n else {\n // This isn't a obstacle region border.\n isBorder = false;\n if (neighbor.regionID !== borderRegionID)\n // It points across a border to a non-obstacle region.\n // This means the current contour can't\n // represent a fully encompassed obstacle region.\n hasSingleConnection = false;\n }\n }\n // Process the border.\n if (isBorder) {\n // It is a border edge.\n if (borderSeenLastLoop) {\n // A border was detected during the last loop as well.\n // Two detections in a row indicates we passed an acute\n // (inner) corner.\n //\n // a x\n // x x\n acuteCornerCount++;\n }\n else if (stepsWithoutBorder > 1) {\n // We have moved at least two cells before detecting\n // a border. This indicates we passed an obtuse\n // (outer) corner.\n //\n // a a\n // a x\n obtuseCornerCount++;\n stepsWithoutBorder = 0;\n // Detect and fix cell configuration issue around this\n // corner.\n if (this.processOuterCorner(grid, cell, direction))\n // A change was made and it resulted in the\n // corner area having multiple region connections.\n hasSingleConnection = false;\n }\n direction = (direction + 1) & 0x3; // Rotate in clockwise direction.\n borderSeenLastLoop = true;\n stepsWithoutBorder = 0;\n }\n else {\n // Not a obstacle region border.\n // Move to the neighbor and swing the search direction back\n // one increment (counterclockwise). By moving the direction\n // back one increment we guarantee we don't miss any edges.\n cell = neighbor;\n direction = (direction + 3) & 0x3; // Rotate counterclockwise direction.\n borderSeenLastLoop = false;\n stepsWithoutBorder++;\n if (cell.x < upLeftCell.x ||\n (cell.x === upLeftCell.x && cell.y < upLeftCell.y)) {\n upLeftCell = cell;\n }\n if (cell.x > downRightCell.x ||\n (cell.x === downRightCell.x && cell.y > downRightCell.y)) {\n downRightCell = cell;\n }\n }\n if (startCell === cell && startDirection === direction) {\n extremeCells[0] = upLeftCell;\n extremeCells[1] = downRightCell;\n // Have returned to the original cell and direction.\n // The search is complete.\n // Is the obstacle region inside the contour?\n return hasSingleConnection && obtuseCornerCount > acuteCornerCount;\n }\n }\n // If got here then the obstacle region boarder is too large to be fully\n // explored. So it can't be encompassed.\n return false;\n };\n /**\n * Detects and fixes cell configuration issues in the vicinity\n * of obtuse (outer) obstacle region corners.\n * @param grid\n * @param referenceCell The cell in a non-obstacle region that is\n * just past the outer corner.\n * @param borderDirection The direction of the obstacle region border.\n * @return TRUE if more than one region connects to the obstacle region\n * in the vicinity of the corner (this may or may not be due to\n * a change made by this operation).\n */\n ObstacleRegionBordersCleaner.prototype.processOuterCorner = function (grid, referenceCell, borderDirection) {\n var hasMultiRegions = false;\n // Get the previous two cells along the border.\n var backOne = grid.getNeighbor(referenceCell, (borderDirection + 3) & 0x3);\n var backTwo = grid.getNeighbor(backOne, borderDirection);\n var testCell;\n if (backOne.regionID !== referenceCell.regionID &&\n // This differ from the CritterAI implementation.\n // To filter vertices in the middle, this must be avoided too:\n // a x\n // b c\n backTwo.regionID !== backOne.regionID) {\n // Dangerous corner configuration.\n //\n // a x\n // b a\n //\n // Need to change to one of the following configurations:\n //\n // b x a x\n // b a b b\n //\n // Reason: During contour detection this type of configuration can\n // result in the region connection being detected as a\n // region-region portal, when it is not. The region connection\n // is actually interrupted by the obstacle region.\n //\n // This configuration has been demonstrated to result in\n // two regions being improperly merged to encompass an\n // internal obstacle region.\n //\n // Example:\n //\n // a a x x x a\n // a a x x a a\n // b b a a a a\n // b b a a a a\n //\n // During contour and connection detection for region b, at no\n // point will the obstacle region be detected. It will appear\n // as if a clean a-b portal exists.\n //\n // An investigation into fixing this issue via updates to the\n // watershed or contour detection algorithms did not turn\n // up a better way of resolving this issue.\n hasMultiRegions = true;\n // Determine how many connections backTwo has to backOne's region.\n testCell = grid.getNeighbor(backOne, (borderDirection + 3) & 0x3);\n var backTwoConnections = 0;\n if (testCell.regionID === backOne.regionID) {\n backTwoConnections++;\n testCell = grid.getNeighbor(testCell, borderDirection);\n if (testCell.regionID === backOne.regionID)\n backTwoConnections++;\n }\n // Determine how many connections the reference cell has\n // to backOne's region.\n var referenceConnections = 0;\n testCell = grid.getNeighbor(backOne, (borderDirection + 2) & 0x3);\n if (testCell.regionID === backOne.regionID) {\n referenceConnections++;\n testCell = grid.getNeighbor(testCell, (borderDirection + 2) & 0x3);\n if (testCell.regionID === backOne.regionID)\n backTwoConnections++;\n }\n // Change the region of the cell that has the most connections\n // to the target region.\n if (referenceConnections > backTwoConnections)\n referenceCell.regionID = backOne.regionID;\n else\n backTwo.regionID = backOne.regionID;\n }\n else if (backOne.regionID === referenceCell.regionID &&\n backTwo.regionID === referenceCell.regionID) {\n // Potential dangerous short wrap.\n //\n // a x\n // a a\n //\n // Example of actual problem configuration:\n //\n // b b x x\n // b a x x <- Short wrap.\n // b a a a\n //\n // In the above case, the short wrap around the corner of the\n // obstacle region has been demonstrated to cause self-intersecting\n // polygons during polygon formation.\n //\n // This algorithm detects whether or not one (and only one)\n // of the axis neighbors of the corner should be re-assigned to\n // a more appropriate region.\n //\n // In the above example, the following configuration is more\n // appropriate:\n //\n // b b x x\n // b b x x <- Change to this row.\n // b a a a\n // Check to see if backTwo should be in a different region.\n var selectedRegion = this.selectedRegionID(grid, backTwo, (borderDirection + 1) & 0x3, (borderDirection + 2) & 0x3);\n if (selectedRegion === backTwo.regionID) {\n // backTwo should not be re-assigned. How about\n // the reference cell?\n selectedRegion = this.selectedRegionID(grid, referenceCell, borderDirection, (borderDirection + 3) & 0x3);\n if (selectedRegion !== referenceCell.regionID) {\n // The reference cell should be reassigned\n // to a new region.\n referenceCell.regionID = selectedRegion;\n hasMultiRegions = true;\n }\n }\n else {\n // backTwo should be re-assigned to a new region.\n backTwo.regionID = selectedRegion;\n hasMultiRegions = true;\n }\n }\n else\n hasMultiRegions = true;\n // No dangerous configurations detected. But definitely\n // has a change in regions at the corner. We know this\n // because one of the previous checks looked for a single\n // region for all wrap cells.\n return hasMultiRegions;\n };\n /**\n * Checks the cell to see if it should be reassigned to a new region.\n *\n * @param grid\n * @param referenceCell A cell on one side of an obstacle region contour's\n * outer corner. It is expected that the all cells that wrap the\n * corner are in the same region.\n * @param borderDirection The direction of the obstacle region border.\n * @param cornerDirection The direction of the outer corner from the\n * reference cell.\n * @return The region the cell should be a member of. May be the\n * region the cell is currently a member of.\n */\n ObstacleRegionBordersCleaner.prototype.selectedRegionID = function (grid, referenceCell, borderDirection, cornerDirection) {\n // Initial example state:\n //\n // a - Known region.\n // x - Null region.\n // u - Unknown, not checked yet.\n //\n // u u u\n // u a x\n // u a a\n // The only possible alternate region id is from\n // the cell that is opposite the border. So check it first.\n var regionID = grid.getNeighbor(referenceCell, (borderDirection + 2) & 0x3)\n .regionID;\n if (regionID === referenceCell.regionID ||\n regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // The region away from the border is either a obstacle region\n // or the same region. So we keep the current region.\n //\n // u u u u u u\n // a a x or x a x <-- Potentially bad, but stuck with it.\n // u a a u a a\n return referenceCell.regionID;\n // Candidate region for re-assignment.\n var potentialRegion = regionID;\n // Next we check the region opposite from the corner direction.\n // If it is the current region, then we definitely can't\n // change the region id without risk of splitting the region.\n regionID = grid.getNeighbor(referenceCell, (cornerDirection + 2) & 0x3)\n .regionID;\n if (regionID === referenceCell.regionID ||\n regionID === RasterizationCell.OBSTACLE_REGION_ID)\n // The region opposite from the corner direction is\n // either a obstacle region or the same region. So we\n // keep the current region.\n //\n // u a u u x u\n // b a x or b a x\n // u a a u a a\n return referenceCell.regionID;\n // We have checked the early exit special cases. Now a generalized\n // brute count is performed.\n //\n // Priority is given to the potential region. Here is why:\n // (Highly unlikely worst case scenario)\n //\n // c c c c c c\n // b a x -> b b x Select b even though b count == a count.\n // b a a b a a\n // Neighbors in potential region.\n // We know this will have a minimum value of 1.\n var potentialCount = 0;\n // Neighbors in the cell's current region.\n // We know this will have a minimum value of 2.\n var currentCount = 0;\n // Maximum edge case:\n //\n // b b b\n // b a x\n // b a a\n //\n // The maximum edge case for region A can't exist. It\n // is filtered out during one of the earlier special cases\n // handlers.\n //\n // Other cases may exist if more regions are involved.\n // Such cases will tend to favor the current region.\n for (var direction = 0; direction < 8; direction++) {\n var regionID_1 = grid.getNeighbor(referenceCell, direction).regionID;\n if (regionID_1 === referenceCell.regionID)\n currentCount++;\n else if (regionID_1 === potentialRegion)\n potentialCount++;\n }\n return potentialCount < currentCount\n ? referenceCell.regionID\n : potentialRegion;\n };\n /**\n * Returns the direction of the first neighbor in a non-obstacle region.\n * @param grid\n * @param cell The cell to check.\n * @return The direction of the first neighbor in a non-obstacle region, or\n * -1 if all neighbors are in the obstacle region.\n */\n ObstacleRegionBordersCleaner.prototype.getNonNullBorderDirection = function (grid, cell) {\n // Search axis-neighbors.\n for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {\n var delta = RasterizationGrid.neighbor4Deltas[direction];\n var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);\n if (neighbor.regionID !== RasterizationCell.OBSTACLE_REGION_ID)\n // The neighbor is a obstacle region.\n return direction;\n }\n // All neighbors are in a non-obstacle region.\n return -1;\n };\n return ObstacleRegionBordersCleaner;\n}());\n\n// This implementation is strongly inspired from a Java one\n// by Stephen A. Pratt:\n// http://www.critterai.org/projects/nmgen_study/\n//\n// Most of the comments were written by him and were adapted to fit this implementation.\n// This implementation differs a bit from the original:\n// - it's only 2D instead of 3D\n// - it has less features (see TODO) and might have lesser performance\n// - it uses objects for points instead of pointer-like in arrays of numbers\n// - the rasterization comes from other sources because of the 2d focus\n// - partialFloodRegion was rewritten to fix an issue\n// - filterNonObstacleVertices was added\n//\n// The Java implementation was also inspired from Recast that can be found here:\n// https://github.com/recastnavigation/recastnavigation\nvar NavMeshGenerator = /** @class */ (function () {\n function NavMeshGenerator(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, isometricRatio) {\n if (isometricRatio === void 0) { isometricRatio = 1; }\n this.grid = new RasterizationGrid(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, \n // make cells square in the world\n rasterizationCellSize / isometricRatio);\n this.isometricRatio = isometricRatio;\n this.obstacleRasterizer = new ObstacleRasterizer();\n this.regionGenerator = new RegionGenerator();\n this.contourBuilder = new ContourBuilder();\n this.convexPolygonGenerator = new ConvexPolygonGenerator();\n this.gridCoordinateConverter = new GridCoordinateConverter();\n }\n NavMeshGenerator.prototype.buildNavMesh = function (obstacles, obstacleCellPadding) {\n var _this = this;\n this.grid.clear();\n this.obstacleRasterizer.rasterizeObstacles(this.grid, obstacles);\n this.regionGenerator.generateDistanceField(this.grid);\n this.regionGenerator.generateRegions(this.grid, obstacleCellPadding);\n // It's probably not a good idea to expose the vectorization threshold.\n // As stated in the parameter documentation, the value 1 gives good\n // results in any situations.\n var threshold = 1;\n var contours = this.contourBuilder.buildContours(this.grid, threshold);\n var meshField = this.convexPolygonGenerator.splitToConvexPolygons(contours, 16);\n var scaledMeshField = this.gridCoordinateConverter.convertFromGridBasis(this.grid, meshField);\n if (this.isometricRatio != 1) {\n // Rescale the mesh to have the same unit length on the 2 axis for the pathfinding.\n scaledMeshField.forEach(function (polygon) {\n return polygon.forEach(function (point) {\n point.y *= _this.isometricRatio;\n });\n });\n }\n return scaledMeshField;\n };\n return NavMeshGenerator;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * PathfindingObstaclesManager manages the common objects shared by objects having a\n * pathfinding behavior: In particular, the obstacles behaviors are required to declare\n * themselves (see `PathfindingObstaclesManager.addObstacle`) to the manager of their associated scene\n * (see `gdjs.NavMeshPathfindingRuntimeBehavior.obstaclesManagers`).\n */\nvar NavMeshPathfindingObstaclesManager = /** @class */ (function () {\n function NavMeshPathfindingObstaclesManager(instanceContainer, configuration) {\n /**\n * The navigation meshes by moving object size\n * (rounded on _cellSize)\n */\n this._navMeshes = new Map();\n /**\n * Used while NavMeshes update is disabled to remember to do the update\n * when it's enable back.\n */\n this._navMeshesAreUpToDate = true;\n /**\n * This allows to continue finding paths with the old NavMeshes while\n * moving obstacles.\n */\n this._navMeshesUpdateIsEnabled = true;\n var viewpoint = configuration._getViewpoint();\n if (viewpoint === 'Isometry 2:1 (26.565°)') {\n configuration._setIsometricRatio(2);\n }\n else if (viewpoint === 'True Isometry (30°)') {\n configuration._setIsometricRatio(Math.sqrt(3));\n }\n else {\n configuration._setIsometricRatio(1);\n }\n if (configuration._getCellSize() <= 0) {\n configuration._setCellSize(10);\n }\n if (configuration._getAreaLeftBound() === 0 &&\n configuration._getAreaTopBound() === 0 &&\n configuration._getAreaRightBound() === 0 &&\n configuration._getAreaBottomBound() === 0) {\n var game = instanceContainer.getGame();\n configuration._setAreaLeftBound(0);\n configuration._setAreaTopBound(0);\n configuration._setAreaRightBound(game.getGameResolutionWidth());\n configuration._setAreaBottomBound(game.getGameResolutionHeight());\n }\n this.configuration = configuration;\n this._obstacles = new Set();\n this._polygonIterableAdapter = new PolygonIterableAdapter();\n this._navMeshGenerator = new NavMeshGenerator(configuration._getAreaLeftBound(), configuration._getAreaTopBound(), configuration._getAreaRightBound(), configuration._getAreaBottomBound(), configuration._getCellSize(), \n // make cells square in the world\n configuration._getIsometricRatio());\n }\n /**\n * Get the obstacles manager of a scene.\n */\n NavMeshPathfindingObstaclesManager.getManager = function (instanceContainer) {\n // @ts-ignore\n return instanceContainer.navMeshPathfindingObstaclesManager;\n };\n NavMeshPathfindingObstaclesManager.getManagerOrCreate = function (instanceContainer, configuration) {\n // @ts-ignore\n if (!instanceContainer.navMeshPathfindingObstaclesManager) {\n // Create the shared manager if necessary.\n // @ts-ignore\n instanceContainer.navMeshPathfindingObstaclesManager = new NavMeshPathfindingObstaclesManager(instanceContainer, configuration);\n }\n // @ts-ignore\n return instanceContainer.navMeshPathfindingObstaclesManager;\n };\n NavMeshPathfindingObstaclesManager.prototype.setNavMeshesUpdateEnabled = function (navMeshesUpdateIsEnabled) {\n this._navMeshesUpdateIsEnabled = navMeshesUpdateIsEnabled;\n if (navMeshesUpdateIsEnabled && !this._navMeshesAreUpToDate) {\n this._navMeshes.clear();\n this._navMeshesAreUpToDate = true;\n }\n };\n /**\n * Add a obstacle to the list of existing obstacles.\n */\n NavMeshPathfindingObstaclesManager.prototype.addObstacle = function (pathfindingObstacleBehavior) {\n this._obstacles.add(pathfindingObstacleBehavior.behavior.owner);\n this.invalidateNavMesh();\n };\n /**\n * Remove a obstacle from the list of existing obstacles. Be sure that the obstacle was\n * added before.\n */\n NavMeshPathfindingObstaclesManager.prototype.removeObstacle = function (pathfindingObstacleBehavior) {\n this._obstacles.delete(pathfindingObstacleBehavior.behavior.owner);\n this.invalidateNavMesh();\n };\n NavMeshPathfindingObstaclesManager.prototype.invalidateNavMesh = function () {\n if (this._navMeshesUpdateIsEnabled) {\n this._navMeshes.clear();\n this._navMeshesAreUpToDate = true;\n }\n else {\n this._navMeshesAreUpToDate = false;\n }\n };\n NavMeshPathfindingObstaclesManager.prototype.getNavMesh = function (obstacleCellPadding) {\n var navMesh = this._navMeshes.get(obstacleCellPadding);\n if (!navMesh) {\n var navMeshPolygons = this._navMeshGenerator.buildNavMesh(this._getVerticesIterable(this._obstacles), obstacleCellPadding);\n navMesh = new NavMesh(navMeshPolygons);\n this._navMeshes.set(obstacleCellPadding, navMesh);\n }\n return navMesh;\n };\n NavMeshPathfindingObstaclesManager.prototype._getVerticesIterable = function (objects) {\n this._polygonIterableAdapter.set(objects);\n return this._polygonIterableAdapter;\n };\n return NavMeshPathfindingObstaclesManager;\n}());\n/**\n * Iterable that adapts `RuntimeObject` to `Iterable<{x: float y: float}>`.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar PolygonIterableAdapter = /** @class */ (function () {\n function PolygonIterableAdapter() {\n this.objects = [];\n this.objectsItr = this.objects[Symbol.iterator]();\n this.polygonsItr = [][Symbol.iterator]();\n this.pointIterableAdapter = new PointIterableAdapter();\n this.result = {\n value: this.pointIterableAdapter,\n done: false,\n };\n }\n PolygonIterableAdapter.prototype.set = function (objects) {\n this.objects = objects;\n };\n PolygonIterableAdapter.prototype[Symbol.iterator] = function () {\n this.objectsItr = this.objects[Symbol.iterator]();\n this.polygonsItr = [][Symbol.iterator]();\n return this;\n };\n PolygonIterableAdapter.prototype.next = function () {\n var polygonNext = this.polygonsItr.next();\n while (polygonNext.done) {\n var objectNext = this.objectsItr.next();\n if (objectNext.done) {\n // IteratorReturnResult require a defined value\n // even though the spec state otherwise.\n // So, this class can't be typed as an iterable.\n this.result.value = undefined;\n this.result.done = true;\n return this.result;\n }\n this.polygonsItr = objectNext.value.getHitBoxes().values();\n polygonNext = this.polygonsItr.next();\n }\n this.pointIterableAdapter.set(polygonNext.value.vertices);\n this.result.value = this.pointIterableAdapter;\n this.result.done = false;\n return this.result;\n };\n return PolygonIterableAdapter;\n}());\n/**\n * Iterable that adapts coordinates from `[int, int]` to `{x: int, y: int}`.\n *\n * This is an allocation free iterable\n * that can only do one iteration at a time.\n */\nvar PointIterableAdapter = /** @class */ (function () {\n function PointIterableAdapter() {\n this.vertices = [];\n this.verticesItr = this.vertices[Symbol.iterator]();\n this.result = {\n value: { x: 0, y: 0 },\n done: false,\n };\n }\n PointIterableAdapter.prototype.set = function (vertices) {\n this.vertices = vertices;\n };\n PointIterableAdapter.prototype[Symbol.iterator] = function () {\n this.verticesItr = this.vertices[Symbol.iterator]();\n return this;\n };\n PointIterableAdapter.prototype.next = function () {\n var next = this.verticesItr.next();\n if (next.done) {\n return next;\n }\n this.result.value.x = next.value[0];\n this.result.value.y = next.value[1];\n return this.result;\n };\n return PointIterableAdapter;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar NavMeshRenderer = /** @class */ (function () {\n function NavMeshRenderer() {\n /** Used to draw traces for debugging */\n this._lastUsedObstacleCellPadding = null;\n }\n NavMeshRenderer.prototype.setLastUsedObstacleCellPadding = function (lastUsedObstacleCellPadding) {\n this._lastUsedObstacleCellPadding = lastUsedObstacleCellPadding;\n };\n NavMeshRenderer.prototype.render = function (instanceContainer, shapePainter) {\n if (this._lastUsedObstacleCellPadding === null) {\n return;\n }\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n // TODO find a way to rebuild drawing only when necessary.\n // Draw the navigation mesh on a shape painter object for debugging purpose\n var navMesh = manager.getNavMesh(this._lastUsedObstacleCellPadding);\n for (var _i = 0, _a = navMesh.getPolygons(); _i < _a.length; _i++) {\n var navPoly = _a[_i];\n var polygon = navPoly.getPoints();\n if (polygon.length === 0)\n continue;\n for (var index = 1; index < polygon.length; index++) {\n // It helps to spot vertices with 180° between edges.\n shapePainter.drawCircle(polygon[index].x, polygon[index].y / isometricRatio, 3);\n }\n }\n for (var _b = 0, _c = navMesh.getPolygons(); _b < _c.length; _b++) {\n var navPoly = _c[_b];\n var polygon = navPoly.getPoints();\n if (polygon.length === 0)\n continue;\n shapePainter.beginFillPath(polygon[0].x, polygon[0].y / isometricRatio);\n for (var index = 1; index < polygon.length; index++) {\n shapePainter.drawPathLineTo(polygon[index].x, polygon[index].y / isometricRatio);\n }\n shapePainter.closePath();\n shapePainter.endFillPath();\n }\n };\n return NavMeshRenderer;\n}());\n\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar PathFollower = /** @class */ (function () {\n function PathFollower(configuration) {\n // Attributes used for traveling on the path:\n this._path = [];\n this._speed = 0;\n this._distanceOnSegment = 0;\n this._totalSegmentDistance = 0;\n this._currentSegment = 0;\n this._movementAngle = 0;\n this.configuration = configuration;\n }\n PathFollower.prototype.setSpeed = function (speed) {\n this._speed = speed;\n };\n PathFollower.prototype.getSpeed = function () {\n return this._speed;\n };\n PathFollower.prototype.getMovementAngle = function () {\n return this._movementAngle;\n };\n PathFollower.prototype.movementAngleIsAround = function (degreeAngle, tolerance) {\n return (Math.abs(gdjs.evtTools.common.angleDifference(this._movementAngle, degreeAngle)) <= tolerance);\n };\n PathFollower.prototype.getNodeX = function (index) {\n if (index < this._path.length) {\n return this._path[index][0];\n }\n return 0;\n };\n PathFollower.prototype.getNodeY = function (index) {\n if (index < this._path.length) {\n return this._path[index][1];\n }\n return 0;\n };\n PathFollower.prototype.getNextNodeIndex = function () {\n return Math.min(this._currentSegment + 1, this._path.length - 1);\n };\n PathFollower.prototype.getNodeCount = function () {\n return this._path.length;\n };\n PathFollower.prototype.getNextNodeX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);\n return this._path[nextIndex][0];\n };\n PathFollower.prototype.getNextNodeY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);\n return this._path[nextIndex][1];\n };\n PathFollower.prototype.getPreviousNodeX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var previousIndex = Math.min(this._currentSegment, this._path.length - 1);\n return this._path[previousIndex][0];\n };\n PathFollower.prototype.getPreviousNodeY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n var previousIndex = Math.min(this._currentSegment, this._path.length - 1);\n return this._path[previousIndex][1];\n };\n PathFollower.prototype.getDestinationX = function () {\n if (this._path.length === 0) {\n return 0;\n }\n return this._path[this._path.length - 1][0];\n };\n PathFollower.prototype.getDestinationY = function () {\n if (this._path.length === 0) {\n return 0;\n }\n return (this._path[this._path.length - 1][1]);\n };\n /**\n * Return true if the object reached its destination.\n */\n PathFollower.prototype.destinationReached = function () {\n return this._currentSegment >= this._path.length - 1;\n };\n /**\n * Compute and move on the path to the specified destination.\n */\n PathFollower.prototype.setPath = function (path) {\n this._path = path;\n this._enterSegment(0);\n };\n PathFollower.prototype._enterSegment = function (segmentNumber) {\n if (this._path.length === 0) {\n return;\n }\n this._currentSegment = segmentNumber;\n if (this._currentSegment < this._path.length - 1) {\n var pathX = this._path[this._currentSegment + 1][0] -\n this._path[this._currentSegment][0];\n var pathY = this._path[this._currentSegment + 1][1] -\n this._path[this._currentSegment][1];\n this._totalSegmentDistance = Math.sqrt(pathX * pathX + pathY * pathY);\n this._distanceOnSegment = 0;\n this._movementAngle =\n (gdjs.toDegrees(Math.atan2(pathY, pathX)) + 360) % 360;\n }\n else {\n this._speed = 0;\n }\n };\n PathFollower.prototype.isMoving = function () {\n return !(this._path.length === 0 || this.destinationReached());\n };\n PathFollower.prototype.step = function (timeDelta) {\n if (this._path.length === 0 || this.destinationReached()) {\n return;\n }\n // Update the speed of the object\n var previousSpeed = this._speed;\n var maxSpeed = this.configuration._getMaxSpeed();\n if (this._speed !== maxSpeed) {\n this._speed += this.configuration._getAcceleration() * timeDelta;\n if (this._speed > maxSpeed) {\n this._speed = maxSpeed;\n }\n }\n // Update the time on the segment and change segment if needed\n // Use a Verlet integration to be frame rate independent.\n this._distanceOnSegment +=\n ((this._speed + previousSpeed) / 2) * timeDelta;\n var remainingDistanceOnSegment = this._totalSegmentDistance - this._distanceOnSegment;\n if (remainingDistanceOnSegment <= 0 &&\n this._currentSegment < this._path.length) {\n this._enterSegment(this._currentSegment + 1);\n this._distanceOnSegment = -remainingDistanceOnSegment;\n }\n };\n PathFollower.prototype.getX = function () {\n return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][0], this._path[this._currentSegment + 1][0], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][0];\n };\n PathFollower.prototype.getY = function () {\n return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][1], this._path[this._currentSegment + 1][1], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][1];\n };\n return PathFollower;\n}());\n\n/**\n * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to\n * follow a path computed to avoid obstacles.\n */\nvar NavMeshPathfindingBehavior = /** @class */ (function () {\n function NavMeshPathfindingBehavior(behavior) {\n // Attributes used for traveling on the path:\n this._pathFound = false;\n this.behavior = behavior;\n this.pathFollower = new PathFollower(behavior);\n this.navMeshRenderer = new NavMeshRenderer();\n }\n /**\n * Return true if the latest call to moveTo succeeded.\n */\n NavMeshPathfindingBehavior.prototype.pathFound = function () {\n return this._pathFound;\n };\n /**\n * Compute and move on the path to the specified destination.\n */\n NavMeshPathfindingBehavior.prototype.moveTo = function (instanceContainer, x, y) {\n var owner = this.behavior.owner;\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n this._pathFound = true;\n this.pathFollower.setPath([[owner.getX(), owner.getY()], [x, y]]);\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n var cellSize = manager.configuration._getCellSize();\n var collisionShape = this.behavior._getCollisionShape();\n var extraBorder = this.behavior._getExtraBorder();\n var radiusSqMax = 0;\n if (collisionShape !== 'Dot at center') {\n var centerX = owner.getCenterXInScene();\n var centerY = owner.getCenterYInScene();\n for (var _i = 0, _a = owner.getHitBoxes(); _i < _a.length; _i++) {\n var hitBox = _a[_i];\n for (var _b = 0, _c = hitBox.vertices; _b < _c.length; _b++) {\n var vertex = _c[_b];\n var deltaX = vertex[0] - centerX;\n // to have the same unit on x and y\n var deltaY = (vertex[1] - centerY) * isometricRatio;\n var radiusSq = deltaX * deltaX + deltaY * deltaY;\n radiusSqMax = Math.max(radiusSq, radiusSqMax);\n }\n }\n }\n // Round to avoid to flicker between 2 NavMesh\n // because of trigonometry rounding errors.\n // Round the padding on cellSize to avoid almost identical NavMesh\n var obstacleCellPadding = Math.max(0, Math.round((Math.sqrt(radiusSqMax) + extraBorder) / cellSize));\n this.navMeshRenderer.setLastUsedObstacleCellPadding(obstacleCellPadding);\n var navMesh = manager.getNavMesh(obstacleCellPadding);\n // TODO avoid the path allocation\n var path = navMesh.findPath({\n x: owner.getX(),\n y: owner.getY() * isometricRatio,\n }, { x: x, y: y * isometricRatio }) || [];\n this._pathFound = path.length > 0;\n this.pathFollower.setPath(path.map(function (_a) {\n var x = _a.x, y = _a.y;\n return [x, y];\n }));\n };\n NavMeshPathfindingBehavior.prototype.doStepPreEvents = function (instanceContainer) {\n if (this.pathFollower.destinationReached()) {\n return;\n }\n var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);\n if (!manager) {\n return;\n }\n var isometricRatio = manager.configuration._getIsometricRatio();\n var owner = this.behavior.owner;\n var angleOffset = this.behavior._getAngleOffset();\n var angularMaxSpeed = this.behavior._getMaxSpeed();\n var rotateObject = this.behavior._getRotateObject();\n var timeDelta = owner.getElapsedTime(instanceContainer) / 1000;\n this.pathFollower.step(timeDelta);\n // Position object on the segment and update its angle\n var movementAngle = this.pathFollower.getMovementAngle();\n if (rotateObject &&\n owner.getAngle() !== movementAngle + angleOffset) {\n owner.rotateTowardAngle(movementAngle + angleOffset, angularMaxSpeed, instanceContainer);\n }\n owner.setX(this.pathFollower.getX());\n // In case of isometry, convert coords back in screen.\n owner.setY(this.pathFollower.getY() / isometricRatio);\n };\n return NavMeshPathfindingBehavior;\n}());\n\n/*\nGDevelop - NavMesh Pathfinding Behavior Extension\n */\n/**\n * NavMeshPathfindingObstacleRuntimeBehavior represents a behavior allowing objects to be\n * considered as a obstacle by objects having Pathfinding Behavior.\n */\nvar NavMeshPathfindingObstacleBehavior = /** @class */ (function () {\n function NavMeshPathfindingObstacleBehavior(instanceContainer, behavior) {\n this._oldX = 0;\n this._oldY = 0;\n this._oldWidth = 0;\n this._oldHeight = 0;\n this._registeredInManager = false;\n this.behavior = behavior;\n this._manager = NavMeshPathfindingObstaclesManager.getManagerOrCreate(instanceContainer, \n // @ts-ignore\n behavior._sharedData);\n //Note that we can't use getX(), getWidth()... of owner here:\n //The owner is not yet fully constructed.\n }\n NavMeshPathfindingObstacleBehavior.prototype.onDestroy = function () {\n if (this._manager && this._registeredInManager) {\n this._manager.removeObstacle(this);\n }\n };\n NavMeshPathfindingObstacleBehavior.prototype.doStepPreEvents = function (instanceContainer) {\n var owner = this.behavior.owner;\n //Make sure the obstacle is or is not in the obstacles manager.\n if (!this.behavior.activated() && this._registeredInManager) {\n this._manager.removeObstacle(this);\n this._registeredInManager = false;\n }\n else {\n if (this.behavior.activated() && !this._registeredInManager) {\n this._manager.addObstacle(this);\n this._registeredInManager = true;\n }\n }\n //Track changes in size or position\n if (this._oldX !== owner.getX() ||\n this._oldY !== owner.getY() ||\n this._oldWidth !== owner.getWidth() ||\n this._oldHeight !== owner.getHeight()) {\n if (this._registeredInManager) {\n this._manager.removeObstacle(this);\n this._manager.addObstacle(this);\n }\n this._oldX = owner.getX();\n this._oldY = owner.getY();\n this._oldWidth = owner.getWidth();\n this._oldHeight = owner.getHeight();\n }\n };\n NavMeshPathfindingObstacleBehavior.prototype.doStepPostEvents = function (instanceContainer) { };\n NavMeshPathfindingObstacleBehavior.prototype.onActivate = function () {\n if (this._registeredInManager) {\n return;\n }\n this._manager.addObstacle(this);\n this._registeredInManager = true;\n };\n NavMeshPathfindingObstacleBehavior.prototype.onDeActivate = function () {\n if (!this._registeredInManager) {\n return;\n }\n this._manager.removeObstacle(this);\n this._registeredInManager = false;\n };\n return NavMeshPathfindingObstacleBehavior;\n}());\n\ngdjs.__NavMeshPathfinding = gdjs.__NavMeshPathfinding || {};\ngdjs.__NavMeshPathfinding.NavMeshPathfindingBehavior = NavMeshPathfindingBehavior;\ngdjs.__NavMeshPathfinding.NavMeshPathfindingObstacleBehavior = NavMeshPathfindingObstacleBehavior;\n", + "inlineCode": [ + "", + "// This code has been built from https://github.com/D8H/NavMesh-GDevelop-Extension", + "// If you need to make any modification, please open a PR on github.", + "", + "var extendStatics = function(d, b) {", + " extendStatics = Object.setPrototypeOf ||", + " ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||", + " function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };", + " return extendStatics(d, b);", + "};", + "", + "function __extends(d, b) {", + " if (typeof b !== \"function\" && b !== null)", + " throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");", + " extendStatics(d, b);", + " function __() { this.constructor = d; }", + " d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());", + "}", + "", + "/**", + " * Stripped down version of Phaser's Vector2 with just the functionality needed for navmeshes.", + " *", + " * @export", + " * @class Vector2", + " */", + "var Vector2 = /** @class */ (function () {", + " function Vector2(x, y) {", + " if (x === void 0) { x = 0; }", + " if (y === void 0) { y = 0; }", + " this.x = x;", + " this.y = y;", + " }", + " Vector2.prototype.equals = function (v) {", + " return this.x === v.x && this.y === v.y;", + " };", + " Vector2.prototype.angle = function (v) {", + " return Math.atan2(v.y - this.y, v.x - this.x);", + " };", + " Vector2.prototype.distance = function (v) {", + " var dx = v.x - this.x;", + " var dy = v.y - this.y;", + " return Math.sqrt(dx * dx + dy * dy);", + " };", + " Vector2.prototype.add = function (v) {", + " this.x += v.x;", + " this.y += v.y;", + " };", + " Vector2.prototype.subtract = function (v) {", + " this.x -= v.x;", + " this.y -= v.y;", + " };", + " Vector2.prototype.clone = function () {", + " return new Vector2(this.x, this.y);", + " };", + " return Vector2;", + "}());", + "", + "var GridNode = /** @class */ (function () {", + " function GridNode(weight) {", + " this.h = 0;", + " this.g = 0;", + " this.f = 0;", + " this.closed = false;", + " this.visited = false;", + " this.parent = null;", + " this.weight = weight;", + " }", + " GridNode.prototype.isWall = function () {", + " return this.weight === 0;", + " };", + " GridNode.prototype.clean = function () {", + " this.f = 0;", + " this.g = 0;", + " this.h = 0;", + " this.visited = false;", + " this.closed = false;", + " this.parent = null;", + " };", + " return GridNode;", + "}());", + "", + "/**", + " * A class that represents a navigable polygon with a navmesh. It is built on top of a", + " * {@link Polygon}. It implements the properties and fields that javascript-astar needs - weight,", + " * toString, isWall and getCost. See GPS test from astar repo for structure:", + " * https://github.com/bgrins/javascript-astar/blob/master/test/tests.js", + " */", + "var NavPoly = /** @class */ (function (_super) {", + " __extends(NavPoly, _super);", + " /**", + " * Creates an instance of NavPoly.", + " */", + " function NavPoly(id, polygon) {", + " var _this = _super.call(this, 1) || this;", + " _this.id = id;", + " _this.polygon = polygon;", + " _this.edges = polygon.edges;", + " _this.neighbors = [];", + " _this.portals = [];", + " _this.centroid = _this.calculateCentroid();", + " _this.boundingRadius = _this.calculateRadius();", + " return _this;", + " }", + " /**", + " * Returns an array of points that form the polygon.", + " */", + " NavPoly.prototype.getPoints = function () {", + " return this.polygon.points;", + " };", + " /**", + " * Check if the given point-like object is within the polygon.", + " */", + " NavPoly.prototype.contains = function (point) {", + " // Phaser's polygon check doesn't handle when a point is on one of the edges of the line. Note:", + " // check numerical stability here. It would also be good to optimize this for different shapes.", + " return this.polygon.contains(point.x, point.y) || this.isPointOnEdge(point);", + " };", + " /**", + " * Only rectangles are supported, so this calculation works, but this is not actually the centroid", + " * calculation for a polygon. This is just the average of the vertices - proper centroid of a", + " * polygon factors in the area.", + " */", + " NavPoly.prototype.calculateCentroid = function () {", + " var centroid = new Vector2(0, 0);", + " var length = this.polygon.points.length;", + " this.polygon.points.forEach(function (p) { return centroid.add(p); });", + " centroid.x /= length;", + " centroid.y /= length;", + " return centroid;", + " };", + " /**", + " * Calculate the radius of a circle that circumscribes the polygon.", + " */", + " NavPoly.prototype.calculateRadius = function () {", + " var boundingRadius = 0;", + " for (var _i = 0, _a = this.polygon.points; _i < _a.length; _i++) {", + " var point = _a[_i];", + " var d = this.centroid.distance(point);", + " if (d > boundingRadius)", + " boundingRadius = d;", + " }", + " return boundingRadius;", + " };", + " /**", + " * Check if the given point-like object is on one of the edges of the polygon.", + " */", + " NavPoly.prototype.isPointOnEdge = function (_a) {", + " var x = _a.x, y = _a.y;", + " for (var _i = 0, _b = this.edges; _i < _b.length; _i++) {", + " var edge = _b[_i];", + " if (edge.pointOnSegment(x, y))", + " return true;", + " }", + " return false;", + " };", + " NavPoly.prototype.destroy = function () {", + " this.neighbors = [];", + " this.portals = [];", + " };", + " // === jsastar methods ===", + " NavPoly.prototype.toString = function () {", + " return \"NavPoly(id: \" + this.id + \" at: \" + this.centroid + \")\";", + " };", + " NavPoly.prototype.isWall = function () {", + " return false;", + " };", + " NavPoly.prototype.centroidDistance = function (navPolygon) {", + " return this.centroid.distance(navPolygon.centroid);", + " };", + " NavPoly.prototype.getCost = function (navPolygon) {", + " //TODO the cost method should not be in the Node", + " return this.centroidDistance(navPolygon);", + " };", + " return NavPoly;", + "}(GridNode));", + "", + "/**", + " * A graph memory structure", + " */", + "var Graph = /** @class */ (function () {", + " /**", + " * A graph memory structure", + " * @param {Array} gridIn 2D array of input weights", + " * @param {Object} [options]", + " * @param {bool} [options.diagonal] Specifies whether diagonal moves are allowed", + " */", + " function Graph(nodes, options) {", + " this.dirtyNodes = [];", + " options = options || {};", + " this.nodes = nodes;", + " this.diagonal = !!options.diagonal;", + " this.init();", + " }", + " Graph.prototype.init = function () {", + " this.dirtyNodes = [];", + " for (var i = 0; i < this.nodes.length; i++) {", + " this.nodes[i].clean();", + " }", + " };", + " Graph.prototype.cleanDirty = function () {", + " for (var i = 0; i < this.dirtyNodes.length; i++) {", + " this.dirtyNodes[i].clean();", + " }", + " this.dirtyNodes = [];", + " };", + " Graph.prototype.markDirty = function (node) {", + " this.dirtyNodes.push(node);", + " };", + " return Graph;", + "}());", + "", + "/**", + " * Graph for javascript-astar. It implements the functionality for astar. See GPS test from astar", + " * repo for structure: https://github.com/bgrins/javascript-astar/blob/master/test/tests.js", + " *", + " * @class NavGraph", + " * @private", + " */", + "var NavGraph = /** @class */ (function (_super) {", + " __extends(NavGraph, _super);", + " function NavGraph(navPolygons) {", + " var _this = _super.call(this, navPolygons) || this;", + " _this.nodes = navPolygons;", + " _this.init();", + " return _this;", + " }", + " NavGraph.prototype.neighbors = function (navPolygon) {", + " return navPolygon.neighbors;", + " };", + " NavGraph.prototype.navHeuristic = function (navPolygon1, navPolygon2) {", + " return navPolygon1.centroidDistance(navPolygon2);", + " };", + " NavGraph.prototype.destroy = function () {", + " this.cleanDirty();", + " this.nodes = [];", + " };", + " return NavGraph;", + "}(Graph));", + "", + "/**", + " * Calculate the distance squared between two points. This is an optimization to a square root when", + " * you just need to compare relative distances without needing to know the specific distance.", + " * @param a", + " * @param b", + " */", + "function distanceSquared(a, b) {", + " var dx = b.x - a.x;", + " var dy = b.y - a.y;", + " return dx * dx + dy * dy;", + "}", + "/**", + " * Project a point onto a line segment.", + " * JS Source: http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment", + " * @param point", + " * @param line", + " */", + "function projectPointToEdge(point, line) {", + " var a = line.start;", + " var b = line.end;", + " // Consider the parametric equation for the edge's line, p = a + t (b - a). We want to find", + " // where our point lies on the line by solving for t:", + " // t = [(p-a) . (b-a)] / |b-a|^2", + " var l2 = distanceSquared(a, b);", + " var t = ((point.x - a.x) * (b.x - a.x) + (point.y - a.y) * (b.y - a.y)) / l2;", + " // We clamp t from [0,1] to handle points outside the segment vw.", + " t = clamp(t, 0, 1);", + " // Project onto the segment", + " var p = new Vector2(a.x + t * (b.x - a.x), a.y + t * (b.y - a.y));", + " return p;", + "}", + "/**", + " * Twice the area of the triangle formed by a, b and c.", + " */", + "function triarea2(a, b, c) {", + " var ax = b.x - a.x;", + " var ay = b.y - a.y;", + " var bx = c.x - a.x;", + " var by = c.y - a.y;", + " return bx * ay - ax * by;", + "}", + "/**", + " * Clamp the given value between min and max.", + " */", + "function clamp(value, min, max) {", + " if (value < min)", + " value = min;", + " if (value > max)", + " value = max;", + " return value;", + "}", + "/**", + " * Check if two values are within a small margin of one another.", + " */", + "function almostEqual(value1, value2, errorMargin) {", + " if (errorMargin === void 0) { errorMargin = 0.0001; }", + " if (Math.abs(value1 - value2) <= errorMargin)", + " return true;", + " else", + " return false;", + "}", + "/**", + " * Find the smallest angle difference between two angles", + " * https://gist.github.com/Aaronduino/4068b058f8dbc34b4d3a9eedc8b2cbe0", + " */", + "function angleDifference(x, y) {", + " var a = x - y;", + " var i = a + Math.PI;", + " var j = Math.PI * 2;", + " a = i - Math.floor(i / j) * j; // (a+180) % 360; this ensures the correct sign", + " a -= Math.PI;", + " return a;", + "}", + "/**", + " * Check if two lines are collinear (within a small error margin).", + " */", + "function areCollinear(line1, line2, errorMargin) {", + " if (errorMargin === void 0) { errorMargin = 0.0001; }", + " // Figure out if the two lines are equal by looking at the area of the triangle formed", + " // by their points", + " var area1 = triarea2(line1.start, line1.end, line2.start);", + " var area2 = triarea2(line1.start, line1.end, line2.end);", + " if (almostEqual(area1, 0, errorMargin) && almostEqual(area2, 0, errorMargin)) {", + " return true;", + " }", + " else", + " return false;", + "}", + "", + "// Mostly sourced from PatrolJS at the moment. TODO: come back and reimplement this as an incomplete", + "/**", + " * @private", + " */", + "var Channel = /** @class */ (function () {", + " function Channel() {", + " this.portals = [];", + " this.path = [];", + " }", + " Channel.prototype.push = function (p1, p2) {", + " if (p2 === undefined)", + " p2 = p1;", + " this.portals.push({", + " left: p1,", + " right: p2,", + " });", + " };", + " Channel.prototype.stringPull = function () {", + " var portals = this.portals;", + " var pts = [];", + " // Init scan state", + " var apexIndex = 0;", + " var leftIndex = 0;", + " var rightIndex = 0;", + " var portalApex = portals[0].left;", + " var portalLeft = portals[0].left;", + " var portalRight = portals[0].right;", + " // Add start point.", + " pts.push(portalApex);", + " for (var i = 1; i < portals.length; i++) {", + " // Find the next portal vertices", + " var left = portals[i].left;", + " var right = portals[i].right;", + " // Update right vertex.", + " if (triarea2(portalApex, portalRight, right) <= 0.0) {", + " if (portalApex.equals(portalRight) || triarea2(portalApex, portalLeft, right) > 0.0) {", + " // Tighten the funnel.", + " portalRight = right;", + " rightIndex = i;", + " }", + " else {", + " // Right vertex just crossed over the left vertex, so the left vertex should", + " // now be part of the path.", + " pts.push(portalLeft);", + " // Restart scan from portal left point.", + " // Make current left the new apex.", + " portalApex = portalLeft;", + " apexIndex = leftIndex;", + " // Reset portal", + " portalLeft = portalApex;", + " portalRight = portalApex;", + " leftIndex = apexIndex;", + " rightIndex = apexIndex;", + " // Restart scan", + " i = apexIndex;", + " continue;", + " }", + " }", + " // Update left vertex.", + " if (triarea2(portalApex, portalLeft, left) >= 0.0) {", + " if (portalApex.equals(portalLeft) || triarea2(portalApex, portalRight, left) < 0.0) {", + " // Tighten the funnel.", + " portalLeft = left;", + " leftIndex = i;", + " }", + " else {", + " // Left vertex just crossed over the right vertex, so the right vertex should", + " // now be part of the path", + " pts.push(portalRight);", + " // Restart scan from portal right point.", + " // Make current right the new apex.", + " portalApex = portalRight;", + " apexIndex = rightIndex;", + " // Reset portal", + " portalLeft = portalApex;", + " portalRight = portalApex;", + " leftIndex = apexIndex;", + " rightIndex = apexIndex;", + " // Restart scan", + " i = apexIndex;", + " continue;", + " }", + " }", + " }", + " if (pts.length === 0 || !pts[pts.length - 1].equals(portals[portals.length - 1].left)) {", + " // Append last point to path.", + " pts.push(portals[portals.length - 1].left);", + " }", + " this.path = pts;", + " return pts;", + " };", + " return Channel;", + "}());", + "", + "/**", + " * Stripped down version of Phaser's Line with just the functionality needed for navmeshes.", + " *", + " * @export", + " * @class Line", + " */", + "var Line = /** @class */ (function () {", + " function Line(x1, y1, x2, y2) {", + " this.start = new Vector2(x1, y1);", + " this.end = new Vector2(x2, y2);", + " this.left = Math.min(x1, x2);", + " this.right = Math.max(x1, x2);", + " this.top = Math.min(y1, y2);", + " this.bottom = Math.max(y1, y2);", + " }", + " Line.prototype.pointOnSegment = function (x, y) {", + " return (x >= this.left &&", + " x <= this.right &&", + " y >= this.top &&", + " y <= this.bottom &&", + " this.pointOnLine(x, y));", + " };", + " Line.prototype.pointOnLine = function (x, y) {", + " // Compare slope of line start -> xy to line start -> line end", + " return (x - this.left) * (this.bottom - this.top) === (this.right - this.left) * (y - this.top);", + " };", + " return Line;", + "}());", + "", + "/**", + " * Stripped down version of Phaser's Polygon with just the functionality needed for navmeshes.", + " *", + " * @export", + " * @class Polygon", + " */", + "var Polygon = /** @class */ (function () {", + " function Polygon(points, closed) {", + " if (closed === void 0) { closed = true; }", + " this.isClosed = closed;", + " this.points = points;", + " this.edges = [];", + " for (var i = 1; i < points.length; i++) {", + " var p1 = points[i - 1];", + " var p2 = points[i];", + " this.edges.push(new Line(p1.x, p1.y, p2.x, p2.y));", + " }", + " if (this.isClosed) {", + " var first = points[0];", + " var last = points[points.length - 1];", + " this.edges.push(new Line(first.x, first.y, last.x, last.y));", + " }", + " }", + " Polygon.prototype.contains = function (x, y) {", + " var inside = false;", + " for (var i = -1, j = this.points.length - 1; ++i < this.points.length; j = i) {", + " var ix = this.points[i].x;", + " var iy = this.points[i].y;", + " var jx = this.points[j].x;", + " var jy = this.points[j].y;", + " if (((iy <= y && y < jy) || (jy <= y && y < iy)) &&", + " x < ((jx - ix) * (y - iy)) / (jy - iy) + ix) {", + " inside = !inside;", + " }", + " }", + " return inside;", + " };", + " return Polygon;", + "}());", + "", + "var BinaryHeap = /** @class */ (function () {", + " function BinaryHeap(scoreFunction) {", + " this.content = new Array();", + " this.scoreFunction = scoreFunction;", + " }", + " BinaryHeap.prototype.push = function (element) {", + " // Add the new element to the end of the array.", + " this.content.push(element);", + " // Allow it to sink down.", + " this.sinkDown(this.content.length - 1);", + " };", + " BinaryHeap.prototype.pop = function () {", + " // Store the first element so we can return it later.", + " var result = this.content[0];", + " // Get the element at the end of the array.", + " var end = this.content.pop();", + " if (!end)", + " return;", + " // If there are any elements left, put the end element at the", + " // start, and let it bubble up.", + " if (this.content.length > 0) {", + " this.content[0] = end;", + " this.bubbleUp(0);", + " }", + " return result;", + " };", + " BinaryHeap.prototype.remove = function (node) {", + " var i = this.content.indexOf(node);", + " // When it is found, the process seen in 'pop' is repeated", + " // to fill up the hole.", + " var end = this.content.pop();", + " if (!end)", + " return;", + " if (i !== this.content.length - 1) {", + " this.content[i] = end;", + " if (this.scoreFunction(end) < this.scoreFunction(node)) {", + " this.sinkDown(i);", + " }", + " else {", + " this.bubbleUp(i);", + " }", + " }", + " };", + " BinaryHeap.prototype.size = function () {", + " return this.content.length;", + " };", + " BinaryHeap.prototype.rescoreElement = function (node) {", + " this.sinkDown(this.content.indexOf(node));", + " };", + " BinaryHeap.prototype.sinkDown = function (n) {", + " // Fetch the element that has to be sunk.", + " var element = this.content[n];", + " // When at 0, an element can not sink any further.", + " while (n > 0) {", + " // Compute the parent element's index, and fetch it.", + " var parentN = ((n + 1) >> 1) - 1;", + " var parent = this.content[parentN];", + " // Swap the elements if the parent is greater.", + " if (this.scoreFunction(element) < this.scoreFunction(parent)) {", + " this.content[parentN] = element;", + " this.content[n] = parent;", + " // Update 'n' to continue at the new position.", + " n = parentN;", + " }", + " // Found a parent that is less, no need to sink any further.", + " else {", + " break;", + " }", + " }", + " };", + " BinaryHeap.prototype.bubbleUp = function (n) {", + " // Look up the target element and its score.", + " var length = this.content.length;", + " var element = this.content[n];", + " var elemScore = this.scoreFunction(element);", + " while (true) {", + " // Compute the indices of the child elements.", + " var child2N = (n + 1) << 1;", + " var child1N = child2N - 1;", + " // This is used to store the new position of the element, if any.", + " var swap = null;", + " var child1Score = 0;", + " // If the first child exists (is inside the array)...", + " if (child1N < length) {", + " // Look it up and compute its score.", + " var child1 = this.content[child1N];", + " child1Score = this.scoreFunction(child1);", + " // If the score is less than our element's, we need to swap.", + " if (child1Score < elemScore) {", + " swap = child1N;", + " }", + " }", + " // Do the same checks for the other child.", + " if (child2N < length) {", + " var child2 = this.content[child2N];", + " var child2Score = this.scoreFunction(child2);", + " if (child2Score < (swap === null ? elemScore : child1Score)) {", + " swap = child2N;", + " }", + " }", + " // If the element needs to be moved, swap it, and continue.", + " if (swap !== null) {", + " this.content[n] = this.content[swap];", + " this.content[swap] = element;", + " n = swap;", + " }", + " // Otherwise, we are done.", + " else {", + " break;", + " }", + " }", + " };", + " return BinaryHeap;", + "}());", + "", + "// The following implementation of the A* algorithm is from:", + "var AStar = /** @class */ (function () {", + " function AStar() {", + " }", + " /**", + " * Perform an A* Search on a graph given a start and end node.", + " * @param {Graph} graph", + " * @param {GridNode} start", + " * @param {GridNode} end", + " * @param {Object} [options]", + " * @param {bool} [options.closest] Specifies whether to return the", + " path to the closest node if the target is unreachable.", + " * @param {Function} [options.heuristic] Heuristic function (see", + " * astar.heuristics).", + " */", + " AStar.prototype.search = function (graph, start, end, ", + " // See list of heuristics: http://theory.stanford.edu/~amitp/GameProgramming/Heuristics.html", + " heuristic, closest) {", + " if (closest === void 0) { closest = false; }", + " graph.cleanDirty();", + " var openHeap = this.getHeap();", + " var closestNode = start; // set the start node to be the closest if required", + " start.h = heuristic(start, end);", + " graph.markDirty(start);", + " openHeap.push(start);", + " while (openHeap.size() > 0) {", + " // Grab the lowest f(x) to process next. Heap keeps this sorted for us.", + " var currentNode = openHeap.pop();", + " // never happen", + " if (!currentNode)", + " return [];", + " // End case -- result has been found, return the traced path.", + " if (currentNode === end) {", + " return this.pathTo(currentNode);", + " }", + " // Normal case -- move currentNode from open to closed, process each of its neighbors.", + " currentNode.closed = true;", + " // Find all neighbors for the current node.", + " var neighbors = graph.neighbors(currentNode);", + " for (var i = 0, il = neighbors.length; i < il; ++i) {", + " var neighbor = neighbors[i];", + " if (neighbor.closed || neighbor.isWall()) {", + " // Not a valid node to process, skip to next neighbor.", + " continue;", + " }", + " // The g score is the shortest distance from start to current node.", + " // We need to check if the path we have arrived at this neighbor is the shortest one we have seen yet.", + " var gScore = currentNode.g + neighbor.getCost(currentNode);", + " var beenVisited = neighbor.visited;", + " if (!beenVisited || gScore < neighbor.g) {", + " // Found an optimal (so far) path to this node. Take score for node to see how good it is.", + " neighbor.visited = true;", + " neighbor.parent = currentNode;", + " neighbor.h = neighbor.h || heuristic(neighbor, end);", + " neighbor.g = gScore;", + " neighbor.f = neighbor.g + neighbor.h;", + " graph.markDirty(neighbor);", + " if (closest) {", + " // If the neighbor is closer than the current closestNode or if it's equally close but has", + " // a cheaper path than the current closest node then it becomes the closest node", + " if (neighbor.h < closestNode.h ||", + " (neighbor.h === closestNode.h && neighbor.g < closestNode.g)) {", + " closestNode = neighbor;", + " }", + " }", + " if (!beenVisited) {", + " // Pushing to heap will put it in proper place based on the 'f' value.", + " openHeap.push(neighbor);", + " }", + " else {", + " // Already seen the node, but since it has been rescored we need to reorder it in the heap", + " openHeap.rescoreElement(neighbor);", + " }", + " }", + " }", + " }", + " if (closest) {", + " return this.pathTo(closestNode);", + " }", + " // No result was found - empty array signifies failure to find path.", + " return [];", + " };", + " AStar.prototype.pathTo = function (node) {", + " var curr = node;", + " var path = new Array();", + " while (curr.parent) {", + " path.unshift(curr);", + " curr = curr.parent;", + " }", + " return path;", + " };", + " AStar.prototype.getHeap = function () {", + " return new BinaryHeap(function (node) {", + " return node.f;", + " });", + " };", + " return AStar;", + "}());", + "", + "/**", + " * The `NavMesh` class is the workhorse that represents a navigation mesh built from a series of", + " * polygons. Once built, the mesh can be asked for a path from one point to another point. Some", + " * internal terminology usage:", + " * - neighbor: a polygon that shares part of an edge with another polygon", + " * - portal: when two neighbor's have edges that overlap, the portal is the overlapping line segment", + " * - channel: the path of polygons from starting point to end point", + " * - pull the string: run the funnel algorithm on the channel so that the path hugs the edges of the", + " * channel. Equivalent to having a string snaking through a hallway and then pulling it taut.", + " */", + "var NavMesh = /** @class */ (function () {", + " /**", + " * @param meshPolygonPoints Array where each element is an array of point-like objects that", + " * defines a polygon.", + " * @param meshShrinkAmount The amount (in pixels) that the navmesh has been shrunk around", + " * obstacles (a.k.a the amount obstacles have been expanded).", + " */", + " function NavMesh(meshPolygonPoints, meshShrinkAmount) {", + " if (meshShrinkAmount === void 0) { meshShrinkAmount = 0; }", + " this.meshShrinkAmount = meshShrinkAmount;", + " // Convert the PolyPoints[] into NavPoly instances.", + " this.navPolygons = meshPolygonPoints.map(function (polyPoints, i) { return new NavPoly(i, new Polygon(polyPoints)); });", + " this.calculateNeighbors();", + " // Astar graph of connections between polygons", + " this.graph = new NavGraph(this.navPolygons);", + " }", + " /**", + " * Get the NavPolys that are in this navmesh.", + " */", + " NavMesh.prototype.getPolygons = function () {", + " return this.navPolygons;", + " };", + " /**", + " * Cleanup method to remove references.", + " */", + " NavMesh.prototype.destroy = function () {", + " this.graph.destroy();", + " for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {", + " var poly = _a[_i];", + " poly.destroy();", + " }", + " this.navPolygons = [];", + " };", + " /**", + " * Find if the given point is within any of the polygons in the mesh.", + " * @param point", + " */", + " NavMesh.prototype.isPointInMesh = function (point) {", + " return this.navPolygons.some(function (navPoly) { return navPoly.contains(point); });", + " };", + " /**", + " * Find the closest point in the mesh to the given point. If the point is already in the mesh,", + " * this will give you that point. If the point is outside of the mesh, this will attempt to", + " * project this point into the mesh (up to the given maxAllowableDist). This returns an object", + " * with:", + " * - distance - from the given point to the mesh", + " * - polygon - the one the point is closest to, or null", + " * - point - the point inside the mesh, or null", + " * @param point", + " * @param maxAllowableDist", + " */", + " NavMesh.prototype.findClosestMeshPoint = function (point, maxAllowableDist) {", + " if (maxAllowableDist === void 0) { maxAllowableDist = Number.POSITIVE_INFINITY; }", + " var minDistance = maxAllowableDist;", + " var closestPoly = null;", + " var pointOnClosestPoly = null;", + " for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {", + " var navPoly = _a[_i];", + " // If we are inside a poly, we've got the closest.", + " if (navPoly.contains(point)) {", + " minDistance = 0;", + " closestPoly = navPoly;", + " pointOnClosestPoly = point;", + " break;", + " }", + " // Is the poly close enough to warrant a more accurate check? Point is definitely outside of", + " // the polygon. Distance - Radius is the smallest possible distance to an edge of the poly.", + " // This will underestimate distance, but that's perfectly fine.", + " var r = navPoly.boundingRadius;", + " var d = navPoly.centroid.distance(point);", + " if (d - r < minDistance) {", + " var result = this.projectPointToPolygon(point, navPoly);", + " if (result.distance < minDistance) {", + " minDistance = result.distance;", + " closestPoly = navPoly;", + " pointOnClosestPoly = result.point;", + " }", + " }", + " }", + " return { distance: minDistance, polygon: closestPoly, point: pointOnClosestPoly };", + " };", + " /**", + " * Find a path from the start point to the end point using this nav mesh.", + " * @param startPoint A point-like object in the form {x, y}", + " * @param endPoint A point-like object in the form {x, y}", + " * @returns An array of points if a path is found, or null if no path", + " */", + " NavMesh.prototype.findPath = function (startPoint, endPoint) {", + " var startPoly = null;", + " var endPoly = null;", + " var startDistance = Number.MAX_VALUE;", + " var endDistance = Number.MAX_VALUE;", + " var d, r;", + " var startVector = new Vector2(startPoint.x, startPoint.y);", + " var endVector = new Vector2(endPoint.x, endPoint.y);", + " // Find the closest poly for the starting and ending point", + " for (var _i = 0, _a = this.navPolygons; _i < _a.length; _i++) {", + " var navPoly = _a[_i];", + " r = navPoly.boundingRadius;", + " // Start", + " d = navPoly.centroid.distance(startVector);", + " if (d <= startDistance && d <= r && navPoly.contains(startVector)) {", + " startPoly = navPoly;", + " startDistance = d;", + " }", + " // End", + " d = navPoly.centroid.distance(endVector);", + " if (d <= endDistance && d <= r && navPoly.contains(endVector)) {", + " endPoly = navPoly;", + " endDistance = d;", + " }", + " }", + " // If the end point wasn't inside a polygon, run a more liberal check that allows a point", + " // to be within meshShrinkAmount radius of a polygon", + " if (!endPoly && this.meshShrinkAmount > 0) {", + " for (var _b = 0, _c = this.navPolygons; _b < _c.length; _b++) {", + " var navPoly = _c[_b];", + " r = navPoly.boundingRadius + this.meshShrinkAmount;", + " d = navPoly.centroid.distance(endVector);", + " if (d <= r) {", + " var distance = this.projectPointToPolygon(endVector, navPoly).distance;", + " if (distance <= this.meshShrinkAmount && distance < endDistance) {", + " endPoly = navPoly;", + " endDistance = distance;", + " }", + " }", + " }", + " }", + " // No matching polygons locations for the end, so no path found", + " // because start point is valid normally, check end point first", + " if (!endPoly)", + " return null;", + " // Same check as above, but for the start point", + " if (!startPoly && this.meshShrinkAmount > 0) {", + " for (var _d = 0, _e = this.navPolygons; _d < _e.length; _d++) {", + " var navPoly = _e[_d];", + " // Check if point is within bounding circle to avoid extra projection calculations", + " r = navPoly.boundingRadius + this.meshShrinkAmount;", + " d = navPoly.centroid.distance(startVector);", + " if (d <= r) {", + " // Check if projected point is within range of a polygon and is closer than the", + " // previous point", + " var distance = this.projectPointToPolygon(startVector, navPoly).distance;", + " if (distance <= this.meshShrinkAmount && distance < startDistance) {", + " startPoly = navPoly;", + " startDistance = distance;", + " }", + " }", + " }", + " }", + " // No matching polygons locations for the start, so no path found", + " if (!startPoly)", + " return null;", + " // If the start and end polygons are the same, return a direct path", + " if (startPoly === endPoly)", + " return [startVector, endVector];", + " // Search!", + " var astarPath = new AStar().search(this.graph, startPoly, endPoly, this.graph.navHeuristic);", + " // While the start and end polygons may be valid, no path between them", + " if (astarPath.length === 0)", + " return null;", + " // jsastar drops the first point from the path, but the funnel algorithm needs it", + " astarPath.unshift(startPoly);", + " // We have a path, so now time for the funnel algorithm", + " var channel = new Channel();", + " channel.push(startVector);", + " for (var i = 0; i < astarPath.length - 1; i++) {", + " var navPolygon = astarPath[i];", + " var nextNavPolygon = astarPath[i + 1];", + " // Find the portal", + " var portal = null;", + " for (var i_1 = 0; i_1 < navPolygon.neighbors.length; i_1++) {", + " if (navPolygon.neighbors[i_1].id === nextNavPolygon.id) {", + " portal = navPolygon.portals[i_1];", + " }", + " }", + " if (!portal)", + " throw new Error(\"Path was supposed to be found, but portal is missing!\");", + " // Push the portal vertices into the channel", + " channel.push(portal.start, portal.end);", + " }", + " channel.push(endVector);", + " // Pull a string along the channel to run the funnel", + " channel.stringPull();", + " // Clone path, excluding duplicates", + " var lastPoint = null;", + " var phaserPath = new Array();", + " for (var _f = 0, _g = channel.path; _f < _g.length; _f++) {", + " var p = _g[_f];", + " var newPoint = p.clone();", + " if (!lastPoint || !newPoint.equals(lastPoint))", + " phaserPath.push(newPoint);", + " lastPoint = newPoint;", + " }", + " return phaserPath;", + " };", + " NavMesh.prototype.calculateNeighbors = function () {", + " // Fill out the neighbor information for each navpoly", + " for (var i = 0; i < this.navPolygons.length; i++) {", + " var navPoly = this.navPolygons[i];", + " for (var j = i + 1; j < this.navPolygons.length; j++) {", + " var otherNavPoly = this.navPolygons[j];", + " // Check if the other navpoly is within range to touch", + " var d = navPoly.centroid.distance(otherNavPoly.centroid);", + " if (d > navPoly.boundingRadius + otherNavPoly.boundingRadius)", + " continue;", + " // The are in range, so check each edge pairing", + " for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {", + " var edge = _a[_i];", + " for (var _b = 0, _c = otherNavPoly.edges; _b < _c.length; _b++) {", + " var otherEdge = _c[_b];", + " // If edges aren't collinear, not an option for connecting navpolys", + " if (!areCollinear(edge, otherEdge))", + " continue;", + " // If they are collinear, check if they overlap", + " var overlap = this.getSegmentOverlap(edge, otherEdge);", + " if (!overlap)", + " continue;", + " // Connections are symmetric!", + " navPoly.neighbors.push(otherNavPoly);", + " otherNavPoly.neighbors.push(navPoly);", + " // Calculate the portal between the two polygons - this needs to be in", + " // counter-clockwise order, relative to each polygon", + " var p1 = overlap[0], p2 = overlap[1];", + " var edgeStartAngle = navPoly.centroid.angle(edge.start);", + " var a1 = navPoly.centroid.angle(overlap[0]);", + " var a2 = navPoly.centroid.angle(overlap[1]);", + " var d1 = angleDifference(edgeStartAngle, a1);", + " var d2 = angleDifference(edgeStartAngle, a2);", + " if (d1 < d2) {", + " navPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));", + " }", + " else {", + " navPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));", + " }", + " edgeStartAngle = otherNavPoly.centroid.angle(otherEdge.start);", + " a1 = otherNavPoly.centroid.angle(overlap[0]);", + " a2 = otherNavPoly.centroid.angle(overlap[1]);", + " d1 = angleDifference(edgeStartAngle, a1);", + " d2 = angleDifference(edgeStartAngle, a2);", + " if (d1 < d2) {", + " otherNavPoly.portals.push(new Line(p1.x, p1.y, p2.x, p2.y));", + " }", + " else {", + " otherNavPoly.portals.push(new Line(p2.x, p2.y, p1.x, p1.y));", + " }", + " // Two convex polygons shouldn't be connected more than once! (Unless", + " // there are unnecessary vertices...)", + " }", + " }", + " }", + " }", + " };", + " // Check two collinear line segments to see if they overlap by sorting the points.", + " // Algorithm source: http://stackoverflow.com/a/17152247", + " NavMesh.prototype.getSegmentOverlap = function (line1, line2) {", + " var points = [", + " { line: line1, point: line1.start },", + " { line: line1, point: line1.end },", + " { line: line2, point: line2.start },", + " { line: line2, point: line2.end },", + " ];", + " points.sort(function (a, b) {", + " if (a.point.x < b.point.x)", + " return -1;", + " else if (a.point.x > b.point.x)", + " return 1;", + " else {", + " if (a.point.y < b.point.y)", + " return -1;", + " else if (a.point.y > b.point.y)", + " return 1;", + " else", + " return 0;", + " }", + " });", + " // If the first two points in the array come from the same line, no overlap", + " var noOverlap = points[0].line === points[1].line;", + " // If the two middle points in the array are the same coordinates, then there is a", + " // single point of overlap.", + " var singlePointOverlap = points[1].point.equals(points[2].point);", + " if (noOverlap || singlePointOverlap)", + " return null;", + " else", + " return [points[1].point, points[2].point];", + " };", + " /**", + " * Project a point onto a polygon in the shortest distance possible.", + " *", + " * @param {Phaser.Point} point The point to project", + " * @param {NavPoly} navPoly The navigation polygon to test against", + " * @returns {{point: Phaser.Point, distance: number}}", + " */", + " NavMesh.prototype.projectPointToPolygon = function (point, navPoly) {", + " var closestProjection = null;", + " var closestDistance = Number.MAX_VALUE;", + " for (var _i = 0, _a = navPoly.edges; _i < _a.length; _i++) {", + " var edge = _a[_i];", + " var projectedPoint = projectPointToEdge(point, edge);", + " var d = point.distance(projectedPoint);", + " if (closestProjection === null || d < closestDistance) {", + " closestDistance = d;", + " closestProjection = projectedPoint;", + " }", + " }", + " return { point: closestProjection, distance: closestDistance };", + " };", + " return NavMesh;", + "}());", + "", + "/**", + " * This implementation is strongly inspired from CritterAI class \"Geometry\".", + " */", + "var Geometry = /** @class */ (function () {", + " function Geometry() {", + " }", + " /**", + " * Returns TRUE if line segment AB intersects with line segment CD in any", + " * manner. Either collinear or at a single point.", + " * @param ax The x-value for point (ax, ay) in line segment AB.", + " * @param ay The y-value for point (ax, ay) in line segment AB.", + " * @param bx The x-value for point (bx, by) in line segment AB.", + " * @param by The y-value for point (bx, by) in line segment AB.", + " * @param cx The x-value for point (cx, cy) in line segment CD.", + " * @param cy The y-value for point (cx, cy) in line segment CD.", + " * @param dx The x-value for point (dx, dy) in line segment CD.", + " * @param dy The y-value for point (dx, dy) in line segment CD.", + " * @return TRUE if line segment AB intersects with line segment CD in any", + " * manner.", + " */", + " Geometry.segmentsIntersect = function (ax, ay, bx, by, cx, cy, dx, dy) {", + " // This is modified 2D line-line intersection/segment-segment", + " // intersection test.", + " var deltaABx = bx - ax;", + " var deltaABy = by - ay;", + " var deltaCAx = ax - cx;", + " var deltaCAy = ay - cy;", + " var deltaCDx = dx - cx;", + " var deltaCDy = dy - cy;", + " var numerator = deltaCAy * deltaCDx - deltaCAx * deltaCDy;", + " var denominator = deltaABx * deltaCDy - deltaABy * deltaCDx;", + " // Perform early exit tests.", + " if (denominator === 0 && numerator !== 0) {", + " // If numerator is zero, then the lines are colinear.", + " // Since it isn't, then the lines must be parallel.", + " return false;", + " }", + " // Lines intersect. But do the segments intersect?", + " // Forcing float division on both of these via casting of the", + " // denominator.", + " var factorAB = numerator / denominator;", + " var factorCD = (deltaCAy * deltaABx - deltaCAx * deltaABy) / denominator;", + " // Determine the type of intersection", + " if (factorAB >= 0.0 &&", + " factorAB <= 1.0 &&", + " factorCD >= 0.0 &&", + " factorCD <= 1.0) {", + " return true; // The two segments intersect.", + " }", + " // The lines intersect, but segments to not.", + " return false;", + " };", + " /**", + " * Returns the distance squared from the point to the line segment.", + " *", + " * Behavior is undefined if the the closest distance is outside the", + " * line segment.", + " *", + " * @param px The x-value of point (px, py).", + " * @param py The y-value of point (px, py)", + " * @param ax The x-value of the line segment's vertex A.", + " * @param ay The y-value of the line segment's vertex A.", + " * @param bx The x-value of the line segment's vertex B.", + " * @param by The y-value of the line segment's vertex B.", + " * @return The distance squared from the point (px, py) to line segment AB.", + " */", + " Geometry.getPointSegmentDistanceSq = function (px, py, ax, ay, bx, by) {", + " // Reference: http://local.wasp.uwa.edu.au/~pbourke/geometry/pointline/", + " //", + " // The goal of the algorithm is to find the point on line segment AB", + " // that is closest to P and then calculate the distance between P", + " // and that point.", + " var deltaABx = bx - ax;", + " var deltaABy = by - ay;", + " var deltaAPx = px - ax;", + " var deltaAPy = py - ay;", + " var segmentABLengthSq = deltaABx * deltaABx + deltaABy * deltaABy;", + " if (segmentABLengthSq === 0) {", + " // AB is not a line segment. So just return", + " // distanceSq from P to A", + " return deltaAPx * deltaAPx + deltaAPy * deltaAPy;", + " }", + " var u = (deltaAPx * deltaABx + deltaAPy * deltaABy) / segmentABLengthSq;", + " if (u < 0) {", + " // Closest point on line AB is outside outside segment AB and", + " // closer to A. So return distanceSq from P to A.", + " return deltaAPx * deltaAPx + deltaAPy * deltaAPy;", + " }", + " else if (u > 1) {", + " // Closest point on line AB is outside segment AB and closer to B.", + " // So return distanceSq from P to B.", + " return (px - bx) * (px - bx) + (py - by) * (py - by);", + " }", + " // Closest point on lineAB is inside segment AB. So find the exact", + " // point on AB and calculate the distanceSq from it to P.", + " // The calculation in parenthesis is the location of the point on", + " // the line segment.", + " var deltaX = ax + u * deltaABx - px;", + " var deltaY = ay + u * deltaABy - py;", + " return deltaX * deltaX + deltaY * deltaY;", + " };", + " return Geometry;", + "}());", + "", + "/**", + " * A cell that holds data needed by the 1st steps of the NavMesh generation.", + " */", + "var RasterizationCell = /** @class */ (function () {", + " function RasterizationCell(x, y) {", + " /**", + " * 0 means there is an obstacle in the cell.", + " * See {@link RegionGenerator}", + " */", + " this.distanceToObstacle = Number.MAX_VALUE;", + " this.regionID = RasterizationCell.NULL_REGION_ID;", + " this.distanceToRegionCore = 0;", + " /**", + " * If a cell is connected to one or more external regions then the", + " * flag will be a 4 bit value where connections are recorded as", + " * follows:", + " * - bit1 = neighbor0", + " * - bit2 = neighbor1", + " * - bit3 = neighbor2", + " * - bit4 = neighbor3", + " * With the meaning of the bits as follows:", + " * - 0 = neighbor in same region.", + " * - 1 = neighbor not in same region (neighbor may be the obstacle", + " * region or a real region).", + " *", + " * See {@link ContourBuilder}", + " */", + " this.contourFlags = 0;", + " this.x = x;", + " this.y = y;", + " this.clear();", + " }", + " RasterizationCell.prototype.clear = function () {", + " this.distanceToObstacle = Number.MAX_VALUE;", + " this.regionID = RasterizationCell.NULL_REGION_ID;", + " this.distanceToRegionCore = 0;", + " this.contourFlags = 0;", + " };", + " /** A cell that has not been assigned to any region yet */", + " RasterizationCell.NULL_REGION_ID = 0;", + " /**", + " * A cell that contains an obstacle.", + " *", + " * The value is the same as NULL_REGION_ID because the cells that are", + " * not assigned to any region at the end of the flooding algorithm are", + " * the obstacle cells.", + " */", + " RasterizationCell.OBSTACLE_REGION_ID = 0;", + " return RasterizationCell;", + "}());", + "", + "var RasterizationGrid = /** @class */ (function () {", + " function RasterizationGrid(left, top, right, bottom, cellWidth, cellHeight) {", + " this.regionCount = 0;", + " this.cellWidth = cellWidth;", + " this.cellHeight = cellHeight;", + " this.originX = left - cellWidth;", + " this.originY = top - cellHeight;", + " var dimX = 2 + Math.ceil((right - left) / cellWidth);", + " var dimY = 2 + Math.ceil((bottom - top) / cellHeight);", + " this.cells = [];", + " for (var y = 0; y < dimY; y++) {", + " this.cells[y] = [];", + " for (var x = 0; x < dimX; x++) {", + " this.cells[y][x] = new RasterizationCell(x, y);", + " }", + " }", + " }", + " RasterizationGrid.prototype.clear = function () {", + " for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {", + " var row = _a[_i];", + " for (var _b = 0, row_1 = row; _b < row_1.length; _b++) {", + " var cell = row_1[_b];", + " cell.clear();", + " }", + " }", + " this.regionCount = 0;", + " };", + " /**", + " *", + " * @param position the position on the scene", + " * @param gridPosition the position on the grid", + " * @returns the position on the grid", + " */", + " RasterizationGrid.prototype.convertToGridBasis = function (position, gridPosition) {", + " gridPosition.x = (position.x - this.originX) / this.cellWidth;", + " gridPosition.y = (position.y - this.originY) / this.cellHeight;", + " return gridPosition;", + " };", + " /**", + " *", + " * @param gridPosition the position on the grid", + " * @param position the position on the scene", + " * @returns the position on the scene", + " */", + " RasterizationGrid.prototype.convertFromGridBasis = function (gridPosition, position) {", + " position.x = gridPosition.x * this.cellWidth + this.originX;", + " position.y = gridPosition.y * this.cellHeight + this.originY;", + " return position;", + " };", + " RasterizationGrid.prototype.get = function (x, y) {", + " return this.cells[y][x];", + " };", + " RasterizationGrid.prototype.getNeighbor = function (cell, direction) {", + " var delta = RasterizationGrid.neighbor8Deltas[direction];", + " return this.cells[cell.y + delta.y][cell.x + delta.x];", + " };", + " RasterizationGrid.prototype.dimY = function () {", + " return this.cells.length;", + " };", + " RasterizationGrid.prototype.dimX = function () {", + " var firstColumn = this.cells[0];", + " return firstColumn ? firstColumn.length : 0;", + " };", + " RasterizationGrid.prototype.obstacleDistanceMax = function () {", + " var max = 0;", + " for (var _i = 0, _a = this.cells; _i < _a.length; _i++) {", + " var cellRow = _a[_i];", + " for (var _b = 0, cellRow_1 = cellRow; _b < cellRow_1.length; _b++) {", + " var cell = cellRow_1[_b];", + " if (cell.distanceToObstacle > max) {", + " max = cell.distanceToObstacle;", + " }", + " }", + " }", + " return max;", + " };", + " RasterizationGrid.neighbor4Deltas = [", + " { x: -1, y: 0 },", + " { x: 0, y: 1 },", + " { x: 1, y: 0 },", + " { x: 0, y: -1 },", + " ];", + " RasterizationGrid.neighbor8Deltas = [", + " { x: -1, y: 0 },", + " { x: 0, y: 1 },", + " { x: 1, y: 0 },", + " { x: 0, y: -1 },", + " { x: 1, y: 1 },", + " { x: -1, y: 1 },", + " { x: -1, y: -1 },", + " { x: 1, y: -1 },", + " ];", + " return RasterizationGrid;", + "}());", + "", + "/**", + " * Builds a set of contours from the region information contained in", + " * {@link RasterizationCell}. It does this by locating and \"walking\" the edges.", + " *", + " * This implementation is strongly inspired from CritterAI class \"ContourSetBuilder\".", + " * http://www.critterai.org/projects/nmgen_study/contourgen.html", + " */", + "var ContourBuilder = /** @class */ (function () {", + " function ContourBuilder() {", + " // These are working lists whose content changes with each iteration", + " // of the up coming loop. They represent the detailed and simple", + " // contour vertices.", + " // Initial sizing is arbitrary.", + " this.workingRawVertices = new Array(256);", + " this.workingSimplifiedVertices = new Array(64);", + " }", + " /**", + " * Generates a contour set from the provided {@link RasterizationGrid}", + " *", + " * The provided field is expected to contain region information.", + " * Behavior is undefined if the provided field is malformed or incomplete.", + " *", + " * This operation overwrites the flag fields for all cells in the", + " * provided field. So the flags must be saved and restored if they are", + " * important.", + " *", + " * @param grid A fully generated field.", + " * @param threshold The maximum distance (in cells) the edge of the contour", + " * may deviate from the source geometry when the rastered obstacles are", + " * vectorized.", + " *", + " * Setting it to:", + " * - 1 ensure that an aliased edge won't be split to more edges.", + " * - more that 1 will reduce the number of edges but the obstacles edges", + " * will be followed with less accuracy.", + " * - less that 1 might be more accurate but it may try to follow the", + " * aliasing and be a lot less accurate.", + " *", + " * Values under 1 can be useful in specific cases:", + " * - when edges are horizontal or vertical, there is no aliasing so value", + " * near 0 can do better results.", + " * - when edges are 45° multiples, aliased vertex won't be farther than", + " * sqrt(2)/2 so values over 0.71 should give good results but not", + " * necessarily better than 1.", + " *", + " * @return The contours generated from the field.", + " */", + " ContourBuilder.prototype.buildContours = function (grid, threshold) {", + " var contours = new Array(grid.regionCount);", + " contours.length = 0;", + " var contoursByRegion = new Array(grid.regionCount);", + " var discardedContours = 0;", + " // Set the flags on all cells in non-obstacle regions to indicate which", + " // edges are connected to external regions.", + " //", + " // Reference: Neighbor search and nomenclature.", + " // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch", + " //", + " // If a cell has no connections to external regions or is", + " // completely surrounded by other regions (a single cell island),", + " // its flag will be zero.", + " //", + " // If a cell is connected to one or more external regions then the", + " // flag will be a 4 bit value where connections are recorded as", + " // follows:", + " // bit1 = neighbor0", + " // bit2 = neighbor1", + " // bit3 = neighbor2", + " // bit4 = neighbor3", + " // With the meaning of the bits as follows:", + " // 0 = neighbor in same region.", + " // 1 = neighbor not in same region (neighbor may be the obstacle", + " // region or a real region).", + " for (var y = 1; y < grid.dimY() - 1; y++) {", + " for (var x = 1; x < grid.dimX() - 1; x++) {", + " var cell = grid.get(x, y);", + " // Note: This algorithm first sets the flag bits such that", + " // 1 = \"neighbor is in the same region\". At the end it inverts", + " // the bits so flags are as expected.", + " // Default to \"not connected to any external region\".", + " cell.contourFlags = 0;", + " if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID)", + " // Don't care about cells in the obstacle region.", + " continue;", + " for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {", + " var delta = RasterizationGrid.neighbor4Deltas[direction];", + " var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);", + " if (cell.regionID === neighbor.regionID) {", + " // Neighbor is in same region as this cell.", + " // Set the bit for this neighbor to 1 (Will be inverted later).", + " cell.contourFlags |= 1 << direction;", + " }", + " }", + " // Invert the bits so a bit value of 1 indicates neighbor NOT in", + " // same region.", + " cell.contourFlags ^= 0xf;", + " if (cell.contourFlags === 0xf) {", + " // This is an island cell (All neighbors are from other regions)", + " // Get rid of flags.", + " cell.contourFlags = 0;", + " console.warn(\"Discarded contour: Island cell. Can't form a contour. Region: \" +", + " cell.regionID);", + " discardedContours++;", + " }", + " }", + " }", + " // Loop through all cells looking for cells on the edge of a region.", + " //", + " // At this point, only cells with flags != 0 are edge cells that", + " // are part of a region contour.", + " //", + " // The process of building a contour will clear the flags on all cells", + " // that make up the contour to ensure they are only processed once.", + " for (var y = 1; y < grid.dimY() - 1; y++) {", + " for (var x = 1; x < grid.dimX() - 1; x++) {", + " var cell = grid.get(x, y);", + " if (cell.regionID === RasterizationCell.OBSTACLE_REGION_ID ||", + " cell.contourFlags === 0) {", + " // cell is either: Part of the obstacle region, does not", + " // represent an edge cell, or was already processed during", + " // an earlier iteration.", + " continue;", + " }", + " this.workingRawVertices.length = 0;", + " this.workingSimplifiedVertices.length = 0;", + " // The cell is part of an unprocessed region's contour.", + " // Locate a direction of the cell's edge which points toward", + " // another region (there is at least one).", + " var startDirection = 0;", + " while ((cell.contourFlags & (1 << startDirection)) === 0) {", + " startDirection++;", + " }", + " // We now have a cell that is part of a contour and a direction", + " // that points to a different region (obstacle or real).", + " // Build the contour.", + " this.buildRawContours(grid, cell, startDirection, this.workingRawVertices);", + " // Perform post processing on the contour in order to", + " // create the final, simplified contour.", + " this.generateSimplifiedContour(cell.regionID, this.workingRawVertices, this.workingSimplifiedVertices, threshold);", + " // The CritterAI implementation filters polygons with less than", + " // 3 vertices, but they are needed to filter vertices in the middle", + " // (not on an obstacle region border).", + " var contour = Array.from(this.workingSimplifiedVertices);", + " contours.push(contour);", + " contoursByRegion[cell.regionID] = contour;", + " }", + " }", + " if (contours.length + discardedContours !== grid.regionCount - 1) {", + " // The only valid state is one contour per region.", + " //", + " // The only time this should occur is if an invalid contour", + " // was formed or if a region resulted in multiple", + " // contours (bad region data).", + " //", + " // IMPORTANT: While a mismatch may not be a fatal error,", + " // it should be addressed since it can result in odd,", + " // hard to spot anomalies later in the pipeline.", + " //", + " // A known cause is if a region fully encompasses another", + " // region. In such a case, two contours will be formed.", + " // The normal outer contour and an inner contour.", + " // The CleanNullRegionBorders algorithm protects", + " // against internal encompassed obstacle regions.", + " console.error(\"Contour generation failed: Detected contours does\" +", + " \" not match the number of regions. Regions: \" +", + " (grid.regionCount - 1) +", + " \", Detected contours: \" +", + " (contours.length + discardedContours) +", + " \" (Actual: \" +", + " contours.length +", + " \", Discarded: \" +", + " discardedContours +", + " \")\");", + " // The CritterAI implementation has more detailed logs.", + " // They can be interesting for debugging.", + " }", + " this.filterNonObstacleVertices(contours, contoursByRegion);", + " return contours;", + " };", + " /**", + " * Search vertices that are not shared with the obstacle region and", + " * remove them.", + " *", + " * Some contours will have no vertex left.", + " *", + " * @param contours", + " * @param contoursByRegion Some regions may have been discarded", + " * so contours index can't be used.", + " */", + " ContourBuilder.prototype.filterNonObstacleVertices = function (contours, contoursByRegion) {", + " // This was not part of the CritterAI implementation.", + " // The removed vertex is merged on the nearest of the edges other extremity", + " // that is on an obstacle border.", + " var commonVertexContours = new Array(5);", + " var commonVertexIndexes = new Array(5);", + " // Each pass only filter vertex that have an edge other extremity on an obstacle.", + " // Vertex depth (in number of edges to reach an obstacle) is reduces by", + " // at least one by each pass.", + " var movedAnyVertex = false;", + " do {", + " movedAnyVertex = false;", + " for (var _i = 0, contours_1 = contours; _i < contours_1.length; _i++) {", + " var contour = contours_1[_i];", + " for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {", + " var vertex = contour[vertexIndex];", + " var nextVertex = contour[(vertexIndex + 1) % contour.length];", + " if (vertex.region !== RasterizationCell.OBSTACLE_REGION_ID &&", + " nextVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {", + " // This is a vertex in the middle. It must be removed.", + " // Search the contours around the vertex.", + " //", + " // Typically a contour point to its neighbor and it form a cycle.", + " //", + " // \\ C /", + " // \\ /", + " // A | B", + " // |", + " //", + " // C -> B -> A -> C", + " //", + " // There can be more than 3 contours even if it's rare.", + " commonVertexContours.length = 0;", + " commonVertexIndexes.length = 0;", + " commonVertexContours.push(contour);", + " commonVertexIndexes.push(vertexIndex);", + " var errorFound = false;", + " var commonVertex = vertex;", + " do {", + " var neighborContour = contoursByRegion[commonVertex.region];", + " if (!neighborContour) {", + " errorFound = true;", + " if (commonVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {", + " console.warn(\"contour already discarded: \" + commonVertex.region);", + " }", + " break;", + " }", + " var foundVertex = false;", + " for (var neighborVertexIndex = 0; neighborVertexIndex < neighborContour.length; neighborVertexIndex++) {", + " var neighborVertex = neighborContour[neighborVertexIndex];", + " if (neighborVertex.x === commonVertex.x &&", + " neighborVertex.y === commonVertex.y) {", + " commonVertexContours.push(neighborContour);", + " commonVertexIndexes.push(neighborVertexIndex);", + " commonVertex = neighborVertex;", + " foundVertex = true;", + " break;", + " }", + " }", + " if (!foundVertex) {", + " errorFound = true;", + " console.error(\"Can't find a common vertex with a neighbor contour. There is probably a superposition.\");", + " break;", + " }", + " } while (commonVertex !== vertex);", + " if (errorFound) {", + " continue;", + " }", + " if (commonVertexContours.length < 3) {", + " console.error(\"The vertex is shared by only \" + commonVertexContours.length + \" regions.\");", + " }", + " var shorterEdgeContourIndex = -1;", + " var edgeLengthMin = Number.MAX_VALUE;", + " for (var index = 0; index < commonVertexContours.length; index++) {", + " var vertexContour = commonVertexContours[index];", + " var vertexIndex_1 = commonVertexIndexes[index];", + " var previousVertex = vertexContour[(vertexIndex_1 - 1 + vertexContour.length) %", + " vertexContour.length];", + " if (previousVertex.region === RasterizationCell.OBSTACLE_REGION_ID) {", + " var deltaX = previousVertex.x - vertex.x;", + " var deltaY = previousVertex.y - vertex.y;", + " var lengthSq = deltaX * deltaX + deltaY * deltaY;", + " if (lengthSq < edgeLengthMin) {", + " edgeLengthMin = lengthSq;", + " shorterEdgeContourIndex = index;", + " }", + " }", + " }", + " if (shorterEdgeContourIndex === -1) {", + " // A vertex has no neighbor on an obstacle.", + " // It will be solved in next iterations.", + " continue;", + " }", + " // Merge the vertex on the other extremity of the smallest of the 3 edges.", + " //", + " // \\ C /", + " // \\ /", + " // A | B", + " // |", + " //", + " // - the shortest edge is between A and B", + " // - the Y will become a V", + " // - vertices are store clockwise", + " // - there can be more than one C (it's rare)", + " // This is B", + " var shorterEdgeContour = commonVertexContours[shorterEdgeContourIndex];", + " var shorterEdgeVertexIndex = commonVertexIndexes[shorterEdgeContourIndex];", + " var shorterEdgeExtremityVertex = shorterEdgeContour[(shorterEdgeVertexIndex - 1 + shorterEdgeContour.length) %", + " shorterEdgeContour.length];", + " // This is A", + " var shorterEdgeOtherContourIndex = (shorterEdgeContourIndex + 1) % commonVertexContours.length;", + " var shorterEdgeOtherContour = commonVertexContours[shorterEdgeOtherContourIndex];", + " var shorterEdgeOtherVertexIndex = commonVertexIndexes[shorterEdgeOtherContourIndex];", + " for (var index = 0; index < commonVertexContours.length; index++) {", + " if (index === shorterEdgeContourIndex ||", + " index === shorterEdgeOtherContourIndex) {", + " continue;", + " }", + " // These are C", + " var commonVertexContour = commonVertexContours[index];", + " var commonVertexIndex = commonVertexIndexes[index];", + " // Move the vertex to an obstacle border", + " var movedVertex = commonVertexContour[commonVertexIndex];", + " movedVertex.x = shorterEdgeExtremityVertex.x;", + " movedVertex.y = shorterEdgeExtremityVertex.y;", + " movedVertex.region = RasterizationCell.NULL_REGION_ID;", + " }", + " // There is no more border between A and B,", + " // update the region from B to C.", + " shorterEdgeOtherContour[(shorterEdgeOtherVertexIndex + 1) % shorterEdgeOtherContour.length].region =", + " shorterEdgeOtherContour[shorterEdgeOtherVertexIndex].region;", + " // Remove in A and B the vertex that's been move in C.", + " shorterEdgeContour.splice(shorterEdgeVertexIndex, 1);", + " shorterEdgeOtherContour.splice(shorterEdgeOtherVertexIndex, 1);", + " movedAnyVertex = true;", + " }", + " }", + " }", + " } while (movedAnyVertex);", + " // Clean the polygons from identical vertices.", + " //", + " // This can happen with 2 vertices regions.", + " // 2 edges are superposed and there extremity is the same.", + " // One is move over the other.", + " // I could observe this with a region between 2 regions", + " // where one of one of these 2 regions were also encompassed.", + " // A bit like a rainbow, 2 big regions: the land, the sky", + " // and 2 regions for the colors.", + " //", + " // The vertex can't be removed during the process because", + " // they hold data used by other merging.", + " //", + " // Some contour will have no vertex left.", + " // It more efficient to let the next step ignore them.", + " for (var _a = 0, contours_2 = contours; _a < contours_2.length; _a++) {", + " var contour = contours_2[_a];", + " for (var vertexIndex = 0; vertexIndex < contour.length; vertexIndex++) {", + " var vertex = contour[vertexIndex];", + " var nextVertexIndex = (vertexIndex + 1) % contour.length;", + " var nextVertex = contour[nextVertexIndex];", + " if (vertex.x === nextVertex.x && vertex.y === nextVertex.y) {", + " contour.splice(nextVertexIndex, 1);", + " vertexIndex--;", + " }", + " }", + " }", + " };", + " /**", + " * Walk around the edge of this cell's region gathering vertices that", + " * represent the corners of each cell on the sides that are external facing.", + " *", + " * There will be two or three vertices for each edge cell:", + " * Two for cells that don't represent a change in edge direction. Three", + " * for cells that represent a change in edge direction.", + " *", + " * The output array will contain vertices ordered as follows:", + " * (x, y, z, regionID) where regionID is the region (obstacle or real) that", + " * this vertex is considered to be connected to.", + " *", + " * WARNING: Only run this operation on cells that are already known", + " * to be on a region edge. The direction must also be pointing to a", + " * valid edge. Otherwise behavior will be undefined.", + " *", + " * @param grid the grid of cells", + " * @param startCell A cell that is known to be on the edge of a region", + " * (part of a region contour).", + " * @param startDirection The direction of the edge of the cell that is", + " * known to point", + " * across the region edge.", + " * @param outContourVertices The list of vertices that represent the edge", + " * of the region.", + " */", + " ContourBuilder.prototype.buildRawContours = function (grid, startCell, startDirection, outContourVertices) {", + " // Flaw in Algorithm:", + " //", + " // This method of contour generation can result in an inappropriate", + " // impassable seam between two adjacent regions in the following case:", + " //", + " // 1. One region connects to another region on two sides in an", + " // uninterrupted manner (visualize one region wrapping in an L", + " // shape around the corner of another).", + " // 2. At the corner shared by the two regions, a change in height", + " // occurs.", + " //", + " // In this case, the two regions should share a corner vertex", + " // (an obtuse corner vertex for one region and an acute corner", + " // vertex for the other region).", + " //", + " // In reality, though this algorithm will select the same (x, z)", + " // coordinates for each region's corner vertex, the vertex heights", + " // may differ, eventually resulting in an impassable seam.", + " // It is a bit hard to describe the stepping portion of this algorithm.", + " // One way to visualize it is to think of a robot sitting on the", + " // floor facing a known wall. It then does the following to skirt", + " // the wall:", + " // 1. If there is a wall in front of it, turn clockwise in 90 degrees", + " // increments until it finds the wall is gone.", + " // 2. Move forward one step.", + " // 3. Turn counter-clockwise by 90 degrees.", + " // 4. Repeat from step 1 until it finds itself at its original", + " // location facing its original direction.", + " //", + " // See also: http://www.critterai.org/projects/nmgen_study/contourgen.html#robotwalk", + " var cell = startCell;", + " var direction = startDirection;", + " var loopCount = 0;", + " do {", + " // Note: The design of this loop is such that the cell variable", + " // will always reference an edge cell from the same region as", + " // the start cell.", + " if ((cell.contourFlags & (1 << direction)) !== 0) {", + " // The current direction is pointing toward an edge.", + " // Get this edge's vertex.", + " var delta = ContourBuilder.leftVertexOfFacingCellBorderDeltas[direction];", + " var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);", + " outContourVertices.push({", + " x: cell.x + delta.x,", + " y: cell.y + delta.y,", + " region: neighbor.regionID,", + " });", + " // Remove the flag for this edge. We never need to consider", + " // it again since we have a vertex for this edge.", + " cell.contourFlags &= ~(1 << direction);", + " // Rotate in clockwise direction.", + " direction = (direction + 1) & 0x3;", + " }", + " else {", + " // The current direction does not point to an edge. So it", + " // must point to a neighbor cell in the same region as the", + " // current cell. Move to the neighbor and swing the search", + " // direction back one increment (counterclockwise).", + " // By moving the direction back one increment we guarantee we", + " // don't miss any edges.", + " var neighbor = grid.get(cell.x + RasterizationGrid.neighbor4Deltas[direction].x, cell.y + RasterizationGrid.neighbor4Deltas[direction].y);", + " cell = neighbor;", + " direction = (direction + 3) & 0x3; // Rotate counterclockwise.", + " }", + " // The loop limit is arbitrary. It exists only to guarantee that", + " // bad input data doesn't result in an infinite loop.", + " // The only down side of this loop limit is that it limits the", + " // number of detectable edge vertices (the longer the region edge", + " // and the higher the number of \"turns\" in a region's edge, the less", + " // edge vertices can be detected for that region).", + " } while (!(cell === startCell && direction === startDirection) &&", + " ++loopCount < 65535);", + " return outContourVertices;", + " };", + " /**", + " * Takes a group of vertices that represent a region contour and changes", + " * it in the following manner:", + " * - For any edges that connect to non-obstacle regions, remove all", + " * vertices except the start and end vertices for that edge (this", + " * smooths the edges between non-obstacle regions into a straight line).", + " * - Runs an algorithm's against the contour to follow the edge more closely.", + " *", + " * @param regionID The region the contour was derived from.", + " * @param sourceVertices The source vertices that represent the complex", + " * contour.", + " * @param outVertices The simplified contour vertices.", + " * @param threshold The maximum distance the edge of the contour may deviate", + " * from the source geometry.", + " */", + " ContourBuilder.prototype.generateSimplifiedContour = function (regionID, sourceVertices, outVertices, threshold) {", + " var noConnections = true;", + " for (var _i = 0, sourceVertices_1 = sourceVertices; _i < sourceVertices_1.length; _i++) {", + " var sourceVertex = sourceVertices_1[_i];", + " if (sourceVertex.region !== RasterizationCell.OBSTACLE_REGION_ID) {", + " noConnections = false;", + " break;", + " }", + " }", + " // Seed the simplified contour with the mandatory edges", + " // (At least one edge).", + " if (noConnections) {", + " // This contour represents an island region surrounded only by the", + " // obstacle region. Seed the simplified contour with the source's", + " // lower left (ll) and upper right (ur) vertices.", + " var lowerLeftX = sourceVertices[0].x;", + " var lowerLeftY = sourceVertices[0].y;", + " var lowerLeftIndex = 0;", + " var upperRightX = sourceVertices[0].x;", + " var upperRightY = sourceVertices[0].y;", + " var upperRightIndex = 0;", + " for (var index = 0; index < sourceVertices.length; index++) {", + " var sourceVertex = sourceVertices[index];", + " var x = sourceVertex.x;", + " var y = sourceVertex.y;", + " if (x < lowerLeftX || (x === lowerLeftX && y < lowerLeftY)) {", + " lowerLeftX = x;", + " lowerLeftY = y;", + " lowerLeftIndex = index;", + " }", + " if (x >= upperRightX || (x === upperRightX && y > upperRightY)) {", + " upperRightX = x;", + " upperRightY = y;", + " upperRightIndex = index;", + " }", + " }", + " // The region attribute is used to store an index locally in this function.", + " // TODO Maybe there is a way to do this cleanly and keep no memory footprint.", + " // Seed the simplified contour with this edge.", + " outVertices.push({", + " x: lowerLeftX,", + " y: lowerLeftY,", + " region: lowerLeftIndex,", + " });", + " outVertices.push({", + " x: upperRightX,", + " y: upperRightY,", + " region: upperRightIndex,", + " });", + " }", + " else {", + " // The contour shares edges with other non-obstacle regions.", + " // Seed the simplified contour with a new vertex for every", + " // location where the region connection changes. These are", + " // vertices that are important because they represent portals", + " // to other regions.", + " for (var index = 0; index < sourceVertices.length; index++) {", + " var sourceVert = sourceVertices[index];", + " if (sourceVert.region !==", + " sourceVertices[(index + 1) % sourceVertices.length].region) {", + " // The current vertex has a different region than the", + " // next vertex. So there is a change in vertex region.", + " outVertices.push({", + " x: sourceVert.x,", + " y: sourceVert.y,", + " region: index,", + " });", + " }", + " }", + " }", + " this.matchObstacleRegionEdges(sourceVertices, outVertices, threshold);", + " if (outVertices.length < 2) {", + " // It will be ignored by the triangulation.", + " // It should be rare enough not to handle it now.", + " console.warn(\"A region is encompassed in another region. It will be ignored.\");", + " }", + " // There can be polygons with only 2 vertices when a region is between", + " // 2 non-obstacles regions. It's still a useful information to filter", + " // vertices in the middle (not on an obstacle region border).", + " // In this case, the CritterAI implementation adds a 3rd point to avoid", + " // invisible polygons, but it makes it difficult to filter it later.", + " // Replace the index pointers in the output list with region IDs.", + " for (var _a = 0, outVertices_1 = outVertices; _a < outVertices_1.length; _a++) {", + " var outVertex = outVertices_1[_a];", + " outVertex.region = sourceVertices[outVertex.region].region;", + " }", + " };", + " /**", + " * Applies an algorithm to contours which results in obstacle-region edges", + " * following the original detail source geometry edge more closely.", + " * http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple", + " *", + " * Adds vertices from the source list to the result list such that", + " * if any obstacle region vertices are compared against the result list,", + " * none of the vertices will be further from the obstacle region edges than", + " * the allowed threshold.", + " *", + " * Only obstacle-region edges are operated on. All other edges are", + " * ignored.", + " *", + " * The result vertices is expected to be seeded with at least two", + " * source vertices.", + " *", + " * @param sourceVertices", + " * @param inoutResultVertices", + " * @param threshold The maximum distance the edge of the contour may deviate", + " * from the source geometry.", + " */", + " ContourBuilder.prototype.matchObstacleRegionEdges = function (sourceVertices, inoutResultVertices, threshold) {", + " // This implementation is strongly inspired from CritterAI class \"MatchNullRegionEdges\".", + " // Loop through all edges in this contour.", + " //", + " // NOTE: The simplifiedVertCount in the loop condition", + " // increases over iterations. That is what keeps the loop going beyond", + " // the initial vertex count.", + " var resultIndexA = 0;", + " while (resultIndexA < inoutResultVertices.length) {", + " var resultIndexB = (resultIndexA + 1) % inoutResultVertices.length;", + " // The line segment's beginning vertex.", + " var ax = inoutResultVertices[resultIndexA].x;", + " var az = inoutResultVertices[resultIndexA].y;", + " var sourceIndexA = inoutResultVertices[resultIndexA].region;", + " // The line segment's ending vertex.", + " var bx = inoutResultVertices[resultIndexB].x;", + " var bz = inoutResultVertices[resultIndexB].y;", + " var sourceIndexB = inoutResultVertices[resultIndexB].region;", + " // The source index of the next vertex to test (the vertex just", + " // after the current vertex in the source vertex list).", + " var testedSourceIndex = (sourceIndexA + 1) % sourceVertices.length;", + " var maxDeviation = 0;", + " // Default to no index. No new vert to add.", + " var toInsertSourceIndex = -1;", + " if (sourceVertices[testedSourceIndex].region ===", + " RasterizationCell.OBSTACLE_REGION_ID) {", + " // This test vertex is part of a obstacle region edge.", + " // Loop through the source vertices until the end vertex", + " // is found, searching for the vertex that is farthest from", + " // the line segment formed by the begin/end vertices.", + " //", + " // Visualizations:", + " // http://www.critterai.org/projects/nmgen_study/contourgen.html#nulledgesimple", + " while (testedSourceIndex !== sourceIndexB) {", + " var deviation = Geometry.getPointSegmentDistanceSq(sourceVertices[testedSourceIndex].x, sourceVertices[testedSourceIndex].y, ax, az, bx, bz);", + " if (deviation > maxDeviation) {", + " // A new maximum deviation was detected.", + " maxDeviation = deviation;", + " toInsertSourceIndex = testedSourceIndex;", + " }", + " // Move to the next vertex.", + " testedSourceIndex = (testedSourceIndex + 1) % sourceVertices.length;", + " }", + " }", + " if (toInsertSourceIndex !== -1 && maxDeviation > threshold * threshold) {", + " // A vertex was found that is further than allowed from the", + " // current edge. Add this vertex to the contour.", + " inoutResultVertices.splice(resultIndexA + 1, 0, {", + " x: sourceVertices[toInsertSourceIndex].x,", + " y: sourceVertices[toInsertSourceIndex].y,", + " region: toInsertSourceIndex,", + " });", + " // Not incrementing the vertex since we need to test the edge", + " // formed by vertA and this this new vertex on the next", + " // iteration of the loop.", + " }", + " // This edge segment does not need to be altered. Move to", + " // the next vertex.", + " else", + " resultIndexA++;", + " }", + " };", + " ContourBuilder.leftVertexOfFacingCellBorderDeltas = [", + " { x: 0, y: 1 },", + " { x: 1, y: 1 },", + " { x: 1, y: 0 },", + " { x: 0, y: 0 },", + " ];", + " return ContourBuilder;", + "}());", + "", + "/**", + " * Builds convex polygons from the provided polygons.", + " *", + " * This implementation is strongly inspired from CritterAI class \"PolyMeshFieldBuilder\".", + " * http://www.critterai.org/projects/nmgen_study/polygen.html", + " */", + "var ConvexPolygonGenerator = /** @class */ (function () {", + " function ConvexPolygonGenerator() {", + " }", + " /**", + " * Builds convex polygons from the provided polygons.", + " * @param concavePolygons The content is manipulated during the operation", + " * and it will be left in an undefined state at the end of", + " * the operation.", + " * @param maxVerticesPerPolygon cap the vertex number in return polygons.", + " * @return convex polygons.", + " */", + " ConvexPolygonGenerator.prototype.splitToConvexPolygons = function (concavePolygons, maxVerticesPerPolygon) {", + " // The maximum possible number of polygons assuming that all will", + " // be triangles.", + " var maxPossiblePolygons = 0;", + " // The maximum vertices found in a single contour.", + " var maxVerticesPerContour = 0;", + " for (var _i = 0, concavePolygons_1 = concavePolygons; _i < concavePolygons_1.length; _i++) {", + " var contour = concavePolygons_1[_i];", + " var count = contour.length;", + " maxPossiblePolygons += count - 2;", + " maxVerticesPerContour = Math.max(maxVerticesPerContour, count);", + " }", + " // Each list is initialized to a size that will minimize resizing.", + " var convexPolygons = new Array(maxPossiblePolygons);", + " convexPolygons.length = 0;", + " // Various working variables.", + " // (Values are meaningless outside of the iteration)", + " var workingContourFlags = new Array(maxVerticesPerContour);", + " workingContourFlags.length = 0;", + " var workingPolygons = new Array(maxVerticesPerContour + 1);", + " workingPolygons.length = 0;", + " var workingMergeInfo = {", + " lengthSq: -1,", + " polygonAVertexIndex: -1,", + " polygonBVertexIndex: -1,", + " };", + " var workingMergedPolygon = new Array(maxVerticesPerPolygon);", + " workingMergedPolygon.length = 0;", + " var _loop_1 = function (contour) {", + " if (contour.length < 3) {", + " return \"continue\";", + " }", + " // Initialize the working polygon array.", + " workingPolygons.length = 0;", + " // Triangulate the contour.", + " var foundAnyTriangle = false;", + " this_1.triangulate(contour, workingContourFlags, function (p1, p2, p3) {", + " var workingPolygon = new Array(maxVerticesPerPolygon);", + " workingPolygon.length = 0;", + " workingPolygon.push(p1);", + " workingPolygon.push(p2);", + " workingPolygon.push(p3);", + " workingPolygons.push(workingPolygon);", + " foundAnyTriangle = true;", + " });", + " if (!foundAnyTriangle) {", + " /*", + " * Failure of the triangulation.", + " * This is known to occur if the source polygon is", + " * self-intersecting or the source region contains internal", + " * holes. In both cases, the problem is likely due to bad", + " * region formation.", + " */", + " console.error(\"Polygon generation failure: Could not triangulate contour.\");", + " console.error(\"contour:\" +", + " contour.map(function (point) { return point.x + \" \" + point.y; }).join(\" ; \"));", + " return \"continue\";", + " }", + " if (maxVerticesPerPolygon > 3) {", + " // Merging of triangles into larger polygons is permitted.", + " // Continue until no polygons can be found to merge.", + " // http://www.critterai.org/nmgen_polygen#mergepolys", + " while (true) {", + " var longestMergeEdge = -1;", + " var bestPolygonA = [];", + " var polygonAVertexIndex = -1; // Start of the shared edge.", + " var bestPolygonB = [];", + " var polygonBVertexIndex = -1; // Start of the shared edge.", + " var bestPolygonBIndex = -1;", + " // Loop through all but the last polygon looking for the", + " // best polygons to merge in this iteration.", + " for (var indexA = 0; indexA < workingPolygons.length - 1; indexA++) {", + " var polygonA = workingPolygons[indexA];", + " for (var indexB = indexA + 1; indexB < workingPolygons.length; indexB++) {", + " var polygonB = workingPolygons[indexB];", + " // Can polyB merge with polyA?", + " this_1.getPolyMergeInfo(polygonA, polygonB, maxVerticesPerPolygon, workingMergeInfo);", + " if (workingMergeInfo.lengthSq > longestMergeEdge) {", + " // polyB has the longest shared edge with", + " // polyA found so far. Save the merge", + " // information.", + " longestMergeEdge = workingMergeInfo.lengthSq;", + " bestPolygonA = polygonA;", + " polygonAVertexIndex = workingMergeInfo.polygonAVertexIndex;", + " bestPolygonB = polygonB;", + " polygonBVertexIndex = workingMergeInfo.polygonBVertexIndex;", + " bestPolygonBIndex = indexB;", + " }", + " }", + " }", + " if (longestMergeEdge <= 0)", + " // No valid merges found during this iteration.", + " break;", + " // Found polygons to merge. Perform the merge.", + " /*", + " * Fill the mergedPoly array.", + " * Start the vertex at the end of polygon A's shared edge.", + " * Add all vertices until looping back to the vertex just", + " * before the start of the shared edge. Repeat for", + " * polygon B.", + " *", + " * Duplicate vertices are avoided, while ensuring we get", + " * all vertices, since each loop drops the vertex that", + " * starts its polygon's shared edge and:", + " *", + " * PolyAStartVert == PolyBEndVert and", + " * PolyAEndVert == PolyBStartVert.", + " */", + " var vertCountA = bestPolygonA.length;", + " var vertCountB = bestPolygonB.length;", + " workingMergedPolygon.length = 0;", + " for (var i = 0; i < vertCountA - 1; i++)", + " workingMergedPolygon.push(bestPolygonA[(polygonAVertexIndex + 1 + i) % vertCountA]);", + " for (var i = 0; i < vertCountB - 1; i++)", + " workingMergedPolygon.push(bestPolygonB[(polygonBVertexIndex + 1 + i) % vertCountB]);", + " // Copy the merged polygon over the top of polygon A.", + " bestPolygonA.length = 0;", + " Array.prototype.push.apply(bestPolygonA, workingMergedPolygon);", + " // Remove polygon B", + " workingPolygons.splice(bestPolygonBIndex, 1);", + " }", + " }", + " // Polygon creation for this contour is complete.", + " // Add polygons to the global polygon array", + " Array.prototype.push.apply(convexPolygons, workingPolygons);", + " };", + " var this_1 = this;", + " // Split every concave polygon into convex polygons.", + " for (var _a = 0, concavePolygons_2 = concavePolygons; _a < concavePolygons_2.length; _a++) {", + " var contour = concavePolygons_2[_a];", + " _loop_1(contour);", + " }", + " // The original implementation builds polygon adjacency information.", + " // but the library for the pathfinding already does it.", + " return convexPolygons;", + " };", + " /**", + " * Checks two polygons to see if they can be merged. If a merge is", + " * allowed, provides data via the outResult argument (see {@link PolyMergeResult}).", + " *", + " * @param polygonA The polygon A", + " * @param polygonB The polygon B", + " * @param maxVerticesPerPolygon cap the vertex number in return polygons.", + " * @param outResult contains merge information.", + " */", + " ConvexPolygonGenerator.prototype.getPolyMergeInfo = function (polygonA, polygonB, maxVerticesPerPolygon, outResult) {", + " outResult.lengthSq = -1; // Default to invalid merge", + " outResult.polygonAVertexIndex = -1;", + " outResult.polygonBVertexIndex = -1;", + " var vertexCountA = polygonA.length;", + " var vertexCountB = polygonB.length;", + " // If the merged polygon would would have to many vertices, do not", + " // merge. Subtracting two since to take into account the effect of", + " // a merge.", + " if (vertexCountA + vertexCountB - 2 > maxVerticesPerPolygon)", + " return;", + " // Check if the polygons share an edge.", + " for (var indexA = 0; indexA < vertexCountA; indexA++) {", + " // Get the vertex indices for the polygonA edge", + " var vertexA = polygonA[indexA];", + " var nextVertexA = polygonA[(indexA + 1) % vertexCountA];", + " // Search polygonB for matches.", + " for (var indexB = 0; indexB < vertexCountB; indexB++) {", + " // Get the vertex indices for the polygonB edge.", + " var vertexB = polygonB[indexB];", + " var nextVertexB = polygonB[(indexB + 1) % vertexCountB];", + " // === can be used because vertices comme from the same concave polygon.", + " if (vertexA === nextVertexB && nextVertexA === vertexB) {", + " // The vertex indices for this edge are the same and", + " // sequenced in opposite order. So the edge is shared.", + " outResult.polygonAVertexIndex = indexA;", + " outResult.polygonBVertexIndex = indexB;", + " }", + " }", + " }", + " if (outResult.polygonAVertexIndex === -1)", + " // No common edge, cannot merge.", + " return;", + " // Check to see if the merged polygon would be convex.", + " //", + " // Gets the vertices near the section where the merge would occur.", + " // Do they form a concave section? If so, the merge is invalid.", + " //", + " // Note that the following algorithm is only valid for clockwise", + " // wrapped convex polygons.", + " var sharedVertMinus = polygonA[(outResult.polygonAVertexIndex - 1 + vertexCountA) % vertexCountA];", + " var sharedVert = polygonA[outResult.polygonAVertexIndex];", + " var sharedVertPlus = polygonB[(outResult.polygonBVertexIndex + 2) % vertexCountB];", + " if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {", + " // The shared vertex (center) is not to the left of segment", + " // vertMinus->vertPlus. For a clockwise wrapped polygon, this", + " // indicates a concave section. Merged polygon would be concave.", + " // Invalid merge.", + " return;", + " }", + " sharedVertMinus =", + " polygonB[(outResult.polygonBVertexIndex - 1 + vertexCountB) % vertexCountB];", + " sharedVert = polygonB[outResult.polygonBVertexIndex];", + " sharedVertPlus =", + " polygonA[(outResult.polygonAVertexIndex + 2) % vertexCountA];", + " if (!ConvexPolygonGenerator.isLeft(sharedVert.x, sharedVert.y, sharedVertMinus.x, sharedVertMinus.y, sharedVertPlus.x, sharedVertPlus.y)) {", + " // The shared vertex (center) is not to the left of segment", + " // vertMinus->vertPlus. For a clockwise wrapped polygon, this", + " // indicates a concave section. Merged polygon would be concave.", + " // Invalid merge.", + " return;", + " }", + " // Get the vertex indices that form the shared edge.", + " sharedVertMinus = polygonA[outResult.polygonAVertexIndex];", + " sharedVert = polygonA[(outResult.polygonAVertexIndex + 1) % vertexCountA];", + " // Store the lengthSq of the shared edge.", + " var deltaX = sharedVertMinus.x - sharedVert.x;", + " var deltaZ = sharedVertMinus.y - sharedVert.y;", + " outResult.lengthSq = deltaX * deltaX + deltaZ * deltaZ;", + " };", + " /**", + " * Attempts to triangulate a polygon.", + " *", + " * @param vertices the polygon to be triangulate.", + " * The content is manipulated during the operation", + " * and it will be left in an undefined state at the end of", + " * the operation.", + " * @param vertexFlags only used internally", + " * @param outTriangles is called for each triangle derived", + " * from the original polygon.", + " * @return The number of triangles generated. Or, if triangulation", + " * failed, a negative number.", + " */", + " ConvexPolygonGenerator.prototype.triangulate = function (vertices, vertexFlags, outTriangles) {", + " // Terminology, concepts and such:", + " //", + " // This algorithm loops around the edges of a polygon looking for", + " // new internal edges to add that will partition the polygon into a", + " // new valid triangle internal to the starting polygon. During each", + " // iteration the shortest potential new edge is selected to form that", + " // iteration's new triangle.", + " //", + " // Triangles will only be formed if a single new edge will create", + " // a triangle. Two new edges will never be added during a single", + " // iteration. This means that the triangulated portions of the", + " // original polygon will only contain triangles and the only", + " // non-triangle polygon will exist in the untriangulated portion", + " // of the original polygon.", + " //", + " // \"Partition edge\" refers to a potential new edge that will form a", + " // new valid triangle.", + " //", + " // \"Center\" vertex refers to the vertex in a potential new triangle", + " // which, if the triangle is formed, will be external to the", + " // remaining untriangulated portion of the polygon. Since it", + " // is now external to the polygon, it can't be used to form any", + " // new triangles.", + " //", + " // Some documentation refers to \"iPlus2\" even though the variable is", + " // not in scope or does not exist for that section of code. For", + " // documentation purposes, iPlus2 refers to the 2nd vertex after the", + " // primary vertex.", + " // E.g.: i, iPlus1, and iPlus2.", + " //", + " // Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#triangulation", + " // Loop through all vertices, flagging all indices that represent", + " // a center vertex of a valid new triangle.", + " vertexFlags.length = vertices.length;", + " for (var i = 0; i < vertices.length; i++) {", + " var iPlus1 = (i + 1) % vertices.length;", + " var iPlus2 = (i + 2) % vertices.length;", + " // A triangle formed by i, iPlus1, and iPlus2 will result", + " // in a valid internal triangle.", + " // Flag the center vertex (iPlus1) to indicate a valid triangle", + " // location.", + " vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, iPlus2, vertices);", + " }", + " // Loop through the vertices creating triangles. When there is only a", + " // single triangle left, the operation is complete.", + " //", + " // When a valid triangle is formed, remove its center vertex. So for", + " // each loop, a single vertex will be removed.", + " //", + " // At the start of each iteration the indices list is in the following", + " // state:", + " // - Represents a simple polygon representing the un-triangulated", + " // portion of the original polygon.", + " // - All valid center vertices are flagged.", + " while (vertices.length > 3) {", + " // Find the shortest new valid edge.", + " // NOTE: i and iPlus1 are defined in two different scopes in", + " // this section. So be careful.", + " // Loop through all indices in the remaining polygon.", + " var minLengthSq = Number.MAX_VALUE;", + " var minLengthSqVertexIndex = -1;", + " for (var i_1 = 0; i_1 < vertices.length; i_1++) {", + " if (vertexFlags[(i_1 + 1) % vertices.length]) {", + " // Indices i, iPlus1, and iPlus2 are known to form a", + " // valid triangle.", + " var vert = vertices[i_1];", + " var vertPlus2 = vertices[(i_1 + 2) % vertices.length];", + " // Determine the length of the partition edge.", + " // (i -> iPlus2)", + " var deltaX = vertPlus2.x - vert.x;", + " var deltaY = vertPlus2.y - vert.y;", + " var lengthSq = deltaX * deltaX + deltaY * deltaY;", + " if (lengthSq < minLengthSq) {", + " minLengthSq = lengthSq;", + " minLengthSqVertexIndex = i_1;", + " }", + " }", + " }", + " if (minLengthSqVertexIndex === -1)", + " // Could not find a new triangle. Triangulation failed.", + " // This happens if there are three or more vertices", + " // left, but none of them are flagged as being a", + " // potential center vertex.", + " return;", + " var i = minLengthSqVertexIndex;", + " var iPlus1 = (i + 1) % vertices.length;", + " // Add the new triangle to the output.", + " outTriangles(vertices[i], vertices[iPlus1], vertices[(i + 2) % vertices.length]);", + " // iPlus1, the \"center\" vert in the new triangle, is now external", + " // to the untriangulated portion of the polygon. Remove it from", + " // the vertices list since it cannot be a member of any new", + " // triangles.", + " vertices.splice(iPlus1, 1);", + " vertexFlags.splice(iPlus1, 1);", + " if (iPlus1 === 0 || iPlus1 >= vertices.length) {", + " // The vertex removal has invalidated iPlus1 and/or i. So", + " // force a wrap, fixing the indices so they reference the", + " // correct indices again. This only occurs when the new", + " // triangle is formed across the wrap location of the polygon.", + " // Case 1: i = 14, iPlus1 = 15, iPlus2 = 0", + " // Case 2: i = 15, iPlus1 = 0, iPlus2 = 1;", + " i = vertices.length - 1;", + " iPlus1 = 0;", + " }", + " // At this point i and iPlus1 refer to the two indices from a", + " // successful triangulation that will be part of another new", + " // triangle. We now need to re-check these indices to see if they", + " // can now be the center index in a potential new partition.", + " vertexFlags[i] = ConvexPolygonGenerator.isValidPartition((i - 1 + vertices.length) % vertices.length, iPlus1, vertices);", + " vertexFlags[iPlus1] = ConvexPolygonGenerator.isValidPartition(i, (i + 2) % vertices.length, vertices);", + " }", + " // Only 3 vertices remain.", + " // Add their triangle to the output list.", + " outTriangles(vertices[0], vertices[1], vertices[2]);", + " };", + " /**", + " * Check if the line segment formed by vertex A and vertex B will", + " * form a valid partition of the polygon.", + " *", + " * I.e. the line segment AB is internal to the polygon and will not", + " * cross existing line segments.", + " *", + " * Assumptions:", + " * - The vertices arguments define a valid simple polygon", + " * with vertices wrapped clockwise.", + " * - indexA != indexB", + " *", + " * Behavior is undefined if the arguments to not meet these", + " * assumptions", + " *", + " * @param indexA the index of the vertex that will form the segment AB.", + " * @param indexB the index of the vertex that will form the segment AB.", + " * @param vertices a polygon wrapped clockwise.", + " * @return true if the line segment formed by vertex A and vertex B will", + " * form a valid partition of the polygon.", + " */", + " ConvexPolygonGenerator.isValidPartition = function (indexA, indexB, vertices) {", + " // First check whether the segment AB lies within the internal", + " // angle formed at A (this is the faster check).", + " // If it does, then perform the more costly check.", + " return (ConvexPolygonGenerator.liesWithinInternalAngle(indexA, indexB, vertices) &&", + " !ConvexPolygonGenerator.hasIllegalEdgeIntersection(indexA, indexB, vertices));", + " };", + " /**", + " * Check if vertex B lies within the internal angle of the polygon", + " * at vertex A.", + " *", + " * Vertex B does not have to be within the polygon border. It just has", + " * be be within the area encompassed by the internal angle formed at", + " * vertex A.", + " *", + " * This operation is a fast way of determining whether a line segment", + " * can possibly form a valid polygon partition. If this test returns", + " * FALSE, then more expensive checks can be skipped.", + " *", + " * Visualizations: http://www.critterai.org/projects/nmgen_study/polygen.html#anglecheck", + " *", + " * Special case:", + " * FALSE is returned if vertex B lies directly on either of the rays", + " * cast from vertex A along its associated polygon edges. So the test", + " * on vertex B is exclusive of the polygon edges.", + " *", + " * Assumptions:", + " * - The vertices and indices arguments define a valid simple polygon", + " * with vertices wrapped clockwise.", + " * -indexA != indexB", + " *", + " * Behavior is undefined if the arguments to not meet these", + " * assumptions", + " *", + " * @param indexA the index of the vertex that will form the segment AB.", + " * @param indexB the index of the vertex that will form the segment AB.", + " * @param vertices a polygon wrapped clockwise.", + " * @return true if vertex B lies within the internal angle of", + " * the polygon at vertex A.", + " */", + " ConvexPolygonGenerator.liesWithinInternalAngle = function (indexA, indexB, vertices) {", + " // Get pointers to the main vertices being tested.", + " var vertexA = vertices[indexA];", + " var vertexB = vertices[indexB];", + " // Get pointers to the vertices just before and just after vertA.", + " var vertexAMinus = vertices[(indexA - 1 + vertices.length) % vertices.length];", + " var vertexAPlus = vertices[(indexA + 1) % vertices.length];", + " // First, find which of the two angles formed by the line segments", + " // AMinus->A->APlus is internal to (pointing towards) the polygon.", + " // Then test to see if B lies within the area formed by that angle.", + " // TRUE if A is left of or on line AMinus->APlus", + " if (ConvexPolygonGenerator.isLeftOrCollinear(vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y, vertexAPlus.x, vertexAPlus.y))", + " // The angle internal to the polygon is <= 180 degrees", + " // (non-reflex angle).", + " // Test to see if B lies within this angle.", + " return (ConvexPolygonGenerator.isLeft(", + " // TRUE if B is left of line A->AMinus", + " vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y) &&", + " // TRUE if B is right of line A->APlus", + " ConvexPolygonGenerator.isRight(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y));", + " // The angle internal to the polygon is > 180 degrees (reflex angle).", + " // Test to see if B lies within the external (<= 180 degree) angle and", + " // flip the result. (If B lies within the external angle, it can't", + " // lie within the internal angle)", + " return !(", + " // TRUE if B is left of or on line A->APlus", + " (ConvexPolygonGenerator.isLeftOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAPlus.x, vertexAPlus.y) &&", + " // TRUE if B is right of or on line A->AMinus", + " ConvexPolygonGenerator.isRightOrCollinear(vertexB.x, vertexB.y, vertexA.x, vertexA.y, vertexAMinus.x, vertexAMinus.y)));", + " };", + " /**", + " * Check if the line segment AB intersects any edges not already", + " * connected to one of the two vertices.", + " *", + " * Assumptions:", + " * - The vertices and indices arguments define a valid simple polygon", + " * with vertices wrapped clockwise.", + " * - indexA != indexB", + " *", + " * Behavior is undefined if the arguments to not meet these", + " * assumptions", + " *", + " * @param indexA the index of the vertex that will form the segment AB.", + " * @param indexB the index of the vertex that will form the segment AB.", + " * @param vertices a polygon wrapped clockwise.", + " * @return true if the line segment AB intersects any edges not already", + " * connected to one of the two vertices.", + " */", + " ConvexPolygonGenerator.hasIllegalEdgeIntersection = function (indexA, indexB, vertices) {", + " // Get pointers to the primary vertices being tested.", + " var vertexA = vertices[indexA];", + " var vertexB = vertices[indexB];", + " // Loop through the polygon edges.", + " for (var edgeBeginIndex = 0; edgeBeginIndex < vertices.length; edgeBeginIndex++) {", + " var edgeEndIndex = (edgeBeginIndex + 1) % vertices.length;", + " if (edgeBeginIndex === indexA ||", + " edgeBeginIndex === indexB ||", + " edgeEndIndex === indexA ||", + " edgeEndIndex === indexB) {", + " continue;", + " }", + " // Neither of the test indices are endpoints of this edge.", + " // Get this edge's vertices.", + " var edgeBegin = vertices[edgeBeginIndex];", + " var edgeEnd = vertices[edgeEndIndex];", + " if ((edgeBegin.x === vertexA.x && edgeBegin.y === vertexA.y) ||", + " (edgeBegin.x === vertexB.x && edgeBegin.y === vertexB.y) ||", + " (edgeEnd.x === vertexA.x && edgeEnd.y === vertexA.y) ||", + " (edgeEnd.x === vertexB.x && edgeEnd.y === vertexB.y)) {", + " // One of the test vertices is co-located", + " // with one of the endpoints of this edge (this is a", + " // test of the actual position of the vertices rather than", + " // simply the index check performed earlier).", + " // Skip this edge.", + " continue;", + " }", + " // This edge is not connected to either of the test vertices.", + " // If line segment AB intersects with this edge, then the", + " // intersection is illegal.", + " // I.e. New edges cannot cross existing edges.", + " if (Geometry.segmentsIntersect(vertexA.x, vertexA.y, vertexB.x, vertexB.y, edgeBegin.x, edgeBegin.y, edgeEnd.x, edgeEnd.y)) {", + " return true;", + " }", + " }", + " return false;", + " };", + " /**", + " * Check if point P is to the left of line AB when looking", + " * from A to B.", + " * @param px The x-value of the point to test.", + " * @param py The y-value of the point to test.", + " * @param ax The x-value of the point (ax, ay) that is point A on line AB.", + " * @param ay The y-value of the point (ax, ay) that is point A on line AB.", + " * @param bx The x-value of the point (bx, by) that is point B on line AB.", + " * @param by The y-value of the point (bx, by) that is point B on line AB.", + " * @return TRUE if point P is to the left of line AB when looking", + " * from A to B.", + " */", + " ConvexPolygonGenerator.isLeft = function (px, py, ax, ay, bx, by) {", + " return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) < 0;", + " };", + " /**", + " * Check if point P is to the left of line AB when looking", + " * from A to B or is collinear with line AB.", + " * @param px The x-value of the point to test.", + " * @param py The y-value of the point to test.", + " * @param ax The x-value of the point (ax, ay) that is point A on line AB.", + " * @param ay The y-value of the point (ax, ay) that is point A on line AB.", + " * @param bx The x-value of the point (bx, by) that is point B on line AB.", + " * @param by The y-value of the point (bx, by) that is point B on line AB.", + " * @return TRUE if point P is to the left of line AB when looking", + " * from A to B, or is collinear with line AB.", + " */", + " ConvexPolygonGenerator.isLeftOrCollinear = function (px, py, ax, ay, bx, by) {", + " return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) <= 0;", + " };", + " /**", + " * Check if point P is to the right of line AB when looking", + " * from A to B.", + " * @param px The x-value of the point to test.", + " * @param py The y-value of the point to test.", + " * @param ax The x-value of the point (ax, ay) that is point A on line AB.", + " * @param ay The y-value of the point (ax, ay) that is point A on line AB.", + " * @param bx The x-value of the point (bx, by) that is point B on line AB.", + " * @param by The y-value of the point (bx, by) that is point B on line AB.", + " * @return TRUE if point P is to the right of line AB when looking", + " * from A to B.", + " */", + " ConvexPolygonGenerator.isRight = function (px, py, ax, ay, bx, by) {", + " return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) > 0;", + " };", + " /**", + " * Check if point P is to the right of or on line AB when looking", + " * from A to B.", + " * @param px The x-value of the point to test.", + " * @param py The y-value of the point to test.", + " * @param ax The x-value of the point (ax, ay) that is point A on line AB.", + " * @param ay The y-value of the point (ax, ay) that is point A on line AB.", + " * @param bx The x-value of the point (bx, by) that is point B on line AB.", + " * @param by The y-value of the point (bx, by) that is point B on line AB.", + " * @return TRUE if point P is to the right of or on line AB when looking", + " * from A to B.", + " */", + " ConvexPolygonGenerator.isRightOrCollinear = function (px, py, ax, ay, bx, by) {", + " return ConvexPolygonGenerator.getSignedAreaX2(ax, ay, px, py, bx, by) >= 0;", + " };", + " /**", + " * The absolute value of the returned value is two times the area of the", + " * triangle defined by points (A, B, C).", + " *", + " * A positive value indicates:", + " * - Counterclockwise wrapping of the points.", + " * - Point B lies to the right of line AC, looking from A to C.", + " *", + " * A negative value indicates:", + " * - Clockwise wrapping of the points.<", + " * - Point B lies to the left of line AC, looking from A to C.", + " *", + " * A value of zero indicates that all points are collinear or", + " * represent the same point.", + " *", + " * This is a fast operation.", + " *", + " * @param ax The x-value for point (ax, ay) for vertex A of the triangle.", + " * @param ay The y-value for point (ax, ay) for vertex A of the triangle.", + " * @param bx The x-value for point (bx, by) for vertex B of the triangle.", + " * @param by The y-value for point (bx, by) for vertex B of the triangle.", + " * @param cx The x-value for point (cx, cy) for vertex C of the triangle.", + " * @param cy The y-value for point (cx, cy) for vertex C of the triangle.", + " * @return The signed value of two times the area of the triangle defined", + " * by the points (A, B, C).", + " */", + " ConvexPolygonGenerator.getSignedAreaX2 = function (ax, ay, bx, by, cx, cy) {", + " // References:", + " // http://softsurfer.com/Archive/algorithm_0101/algorithm_0101.htm#Modern%20Triangles", + " // http://mathworld.wolfram.com/TriangleArea.html (Search for \"signed\")", + " return (bx - ax) * (cy - ay) - (cx - ax) * (by - ay);", + " };", + " return ConvexPolygonGenerator;", + "}());", + "", + "var GridCoordinateConverter = /** @class */ (function () {", + " function GridCoordinateConverter() {", + " }", + " /**", + " *", + " * @param gridPosition the position on the grid", + " * @param position the position on the scene", + " * @param scaleY for isometry", + " * @returns the position on the scene", + " */", + " GridCoordinateConverter.prototype.convertFromGridBasis = function (grid, polygons) {", + " // point can be shared so them must be copied to be scaled.", + " return polygons.map(function (polygon) {", + " return polygon.map(function (point) { return grid.convertFromGridBasis(point, { x: 0, y: 0 }); });", + " });", + " };", + " return GridCoordinateConverter;", + "}());", + "", + "/**", + " * It rasterizes obstacles on a grid.", + " *", + " * It flags cells as obstacle to be used by {@link RegionGenerator}.", + " */", + "var ObstacleRasterizer = /** @class */ (function () {", + " function ObstacleRasterizer() {", + " this.workingNodes = new Array(8);", + " this.gridBasisIterable = new GridBasisIterable();", + " }", + " /**", + " * Rasterize obstacles on a grid.", + " * @param grid", + " * @param obstacles", + " */", + " ObstacleRasterizer.prototype.rasterizeObstacles = function (grid, obstacles) {", + " var obstaclesItr = obstacles[Symbol.iterator]();", + " for (var next = obstaclesItr.next(); !next.done; next = obstaclesItr.next()) {", + " var obstacle = next.value;", + " this.gridBasisIterable.set(grid, obstacle);", + " var vertices = this.gridBasisIterable;", + " var minX = Number.MAX_VALUE;", + " var maxX = -Number.MAX_VALUE;", + " var minY = Number.MAX_VALUE;", + " var maxY = -Number.MAX_VALUE;", + " var verticesItr = vertices[Symbol.iterator]();", + " for (var next_1 = verticesItr.next(); !next_1.done; next_1 = verticesItr.next()) {", + " var vertex = next_1.value;", + " minX = Math.min(minX, vertex.x);", + " maxX = Math.max(maxX, vertex.x);", + " minY = Math.min(minY, vertex.y);", + " maxY = Math.max(maxY, vertex.y);", + " }", + " minX = Math.max(Math.floor(minX), 0);", + " maxX = Math.min(Math.ceil(maxX), grid.dimX());", + " minY = Math.max(Math.floor(minY), 0);", + " maxY = Math.min(Math.ceil(maxY), grid.dimY());", + " this.fillPolygon(vertices, minX, maxX, minY, maxY, function (x, y) { return (grid.get(x, y).distanceToObstacle = 0); });", + " }", + " };", + " ObstacleRasterizer.prototype.fillPolygon = function (vertices, minX, maxX, minY, maxY, fill) {", + " // The following implementation of the scan-line polygon fill algorithm", + " // is strongly inspired from:", + " // https://alienryderflex.com/polygon_fill/", + " // The original implementation was under this license:", + " // public-domain code by Darel Rex Finley, 2007", + " // This implementation differ with the following:", + " // - it handles float vertices", + " // so it focus on pixels center", + " // - it is conservative to thin vertical or horizontal polygons", + " var fillAnyPixels = false;", + " this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {", + " for (var pixelX = minX; pixelX < maxX; pixelX++) {", + " fillAnyPixels = true;", + " fill(pixelX, pixelY);", + " }", + " });", + " if (fillAnyPixels) {", + " return;", + " }", + " this.scanY(vertices, minX, maxX, minY, maxY, function (pixelY, minX, maxX) {", + " // conserve thin (less than one cell large) horizontal polygons", + " if (minX === maxX) {", + " fill(minX, pixelY);", + " }", + " });", + " this.scanX(vertices, minX, maxX, minY, maxY, function (pixelX, minY, maxY) {", + " for (var pixelY = minY; pixelY < maxY; pixelY++) {", + " fill(pixelX, pixelY);", + " }", + " // conserve thin (less than one cell large) vertical polygons", + " if (minY === maxY) {", + " fill(pixelX, minY);", + " }", + " });", + " };", + " ObstacleRasterizer.prototype.scanY = function (vertices, minX, maxX, minY, maxY, checkAndFillY) {", + " var workingNodes = this.workingNodes;", + " // Loop through the rows of the image.", + " for (var pixelY = minY; pixelY < maxY; pixelY++) {", + " var pixelCenterY = pixelY + 0.5;", + " // Build a list of nodes.", + " workingNodes.length = 0;", + " //let j = vertices.length - 1;", + " var verticesItr = vertices[Symbol.iterator]();", + " var next = verticesItr.next();", + " var vertex = next.value;", + " // The iterator always return the same instance.", + " // It must be copied to be save for later.", + " var firstVertexX = vertex.x;", + " var firstVertexY = vertex.y;", + " while (!next.done) {", + " var previousVertexX = vertex.x;", + " var previousVertexY = vertex.y;", + " next = verticesItr.next();", + " if (next.done) {", + " vertex.x = firstVertexX;", + " vertex.y = firstVertexY;", + " }", + " else {", + " vertex = next.value;", + " }", + " if ((vertex.y <= pixelCenterY && pixelCenterY < previousVertexY) ||", + " (previousVertexY < pixelCenterY && pixelCenterY <= vertex.y)) {", + " workingNodes.push(Math.round(vertex.x +", + " ((pixelCenterY - vertex.y) / (previousVertexY - vertex.y)) *", + " (previousVertexX - vertex.x)));", + " }", + " }", + " // Sort the nodes, via a simple “Bubble” sort.", + " {", + " var i = 0;", + " while (i < workingNodes.length - 1) {", + " if (workingNodes[i] > workingNodes[i + 1]) {", + " var swap = workingNodes[i];", + " workingNodes[i] = workingNodes[i + 1];", + " workingNodes[i + 1] = swap;", + " if (i > 0)", + " i--;", + " }", + " else {", + " i++;", + " }", + " }", + " }", + " // Fill the pixels between node pairs.", + " for (var i = 0; i < workingNodes.length; i += 2) {", + " if (workingNodes[i] >= maxX) {", + " break;", + " }", + " if (workingNodes[i + 1] <= minX) {", + " continue;", + " }", + " if (workingNodes[i] < minX) {", + " workingNodes[i] = minX;", + " }", + " if (workingNodes[i + 1] > maxX) {", + " workingNodes[i + 1] = maxX;", + " }", + " checkAndFillY(pixelY, workingNodes[i], workingNodes[i + 1]);", + " }", + " }", + " };", + " ObstacleRasterizer.prototype.scanX = function (vertices, minX, maxX, minY, maxY, checkAndFillX) {", + " var workingNodes = this.workingNodes;", + " // Loop through the columns of the image.", + " for (var pixelX = minX; pixelX < maxX; pixelX++) {", + " var pixelCenterX = pixelX + 0.5;", + " // Build a list of nodes.", + " workingNodes.length = 0;", + " var verticesItr = vertices[Symbol.iterator]();", + " var next = verticesItr.next();", + " var vertex = next.value;", + " // The iterator always return the same instance.", + " // It must be copied to be save for later.", + " var firstVertexX = vertex.x;", + " var firstVertexY = vertex.y;", + " while (!next.done) {", + " var previousVertexX = vertex.x;", + " var previousVertexY = vertex.y;", + " next = verticesItr.next();", + " if (next.done) {", + " vertex.x = firstVertexX;", + " vertex.y = firstVertexY;", + " }", + " else {", + " vertex = next.value;", + " }", + " if ((vertex.x < pixelCenterX && pixelCenterX < previousVertexX) ||", + " (previousVertexX < pixelCenterX && pixelCenterX < vertex.x)) {", + " workingNodes.push(Math.round(vertex.y +", + " ((pixelCenterX - vertex.x) / (previousVertexX - vertex.x)) *", + " (previousVertexY - vertex.y)));", + " }", + " }", + " // Sort the nodes, via a simple “Bubble” sort.", + " {", + " var i = 0;", + " while (i < workingNodes.length - 1) {", + " if (workingNodes[i] > workingNodes[i + 1]) {", + " var swap = workingNodes[i];", + " workingNodes[i] = workingNodes[i + 1];", + " workingNodes[i + 1] = swap;", + " if (i > 0)", + " i--;", + " }", + " else {", + " i++;", + " }", + " }", + " }", + " // Fill the pixels between node pairs.", + " for (var i = 0; i < workingNodes.length; i += 2) {", + " if (workingNodes[i] >= maxY) {", + " break;", + " }", + " if (workingNodes[i + 1] <= minY) {", + " continue;", + " }", + " if (workingNodes[i] < minY) {", + " workingNodes[i] = minY;", + " }", + " if (workingNodes[i + 1] > maxY) {", + " workingNodes[i + 1] = maxY;", + " }", + " checkAndFillX(pixelX, workingNodes[i], workingNodes[i + 1]);", + " }", + " }", + " };", + " return ObstacleRasterizer;", + "}());", + "/**", + " * Iterable that converts coordinates to the grid.", + " *", + " * This is an allocation free iterable", + " * that can only do one iteration at a time.", + " */", + "var GridBasisIterable = /** @class */ (function () {", + " function GridBasisIterable() {", + " this.grid = null;", + " this.sceneVertices = [];", + " this.verticesItr = this.sceneVertices[Symbol.iterator]();", + " this.result = {", + " value: { x: 0, y: 0 },", + " done: false,", + " };", + " }", + " GridBasisIterable.prototype.set = function (grid, sceneVertices) {", + " this.grid = grid;", + " this.sceneVertices = sceneVertices;", + " };", + " GridBasisIterable.prototype[Symbol.iterator] = function () {", + " this.verticesItr = this.sceneVertices[Symbol.iterator]();", + " return this;", + " };", + " GridBasisIterable.prototype.next = function () {", + " var next = this.verticesItr.next();", + " if (next.done) {", + " return next;", + " }", + " this.grid.convertToGridBasis(next.value, this.result.value);", + " return this.result;", + " };", + " return GridBasisIterable;", + "}());", + "", + "/**", + " * Build cohesive regions from the non-obstacle space. It uses the data", + " * from the obstacles rasterization {@link ObstacleRasterizer}.", + " *", + " * This implementation is strongly inspired from CritterAI class \"OpenHeightfieldBuilder\".", + " *", + " * Introduction to Height Fields: http://www.critterai.org/projects/nmgen_study/heightfields.html", + " *", + " * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html", + " */", + "var RegionGenerator = /** @class */ (function () {", + " function RegionGenerator() {", + " this.obstacleRegionBordersCleaner = new ObstacleRegionBordersCleaner();", + " this.floodedCells = new Array(1024);", + " this.workingStack = new Array(1024);", + " }", + " //TODO implement the smoothing pass on the distance field?", + " /**", + " * Groups cells into cohesive regions using an watershed based algorithm.", + " *", + " * This operation depends on neighbor and distance field information.", + " * So {@link RegionGenerator.generateDistanceField} operations must be", + " * run before this operation.", + " *", + " * @param grid A field with cell distance information fully generated.", + " * @param obstacleCellPadding a padding in cells to apply around the", + " * obstacles.", + " */", + " RegionGenerator.prototype.generateRegions = function (grid, obstacleCellPadding) {", + " // Watershed Algorithm", + " //", + " // Reference: http://en.wikipedia.org/wiki/Watershed_%28algorithm%29", + " // A good visualization:", + " // http://artis.imag.fr/Publications/2003/HDS03/ (PDF)", + " //", + " // Summary:", + " //", + " // This algorithm utilizes the cell.distanceToObstacle value, which", + " // is generated by the generateDistanceField() operation.", + " //", + " // Using the watershed analogy, the cells which are furthest from", + " // a border (highest distance to border) represent the lowest points", + " // in the watershed. A border cell represents the highest possible", + " // water level.", + " //", + " // The main loop iterates, starting at the lowest point in the", + " // watershed, then incrementing with each loop until the highest", + " // allowed water level is reached. This slowly \"floods\" the cells", + " // starting at the lowest points.", + " //", + " // During each iteration of the loop, cells that are below the", + " // current water level are located and an attempt is made to either", + " // add them to exiting regions or create new regions from them.", + " //", + " // During the region expansion phase, if a newly flooded cell", + " // borders on an existing region, it is usually added to the region.", + " //", + " // Any newly flooded cell that survives the region expansion phase", + " // is used as a seed for a new region.", + " //", + " // At the end of the main loop, a final region expansion is", + " // performed which should catch any stray cells that escaped region", + " // assignment during the main loop.", + " // Represents the minimum distance to an obstacle that is considered", + " // traversable. I.e. Can't traverse cells closer than this distance", + " // to a border. This provides a way of artificially capping the", + " // height to which watershed flooding can occur.", + " // I.e. Don't let the algorithm flood all the way to the actual border.", + " //", + " // We add the minimum border distance to take into account the", + " // blurring algorithm which can result in a border cell having a", + " // border distance > 0.", + " var distanceMin = obstacleCellPadding * 2;", + " // TODO: EVAL: Figure out why this iteration limit is needed", + " // (todo from the CritterAI sources).", + " var expandIterations = 4 + distanceMin * 2;", + " // Zero is reserved for the obstacle-region. So initializing to 1.", + " var nextRegionID = 1;", + " var floodedCells = this.floodedCells;", + " // Search until the current distance reaches the minimum allowed", + " // distance.", + " //", + " // Note: This loop will not necessarily complete all region", + " // assignments. This is OK since a final region assignment step", + " // occurs after the loop iteration is complete.", + " for (", + " // This value represents the current distance from the border which", + " // is to be searched. The search starts at the maximum distance then", + " // moves toward zero (toward borders).", + " //", + " // This number will always be divisible by 2.", + " var distance = grid.obstacleDistanceMax() & ~1; distance > distanceMin; distance = Math.max(distance - 2, 0)) {", + " // Find all cells that are at or below the current \"water level\"", + " // and are not already assigned to a region. Add these cells to", + " // the flooded cell list for processing.", + " floodedCells.length = 0;", + " for (var y = 1; y < grid.dimY() - 1; y++) {", + " for (var x = 1; x < grid.dimX() - 1; x++) {", + " var cell = grid.get(x, y);", + " if (cell.regionID === RasterizationCell.NULL_REGION_ID &&", + " cell.distanceToObstacle >= distance) {", + " // The cell is not already assigned a region and is", + " // below the current \"water level\". So the cell can be", + " // considered for region assignment.", + " floodedCells.push(cell);", + " }", + " }", + " }", + " if (nextRegionID > 1) {", + " // At least one region has already been created, so first", + " // try to put the newly flooded cells into existing regions.", + " if (distance > 0) {", + " this.expandRegions(grid, floodedCells, expandIterations);", + " }", + " else {", + " this.expandRegions(grid, floodedCells, -1);", + " }", + " }", + " // Create new regions for all cells that could not be added to", + " // existing regions.", + " for (var _i = 0, floodedCells_1 = floodedCells; _i < floodedCells_1.length; _i++) {", + " var floodedCell = floodedCells_1[_i];", + " if (!floodedCell ||", + " floodedCell.regionID !== RasterizationCell.NULL_REGION_ID) {", + " // This cell was assigned to a newly created region", + " // during an earlier iteration of this loop.", + " // So it can be skipped.", + " continue;", + " }", + " // Fill to slightly more than the current \"water level\".", + " // This improves efficiency of the algorithm.", + " // And it is necessary with the conservative expansion to ensure that", + " // more than one cell is added initially to a new regions otherwise", + " // no cell could be added to it later because of the conservative", + " // constraint.", + " var fillTo = Math.max(distance - 2, distanceMin + 1, 1);", + " if (this.floodNewRegion(grid, floodedCell, fillTo, nextRegionID)) {", + " nextRegionID++;", + " }", + " }", + " }", + " // Find all cells that haven't been assigned regions by the main loop", + " // (up to the minimum distance).", + " floodedCells.length = 0;", + " for (var y = 1; y < grid.dimY() - 1; y++) {", + " for (var x = 1; x < grid.dimX() - 1; x++) {", + " var cell = grid.get(x, y);", + " if (cell.distanceToObstacle > distanceMin &&", + " cell.regionID === RasterizationCell.NULL_REGION_ID) {", + " // Not a border or obstacle region cell. Should be in a region.", + " floodedCells.push(cell);", + " }", + " }", + " }", + " // Perform a final expansion of existing regions.", + " // Allow more iterations than normal for this last expansion.", + " if (distanceMin > 0) {", + " this.expandRegions(grid, floodedCells, expandIterations * 8);", + " }", + " else {", + " this.expandRegions(grid, floodedCells, -1);", + " }", + " grid.regionCount = nextRegionID;", + " this.obstacleRegionBordersCleaner.fixObstacleRegion(grid);", + " //TODO Also port FilterOutSmallRegions?", + " // The algorithm to remove vertices in the middle (added at the end of", + " // ContourBuilder.buildContours) may already filter them and contour are", + " // faster to process than cells.", + " };", + " /**", + " * Attempts to find the most appropriate regions to attach cells to.", + " *", + " * Any cells successfully attached to a region will have their list", + " * entry set to null. So any non-null entries in the list will be cells", + " * for which a region could not be determined.", + " *", + " * @param grid", + " * @param inoutCells As input, the list of cells available for formation", + " * of new regions. As output, the cells that could not be assigned", + " * to new regions.", + " * @param maxIterations If set to -1, will iterate through completion.", + " */", + " RegionGenerator.prototype.expandRegions = function (grid, inoutCells, iterationMax) {", + " if (inoutCells.length === 0)", + " return;", + " var skipped = 0;", + " for (var iteration = 0; (iteration < iterationMax || iterationMax === -1) &&", + " // All cells have either been processed or could not be", + " // processed during the last cycle.", + " skipped < inoutCells.length; iteration++) {", + " // The number of cells in the working list that have been", + " // successfully processed or could not be processed successfully", + " // for some reason.", + " // This value controls when iteration ends.", + " skipped = 0;", + " for (var index = 0; index < inoutCells.length; index++) {", + " var cell = inoutCells[index];", + " if (cell === null) {", + " // The cell originally at this index location has", + " // already been successfully assigned a region. Nothing", + " // else to do with it.", + " skipped++;", + " continue;", + " }", + " // Default to unassigned.", + " var cellRegion = RasterizationCell.NULL_REGION_ID;", + " var regionCenterDist = Number.MAX_VALUE;", + " for (var _i = 0, _a = RasterizationGrid.neighbor4Deltas; _i < _a.length; _i++) {", + " var delta = _a[_i];", + " var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);", + " if (neighbor.regionID !== RasterizationCell.NULL_REGION_ID) {", + " if (neighbor.distanceToRegionCore + 2 < regionCenterDist) {", + " // This neighbor is closer to its region core", + " // than previously detected neighbors.", + " // Conservative expansion constraint:", + " // Check to ensure that this neighbor has", + " // at least two other neighbors in its region.", + " // This makes sure that adding this cell to", + " // this neighbor's region will not result", + " // in a single width line of cells.", + " var sameRegionCount = 0;", + " for (var neighborDirection = 0; neighborDirection < 4; neighborDirection++) {", + " var nnCell = grid.getNeighbor(neighbor, neighborDirection);", + " // There is a diagonal-neighbor", + " if (nnCell.regionID === neighbor.regionID) {", + " // This neighbor has a neighbor in", + " // the same region.", + " sameRegionCount++;", + " }", + " }", + " if (sameRegionCount > 1) {", + " cellRegion = neighbor.regionID;", + " regionCenterDist = neighbor.distanceToRegionCore + 2;", + " }", + " }", + " }", + " }", + " if (cellRegion !== RasterizationCell.NULL_REGION_ID) {", + " // Found a suitable region for this cell to belong to.", + " // Mark this index as having been processed.", + " inoutCells[index] = null;", + " cell.regionID = cellRegion;", + " cell.distanceToRegionCore = regionCenterDist;", + " }", + " else {", + " // Could not find an existing region for this cell.", + " skipped++;", + " }", + " }", + " }", + " };", + " /**", + " * Creates a new region surrounding a cell, adding neighbor cells to the", + " * new region as appropriate.", + " *", + " * The new region creation will fail if the root cell is on the", + " * border of an existing region.", + " *", + " * All cells added to the new region as part of this process become", + " * \"core\" cells with a distance to region core of zero.", + " *", + " * @param grid", + " * @param rootCell The cell used to seed the new region.", + " * @param fillToDist The watershed distance to flood to.", + " * @param regionID The region ID to use for the new region", + " * (if creation is successful).", + " * @return true if a new region was created.", + " */", + " RegionGenerator.prototype.floodNewRegion = function (grid, rootCell, fillToDist, regionID) {", + " var workingStack = this.workingStack;", + " workingStack.length = 0;", + " workingStack.push(rootCell);", + " rootCell.regionID = regionID;", + " rootCell.distanceToRegionCore = 0;", + " var regionSize = 0;", + " var cell;", + " while ((cell = workingStack.pop())) {", + " // Check regions of neighbor cells.", + " //", + " // If any neighbor is found to have a region assigned, then", + " // the current cell can't be in the new region", + " // (want standard flooding algorithm to handle deciding which", + " // region this cell should go in).", + " //", + " // Up to 8 neighbors are checked.", + " //", + " // Neighbor searches:", + " // http://www.critterai.org/projects/nmgen_study/heightfields.html#nsearch", + " var isOnRegionBorder = false;", + " for (var _i = 0, _a = RasterizationGrid.neighbor8Deltas; _i < _a.length; _i++) {", + " var delta = _a[_i];", + " var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);", + " isOnRegionBorder =", + " neighbor.regionID !== RasterizationCell.NULL_REGION_ID &&", + " neighbor.regionID !== regionID;", + " if (isOnRegionBorder)", + " break;", + " }", + " if (isOnRegionBorder) {", + " cell.regionID = RasterizationCell.NULL_REGION_ID;", + " continue;", + " }", + " regionSize++;", + " // If got this far, we know the current cell is part of the new", + " // region. Now check its neighbors to see if they should be", + " // assigned to this new region.", + " for (var _b = 0, _c = RasterizationGrid.neighbor4Deltas; _b < _c.length; _b++) {", + " var delta = _c[_b];", + " var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);", + " if (neighbor.distanceToObstacle >= fillToDist &&", + " neighbor.regionID === RasterizationCell.NULL_REGION_ID) {", + " neighbor.regionID = regionID;", + " neighbor.distanceToRegionCore = 0;", + " workingStack.push(neighbor);", + " }", + " }", + " }", + " return regionSize > 0;", + " };", + " /**", + " * Generates distance field information.", + " * The {@link RasterizationCell.distanceToObstacle} information is generated", + " * for all cells in the field.", + " *", + " * All distance values are relative and do not represent explicit", + " * distance values (such as grid unit distance). The algorithm which is", + " * used results in an approximation only. It is not exhaustive.", + " *", + " * The data generated by this operation is required by", + " * {@link RegionGenerator.generateRegions}.", + " *", + " * @param grid A field with cells obstacle information already generated.", + " */", + " RegionGenerator.prototype.generateDistanceField = function (grid) {", + " // close borders", + " for (var x = 0; x < grid.dimX(); x++) {", + " var leftCell = grid.get(x, 0);", + " leftCell.distanceToObstacle = 0;", + " var rightCell = grid.get(x, grid.dimY() - 1);", + " rightCell.distanceToObstacle = 0;", + " }", + " for (var y = 1; y < grid.dimY() - 1; y++) {", + " var topCell = grid.get(0, y);", + " topCell.distanceToObstacle = 0;", + " var bottomCell = grid.get(grid.dimX() - 1, y);", + " bottomCell.distanceToObstacle = 0;", + " }", + " // The next two phases basically check the neighbors of a cell and", + " // set the cell's distance field to be slightly greater than the", + " // neighbor with the lowest border distance. Distance is increased", + " // slightly more for diagonal-neighbors than for axis-neighbors.", + " // 1st pass", + " // During this pass, the following neighbors are checked:", + " // (-1, 0) (-1, -1) (0, -1) (1, -1)", + " for (var y = 1; y < grid.dimY() - 1; y++) {", + " for (var x = 1; x < grid.dimX() - 1; x++) {", + " var cell = grid.get(x, y);", + " for (var _i = 0, _a = RegionGenerator.firstPassDeltas; _i < _a.length; _i++) {", + " var delta = _a[_i];", + " var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +", + " delta.distance;", + " if (cell.distanceToObstacle > distanceByNeighbor) {", + " cell.distanceToObstacle = distanceByNeighbor;", + " }", + " }", + " }", + " }", + " // 2nd pass", + " // During this pass, the following neighbors are checked:", + " // (1, 0) (1, 1) (0, 1) (-1, 1)", + " //", + " // Besides checking different neighbors, this pass performs its", + " // grid search in reverse order.", + " for (var y = grid.dimY() - 2; y >= 1; y--) {", + " for (var x = grid.dimX() - 2; x >= 1; x--) {", + " var cell = grid.get(x, y);", + " for (var _b = 0, _c = RegionGenerator.secondPassDeltas; _b < _c.length; _b++) {", + " var delta = _c[_b];", + " var distanceByNeighbor = grid.get(x + delta.x, y + delta.y).distanceToObstacle +", + " delta.distance;", + " if (cell.distanceToObstacle > distanceByNeighbor) {", + " cell.distanceToObstacle = distanceByNeighbor;", + " }", + " }", + " }", + " }", + " };", + " RegionGenerator.firstPassDeltas = [", + " { x: -1, y: 0, distance: 2 },", + " { x: -1, y: -1, distance: 3 },", + " { x: 0, y: -1, distance: 2 },", + " { x: 1, y: -1, distance: 3 },", + " ];", + " RegionGenerator.secondPassDeltas = [", + " { x: 1, y: 0, distance: 2 },", + " { x: 1, y: 1, distance: 3 },", + " { x: 0, y: 1, distance: 2 },", + " { x: -1, y: 1, distance: 3 },", + " ];", + " return RegionGenerator;", + "}());", + "/**", + " * Implements three algorithms that clean up issues that can", + " * develop around obstacle region boarders.", + " *", + " * - Detect and fix encompassed obstacle regions:", + " *", + " * If a obstacle region is found that is fully encompassed by a single", + " * region, then the region will be split into two regions at the", + " * obstacle region border.", + " *", + " * - Detect and fix \"short wrapping\" of obstacle regions:", + " *", + " * Regions can sometimes wrap slightly around the corner of a obstacle region", + " * in a manner that eventually results in the formation of self-intersecting", + " * polygons.", + " *", + " * Example: Before the algorithm is applied:", + " * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_08_cornerwrapbefore.jpg\"", + " *", + " * Example: After the algorithm is applied:", + " * http://www.critterai.org/projects/nmgen_study/media/images/ohfg_09_cornerwrapafter.jpg", + " *", + " * - Detect and fix incomplete obstacle region connections:", + " *", + " * If a region touches obstacle region only diagonally, then contour detection", + " * algorithms may not properly detect the obstacle region connection. This can", + " * adversely effect other algorithms in the pipeline.", + " *", + " * Example: Before algorithm is applied:", + " *", + " * b b a a a a", + " * b b a a a a", + " * a a x x x x", + " * a a x x x x", + " *", + " * Example: After algorithm is applied:", + " *", + " * b b a a a a", + " * b b b a a a <-- Cell transferred to region B.", + " * a a x x x x", + " * a a x x x x", + " *", + " *", + " * Region Generation: http://www.critterai.org/projects/nmgen_study/regiongen.html", + " */", + "var ObstacleRegionBordersCleaner = /** @class */ (function () {", + " function ObstacleRegionBordersCleaner() {", + " this.workingUpLeftOpenCells = new Array(512);", + " this.workingDownRightOpenCells = new Array(512);", + " this.workingOpenCells = new Array(512);", + " }", + " /**", + " * This operation utilizes {@link RasterizationCell.contourFlags}. It", + " * expects the value to be zero on entry, and re-zero's the value", + " * on exit.", + " *", + " * @param grid a grid with fully built regions.", + " */", + " ObstacleRegionBordersCleaner.prototype.fixObstacleRegion = function (grid) {", + " var workingUpLeftOpenCells = this.workingUpLeftOpenCells;", + " workingUpLeftOpenCells.length = 0;", + " var workingDownRightOpenCells = this.workingDownRightOpenCells;", + " workingDownRightOpenCells.length = 0;", + " var workingOpenCells = this.workingOpenCells;", + " workingOpenCells.length = 0;", + " var extremeCells = [", + " null,", + " null,", + " ];", + " var nextRegionID = grid.regionCount;", + " // Iterate over the cells, trying to find obstacle region borders.", + " for (var y = 1; y < grid.dimY() - 1; y++) {", + " for (var x = 1; x < grid.dimX() - 1; x++) {", + " var cell = grid.get(x, y);", + " if (cell.contourFlags !== 0)", + " // Cell was processed in a previous iteration.", + " // Ignore it.", + " continue;", + " cell.contourFlags = 1;", + " var workingCell = null;", + " var edgeDirection = -1;", + " if (cell.regionID !== RasterizationCell.OBSTACLE_REGION_ID) {", + " // Not interested in this cell.", + " continue;", + " }", + " // This is a obstacle region cell. See if it", + " // connects to a cell in a non-obstacle region.", + " edgeDirection = this.getNonNullBorderDirection(grid, cell);", + " if (edgeDirection === -1)", + " // This cell is not a border cell. Ignore it.", + " continue;", + " // This is a border cell. Step into the non-null", + " // region and swing the direction around 180 degrees.", + " workingCell = grid.getNeighbor(cell, edgeDirection);", + " edgeDirection = (edgeDirection + 2) & 0x3;", + " // Process the obstacle region contour. Detect and fix", + " // local issues. Determine if the region is", + " // fully encompassed by a single non-obstacle region.", + " var isEncompassedNullRegion = this.processNullRegion(grid, workingCell, edgeDirection, extremeCells);", + " if (isEncompassedNullRegion) {", + " // This cell is part of a group of obstacle region cells", + " // that is encompassed within a single non-obstacle region.", + " // This is not permitted. Need to fix it.", + " this.partialFloodRegion(grid, extremeCells[0], extremeCells[1], nextRegionID);", + " nextRegionID++;", + " }", + " }", + " }", + " grid.regionCount = nextRegionID;", + " // Clear all flags.", + " for (var y = 1; y < grid.dimY() - 1; y++) {", + " for (var x = 1; x < grid.dimX() - 1; x++) {", + " var cell = grid.get(x, y);", + " cell.contourFlags = 0;", + " }", + " }", + " };", + " /**", + " * Partially flood a region away from the specified direction.", + " *", + " * {@link RasterizationCell.contourFlags}", + " * is set to zero for all flooded cells.", + " *", + " * @param grid", + " * @param startCell The cell to start the flood from.", + " * @param borderDirection The hard border for flooding. No", + " * cells in this direction from the startCell will be flooded.", + " * @param newRegionID The region id to assign the flooded", + " * cells to.", + " */", + " ObstacleRegionBordersCleaner.prototype.partialFloodRegion = function (grid, upLeftCell, downRightCell, newRegionID) {", + " var upLeftOpenCells = this.workingUpLeftOpenCells;", + " var downRightOpenCells = this.workingDownRightOpenCells;", + " var workingOpenCells = this.workingOpenCells;", + " // The implementation differs from CritterAI to avoid non-contiguous", + " // sections. Instead of brushing in one direction, it floods from", + " // 2 extremities of the encompassed obstacle region.", + " var regionID = upLeftCell.regionID;", + " if (regionID === newRegionID) {", + " // avoid infinity loop", + " console.error(\"Can't create a new region with an ID that already exist.\");", + " return;", + " }", + " // The 1st flooding set a new the regionID", + " upLeftCell.regionID = newRegionID;", + " upLeftCell.distanceToRegionCore = 0; // This information is lost.", + " upLeftOpenCells.length = 0;", + " upLeftOpenCells.push(upLeftCell);", + " // The 2nd flooding keep the regionID and mark the cell as visited.", + " downRightCell.contourFlags = 2;", + " downRightCell.distanceToRegionCore = 0; // This information is lost.", + " downRightOpenCells.length = 0;", + " downRightOpenCells.push(downRightCell);", + " var swap;", + " workingOpenCells.length = 0;", + " while (upLeftOpenCells.length !== 0 || downRightOpenCells.length !== 0) {", + " for (var _i = 0, upLeftOpenCells_1 = upLeftOpenCells; _i < upLeftOpenCells_1.length; _i++) {", + " var cell = upLeftOpenCells_1[_i];", + " for (var direction = 0; direction < 4; direction++) {", + " var neighbor = grid.getNeighbor(cell, direction);", + " if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {", + " continue;", + " }", + " // Transfer the neighbor to the new region.", + " neighbor.regionID = newRegionID;", + " neighbor.distanceToRegionCore = 0; // This information is lost.", + " workingOpenCells.push(neighbor);", + " }", + " }", + " // This allows to flood the nearest cells first without needing lifo queue.", + " // But a queue would take less memory.", + " swap = upLeftOpenCells;", + " upLeftOpenCells = workingOpenCells;", + " workingOpenCells = swap;", + " workingOpenCells.length = 0;", + " for (var _a = 0, downRightOpenCells_1 = downRightOpenCells; _a < downRightOpenCells_1.length; _a++) {", + " var cell = downRightOpenCells_1[_a];", + " for (var direction = 0; direction < 4; direction++) {", + " var neighbor = grid.getNeighbor(cell, direction);", + " if (neighbor.regionID !== regionID || neighbor.contourFlags === 2) {", + " continue;", + " }", + " // Keep the neighbor to the current region.", + " neighbor.contourFlags = 2;", + " neighbor.distanceToRegionCore = 0; // This information is lost.", + " workingOpenCells.push(neighbor);", + " }", + " }", + " swap = downRightOpenCells;", + " downRightOpenCells = workingOpenCells;", + " workingOpenCells = swap;", + " workingOpenCells.length = 0;", + " }", + " };", + " /**", + " * Detects and fixes bad cell configurations in the vicinity of a", + " * obstacle region contour (See class description for details).", + " * @param grid", + " * @param startCell A cell in a non-obstacle region that borders a null", + " * region.", + " * @param startDirection The direction of the obstacle region border.", + " * @return TRUE if the start cell's region completely encompasses", + " * the obstacle region.", + " */", + " ObstacleRegionBordersCleaner.prototype.processNullRegion = function (grid, startCell, startDirection, extremeCells) {", + " // This algorithm traverses the contour. As it does so, it detects", + " // and fixes various known dangerous cell configurations.", + " //", + " // Traversing the contour: A good way to visualize it is to think", + " // of a robot sitting on the floor facing a known wall. It then", + " // does the following to skirt the wall:", + " // 1. If there is a wall in front of it, turn clockwise in 90 degrees", + " // increments until it finds the wall is gone.", + " // 2. Move forward one step.", + " // 3. Turn counter-clockwise by 90 degrees.", + " // 4. Repeat from step 1 until it finds itself at its original", + " // location facing its original direction.", + " //", + " // See also: http://www.critterai.org/projects/nmgen_study/regiongen.html#robotwalk", + " //", + " // As the traversal occurs, the number of acute (90 degree) and", + " // obtuse (270 degree) corners are monitored. If a complete contour is", + " // detected and (obtuse corners > acute corners), then the null", + " // region is inside the contour. Otherwise the obstacle region is", + " // outside the contour, which we don't care about.", + " var borderRegionID = startCell.regionID;", + " // Prepare for loop.", + " var cell = startCell;", + " var neighbor = null;", + " var direction = startDirection;", + " var upLeftCell = cell;", + " var downRightCell = cell;", + " // Initialize monitoring variables.", + " var loopCount = 0;", + " var acuteCornerCount = 0;", + " var obtuseCornerCount = 0;", + " var stepsWithoutBorder = 0;", + " var borderSeenLastLoop = false;", + " var isBorder = true; // Initial value doesn't matter.", + " // Assume a single region is connected to the obstacle region", + " // until proven otherwise.", + " var hasSingleConnection = true;", + " // The loop limit exists for the sole reason of preventing", + " // an infinite loop in case of bad input data.", + " // It is set to a very high value because there is no way of", + " // definitively determining a safe smaller value. Setting", + " // the value too low can result in rescanning a contour", + " // multiple times, killing performance.", + " while (++loopCount < 1 << 30) {", + " // Get the cell across the border.", + " neighbor = grid.getNeighbor(cell, direction);", + " // Detect which type of edge this direction points across.", + " if (neighbor === null) {", + " // It points across a obstacle region border edge.", + " isBorder = true;", + " }", + " else {", + " // We never need to perform contour detection", + " // on this cell again. So mark it as processed.", + " neighbor.contourFlags = 1;", + " if (neighbor.regionID === RasterizationCell.OBSTACLE_REGION_ID) {", + " // It points across a obstacle region border edge.", + " isBorder = true;", + " }", + " else {", + " // This isn't a obstacle region border.", + " isBorder = false;", + " if (neighbor.regionID !== borderRegionID)", + " // It points across a border to a non-obstacle region.", + " // This means the current contour can't", + " // represent a fully encompassed obstacle region.", + " hasSingleConnection = false;", + " }", + " }", + " // Process the border.", + " if (isBorder) {", + " // It is a border edge.", + " if (borderSeenLastLoop) {", + " // A border was detected during the last loop as well.", + " // Two detections in a row indicates we passed an acute", + " // (inner) corner.", + " //", + " // a x", + " // x x", + " acuteCornerCount++;", + " }", + " else if (stepsWithoutBorder > 1) {", + " // We have moved at least two cells before detecting", + " // a border. This indicates we passed an obtuse", + " // (outer) corner.", + " //", + " // a a", + " // a x", + " obtuseCornerCount++;", + " stepsWithoutBorder = 0;", + " // Detect and fix cell configuration issue around this", + " // corner.", + " if (this.processOuterCorner(grid, cell, direction))", + " // A change was made and it resulted in the", + " // corner area having multiple region connections.", + " hasSingleConnection = false;", + " }", + " direction = (direction + 1) & 0x3; // Rotate in clockwise direction.", + " borderSeenLastLoop = true;", + " stepsWithoutBorder = 0;", + " }", + " else {", + " // Not a obstacle region border.", + " // Move to the neighbor and swing the search direction back", + " // one increment (counterclockwise). By moving the direction", + " // back one increment we guarantee we don't miss any edges.", + " cell = neighbor;", + " direction = (direction + 3) & 0x3; // Rotate counterclockwise direction.", + " borderSeenLastLoop = false;", + " stepsWithoutBorder++;", + " if (cell.x < upLeftCell.x ||", + " (cell.x === upLeftCell.x && cell.y < upLeftCell.y)) {", + " upLeftCell = cell;", + " }", + " if (cell.x > downRightCell.x ||", + " (cell.x === downRightCell.x && cell.y > downRightCell.y)) {", + " downRightCell = cell;", + " }", + " }", + " if (startCell === cell && startDirection === direction) {", + " extremeCells[0] = upLeftCell;", + " extremeCells[1] = downRightCell;", + " // Have returned to the original cell and direction.", + " // The search is complete.", + " // Is the obstacle region inside the contour?", + " return hasSingleConnection && obtuseCornerCount > acuteCornerCount;", + " }", + " }", + " // If got here then the obstacle region boarder is too large to be fully", + " // explored. So it can't be encompassed.", + " return false;", + " };", + " /**", + " * Detects and fixes cell configuration issues in the vicinity", + " * of obtuse (outer) obstacle region corners.", + " * @param grid", + " * @param referenceCell The cell in a non-obstacle region that is", + " * just past the outer corner.", + " * @param borderDirection The direction of the obstacle region border.", + " * @return TRUE if more than one region connects to the obstacle region", + " * in the vicinity of the corner (this may or may not be due to", + " * a change made by this operation).", + " */", + " ObstacleRegionBordersCleaner.prototype.processOuterCorner = function (grid, referenceCell, borderDirection) {", + " var hasMultiRegions = false;", + " // Get the previous two cells along the border.", + " var backOne = grid.getNeighbor(referenceCell, (borderDirection + 3) & 0x3);", + " var backTwo = grid.getNeighbor(backOne, borderDirection);", + " var testCell;", + " if (backOne.regionID !== referenceCell.regionID &&", + " // This differ from the CritterAI implementation.", + " // To filter vertices in the middle, this must be avoided too:", + " // a x", + " // b c", + " backTwo.regionID !== backOne.regionID) {", + " // Dangerous corner configuration.", + " //", + " // a x", + " // b a", + " //", + " // Need to change to one of the following configurations:", + " //", + " // b x a x", + " // b a b b", + " //", + " // Reason: During contour detection this type of configuration can", + " // result in the region connection being detected as a", + " // region-region portal, when it is not. The region connection", + " // is actually interrupted by the obstacle region.", + " //", + " // This configuration has been demonstrated to result in", + " // two regions being improperly merged to encompass an", + " // internal obstacle region.", + " //", + " // Example:", + " //", + " // a a x x x a", + " // a a x x a a", + " // b b a a a a", + " // b b a a a a", + " //", + " // During contour and connection detection for region b, at no", + " // point will the obstacle region be detected. It will appear", + " // as if a clean a-b portal exists.", + " //", + " // An investigation into fixing this issue via updates to the", + " // watershed or contour detection algorithms did not turn", + " // up a better way of resolving this issue.", + " hasMultiRegions = true;", + " // Determine how many connections backTwo has to backOne's region.", + " testCell = grid.getNeighbor(backOne, (borderDirection + 3) & 0x3);", + " var backTwoConnections = 0;", + " if (testCell.regionID === backOne.regionID) {", + " backTwoConnections++;", + " testCell = grid.getNeighbor(testCell, borderDirection);", + " if (testCell.regionID === backOne.regionID)", + " backTwoConnections++;", + " }", + " // Determine how many connections the reference cell has", + " // to backOne's region.", + " var referenceConnections = 0;", + " testCell = grid.getNeighbor(backOne, (borderDirection + 2) & 0x3);", + " if (testCell.regionID === backOne.regionID) {", + " referenceConnections++;", + " testCell = grid.getNeighbor(testCell, (borderDirection + 2) & 0x3);", + " if (testCell.regionID === backOne.regionID)", + " backTwoConnections++;", + " }", + " // Change the region of the cell that has the most connections", + " // to the target region.", + " if (referenceConnections > backTwoConnections)", + " referenceCell.regionID = backOne.regionID;", + " else", + " backTwo.regionID = backOne.regionID;", + " }", + " else if (backOne.regionID === referenceCell.regionID &&", + " backTwo.regionID === referenceCell.regionID) {", + " // Potential dangerous short wrap.", + " //", + " // a x", + " // a a", + " //", + " // Example of actual problem configuration:", + " //", + " // b b x x", + " // b a x x <- Short wrap.", + " // b a a a", + " //", + " // In the above case, the short wrap around the corner of the", + " // obstacle region has been demonstrated to cause self-intersecting", + " // polygons during polygon formation.", + " //", + " // This algorithm detects whether or not one (and only one)", + " // of the axis neighbors of the corner should be re-assigned to", + " // a more appropriate region.", + " //", + " // In the above example, the following configuration is more", + " // appropriate:", + " //", + " // b b x x", + " // b b x x <- Change to this row.", + " // b a a a", + " // Check to see if backTwo should be in a different region.", + " var selectedRegion = this.selectedRegionID(grid, backTwo, (borderDirection + 1) & 0x3, (borderDirection + 2) & 0x3);", + " if (selectedRegion === backTwo.regionID) {", + " // backTwo should not be re-assigned. How about", + " // the reference cell?", + " selectedRegion = this.selectedRegionID(grid, referenceCell, borderDirection, (borderDirection + 3) & 0x3);", + " if (selectedRegion !== referenceCell.regionID) {", + " // The reference cell should be reassigned", + " // to a new region.", + " referenceCell.regionID = selectedRegion;", + " hasMultiRegions = true;", + " }", + " }", + " else {", + " // backTwo should be re-assigned to a new region.", + " backTwo.regionID = selectedRegion;", + " hasMultiRegions = true;", + " }", + " }", + " else", + " hasMultiRegions = true;", + " // No dangerous configurations detected. But definitely", + " // has a change in regions at the corner. We know this", + " // because one of the previous checks looked for a single", + " // region for all wrap cells.", + " return hasMultiRegions;", + " };", + " /**", + " * Checks the cell to see if it should be reassigned to a new region.", + " *", + " * @param grid", + " * @param referenceCell A cell on one side of an obstacle region contour's", + " * outer corner. It is expected that the all cells that wrap the", + " * corner are in the same region.", + " * @param borderDirection The direction of the obstacle region border.", + " * @param cornerDirection The direction of the outer corner from the", + " * reference cell.", + " * @return The region the cell should be a member of. May be the", + " * region the cell is currently a member of.", + " */", + " ObstacleRegionBordersCleaner.prototype.selectedRegionID = function (grid, referenceCell, borderDirection, cornerDirection) {", + " // Initial example state:", + " //", + " // a - Known region.", + " // x - Null region.", + " // u - Unknown, not checked yet.", + " //", + " // u u u", + " // u a x", + " // u a a", + " // The only possible alternate region id is from", + " // the cell that is opposite the border. So check it first.", + " var regionID = grid.getNeighbor(referenceCell, (borderDirection + 2) & 0x3)", + " .regionID;", + " if (regionID === referenceCell.regionID ||", + " regionID === RasterizationCell.OBSTACLE_REGION_ID)", + " // The region away from the border is either a obstacle region", + " // or the same region. So we keep the current region.", + " //", + " // u u u u u u", + " // a a x or x a x <-- Potentially bad, but stuck with it.", + " // u a a u a a", + " return referenceCell.regionID;", + " // Candidate region for re-assignment.", + " var potentialRegion = regionID;", + " // Next we check the region opposite from the corner direction.", + " // If it is the current region, then we definitely can't", + " // change the region id without risk of splitting the region.", + " regionID = grid.getNeighbor(referenceCell, (cornerDirection + 2) & 0x3)", + " .regionID;", + " if (regionID === referenceCell.regionID ||", + " regionID === RasterizationCell.OBSTACLE_REGION_ID)", + " // The region opposite from the corner direction is", + " // either a obstacle region or the same region. So we", + " // keep the current region.", + " //", + " // u a u u x u", + " // b a x or b a x", + " // u a a u a a", + " return referenceCell.regionID;", + " // We have checked the early exit special cases. Now a generalized", + " // brute count is performed.", + " //", + " // Priority is given to the potential region. Here is why:", + " // (Highly unlikely worst case scenario)", + " //", + " // c c c c c c", + " // b a x -> b b x Select b even though b count == a count.", + " // b a a b a a", + " // Neighbors in potential region.", + " // We know this will have a minimum value of 1.", + " var potentialCount = 0;", + " // Neighbors in the cell's current region.", + " // We know this will have a minimum value of 2.", + " var currentCount = 0;", + " // Maximum edge case:", + " //", + " // b b b", + " // b a x", + " // b a a", + " //", + " // The maximum edge case for region A can't exist. It", + " // is filtered out during one of the earlier special cases", + " // handlers.", + " //", + " // Other cases may exist if more regions are involved.", + " // Such cases will tend to favor the current region.", + " for (var direction = 0; direction < 8; direction++) {", + " var regionID_1 = grid.getNeighbor(referenceCell, direction).regionID;", + " if (regionID_1 === referenceCell.regionID)", + " currentCount++;", + " else if (regionID_1 === potentialRegion)", + " potentialCount++;", + " }", + " return potentialCount < currentCount", + " ? referenceCell.regionID", + " : potentialRegion;", + " };", + " /**", + " * Returns the direction of the first neighbor in a non-obstacle region.", + " * @param grid", + " * @param cell The cell to check.", + " * @return The direction of the first neighbor in a non-obstacle region, or", + " * -1 if all neighbors are in the obstacle region.", + " */", + " ObstacleRegionBordersCleaner.prototype.getNonNullBorderDirection = function (grid, cell) {", + " // Search axis-neighbors.", + " for (var direction = 0; direction < RasterizationGrid.neighbor4Deltas.length; direction++) {", + " var delta = RasterizationGrid.neighbor4Deltas[direction];", + " var neighbor = grid.get(cell.x + delta.x, cell.y + delta.y);", + " if (neighbor.regionID !== RasterizationCell.OBSTACLE_REGION_ID)", + " // The neighbor is a obstacle region.", + " return direction;", + " }", + " // All neighbors are in a non-obstacle region.", + " return -1;", + " };", + " return ObstacleRegionBordersCleaner;", + "}());", + "", + "// This implementation is strongly inspired from a Java one", + "// by Stephen A. Pratt:", + "// http://www.critterai.org/projects/nmgen_study/", + "//", + "// Most of the comments were written by him and were adapted to fit this implementation.", + "// This implementation differs a bit from the original:", + "// - it's only 2D instead of 3D", + "// - it has less features (see TODO) and might have lesser performance", + "// - it uses objects for points instead of pointer-like in arrays of numbers", + "// - the rasterization comes from other sources because of the 2d focus", + "// - partialFloodRegion was rewritten to fix an issue", + "// - filterNonObstacleVertices was added", + "//", + "// The Java implementation was also inspired from Recast that can be found here:", + "// https://github.com/recastnavigation/recastnavigation", + "var NavMeshGenerator = /** @class */ (function () {", + " function NavMeshGenerator(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, isometricRatio) {", + " if (isometricRatio === void 0) { isometricRatio = 1; }", + " this.grid = new RasterizationGrid(areaLeftBound, areaTopBound, areaRightBound, areaBottomBound, rasterizationCellSize, ", + " // make cells square in the world", + " rasterizationCellSize / isometricRatio);", + " this.isometricRatio = isometricRatio;", + " this.obstacleRasterizer = new ObstacleRasterizer();", + " this.regionGenerator = new RegionGenerator();", + " this.contourBuilder = new ContourBuilder();", + " this.convexPolygonGenerator = new ConvexPolygonGenerator();", + " this.gridCoordinateConverter = new GridCoordinateConverter();", + " }", + " NavMeshGenerator.prototype.buildNavMesh = function (obstacles, obstacleCellPadding) {", + " var _this = this;", + " this.grid.clear();", + " this.obstacleRasterizer.rasterizeObstacles(this.grid, obstacles);", + " this.regionGenerator.generateDistanceField(this.grid);", + " this.regionGenerator.generateRegions(this.grid, obstacleCellPadding);", + " // It's probably not a good idea to expose the vectorization threshold.", + " // As stated in the parameter documentation, the value 1 gives good", + " // results in any situations.", + " var threshold = 1;", + " var contours = this.contourBuilder.buildContours(this.grid, threshold);", + " var meshField = this.convexPolygonGenerator.splitToConvexPolygons(contours, 16);", + " var scaledMeshField = this.gridCoordinateConverter.convertFromGridBasis(this.grid, meshField);", + " if (this.isometricRatio != 1) {", + " // Rescale the mesh to have the same unit length on the 2 axis for the pathfinding.", + " scaledMeshField.forEach(function (polygon) {", + " return polygon.forEach(function (point) {", + " point.y *= _this.isometricRatio;", + " });", + " });", + " }", + " return scaledMeshField;", + " };", + " return NavMeshGenerator;", + "}());", + "", + "/*", + "GDevelop - NavMesh Pathfinding Behavior Extension", + " */", + "/**", + " * PathfindingObstaclesManager manages the common objects shared by objects having a", + " * pathfinding behavior: In particular, the obstacles behaviors are required to declare", + " * themselves (see `PathfindingObstaclesManager.addObstacle`) to the manager of their associated scene", + " * (see `NavMeshPathfindingRuntimeBehavior.obstaclesManagers`).", + " */", + "var NavMeshPathfindingObstaclesManager = /** @class */ (function () {", + " function NavMeshPathfindingObstaclesManager(instanceContainer, configuration) {", + " /**", + " * The navigation meshes by moving object size", + " * (rounded on _cellSize)", + " */", + " this._navMeshes = new Map();", + " /**", + " * Used while NavMeshes update is disabled to remember to do the update", + " * when it's enable back.", + " */", + " this._navMeshesAreUpToDate = true;", + " /**", + " * This allows to continue finding paths with the old NavMeshes while", + " * moving obstacles.", + " */", + " this._navMeshesUpdateIsEnabled = true;", + " var viewpoint = configuration._getViewpoint();", + " if (viewpoint === 'Isometry 2:1 (26.565°)') {", + " configuration._setIsometricRatio(2);", + " }", + " else if (viewpoint === 'True Isometry (30°)') {", + " configuration._setIsometricRatio(Math.sqrt(3));", + " }", + " else {", + " configuration._setIsometricRatio(1);", + " }", + " if (configuration._getCellSize() <= 0) {", + " configuration._setCellSize(10);", + " }", + " if (configuration._getAreaLeftBound() === 0 &&", + " configuration._getAreaTopBound() === 0 &&", + " configuration._getAreaRightBound() === 0 &&", + " configuration._getAreaBottomBound() === 0) {", + " var game = instanceContainer.getGame();", + " configuration._setAreaLeftBound(0);", + " configuration._setAreaTopBound(0);", + " configuration._setAreaRightBound(game.getGameResolutionWidth());", + " configuration._setAreaBottomBound(game.getGameResolutionHeight());", + " }", + " this.configuration = configuration;", + " this._obstacles = new Set();", + " this._polygonIterableAdapter = new PolygonIterableAdapter();", + " this._navMeshGenerator = new NavMeshGenerator(configuration._getAreaLeftBound(), configuration._getAreaTopBound(), configuration._getAreaRightBound(), configuration._getAreaBottomBound(), configuration._getCellSize(), ", + " // make cells square in the world", + " configuration._getIsometricRatio());", + " }", + " /**", + " * Get the obstacles manager of a scene.", + " */", + " NavMeshPathfindingObstaclesManager.getManager = function (instanceContainer) {", + " // @ts-ignore", + " return instanceContainer.navMeshPathfindingObstaclesManager;", + " };", + " NavMeshPathfindingObstaclesManager.getManagerOrCreate = function (instanceContainer, configuration) {", + " // @ts-ignore", + " if (!instanceContainer.navMeshPathfindingObstaclesManager) {", + " // Create the shared manager if necessary.", + " // @ts-ignore", + " instanceContainer.navMeshPathfindingObstaclesManager = new NavMeshPathfindingObstaclesManager(instanceContainer, configuration);", + " }", + " // @ts-ignore", + " return instanceContainer.navMeshPathfindingObstaclesManager;", + " };", + " NavMeshPathfindingObstaclesManager.prototype.setNavMeshesUpdateEnabled = function (navMeshesUpdateIsEnabled) {", + " this._navMeshesUpdateIsEnabled = navMeshesUpdateIsEnabled;", + " if (navMeshesUpdateIsEnabled && !this._navMeshesAreUpToDate) {", + " this._navMeshes.clear();", + " this._navMeshesAreUpToDate = true;", + " }", + " };", + " /**", + " * Add a obstacle to the list of existing obstacles.", + " */", + " NavMeshPathfindingObstaclesManager.prototype.addObstacle = function (pathfindingObstacleBehavior) {", + " this._obstacles.add(pathfindingObstacleBehavior.behavior.owner);", + " this.invalidateNavMesh();", + " };", + " /**", + " * Remove a obstacle from the list of existing obstacles. Be sure that the obstacle was", + " * added before.", + " */", + " NavMeshPathfindingObstaclesManager.prototype.removeObstacle = function (pathfindingObstacleBehavior) {", + " this._obstacles.delete(pathfindingObstacleBehavior.behavior.owner);", + " this.invalidateNavMesh();", + " };", + " NavMeshPathfindingObstaclesManager.prototype.invalidateNavMesh = function () {", + " if (this._navMeshesUpdateIsEnabled) {", + " this._navMeshes.clear();", + " this._navMeshesAreUpToDate = true;", + " }", + " else {", + " this._navMeshesAreUpToDate = false;", + " }", + " };", + " NavMeshPathfindingObstaclesManager.prototype.getNavMesh = function (obstacleCellPadding) {", + " var navMesh = this._navMeshes.get(obstacleCellPadding);", + " if (!navMesh) {", + " var navMeshPolygons = this._navMeshGenerator.buildNavMesh(this._getVerticesIterable(this._obstacles), obstacleCellPadding);", + " navMesh = new NavMesh(navMeshPolygons);", + " this._navMeshes.set(obstacleCellPadding, navMesh);", + " }", + " return navMesh;", + " };", + " NavMeshPathfindingObstaclesManager.prototype._getVerticesIterable = function (objects) {", + " this._polygonIterableAdapter.set(objects);", + " return this._polygonIterableAdapter;", + " };", + " return NavMeshPathfindingObstaclesManager;", + "}());", + "/**", + " * Iterable that adapts `RuntimeObject` to `Iterable<{x: float y: float}>`.", + " *", + " * This is an allocation free iterable", + " * that can only do one iteration at a time.", + " */", + "var PolygonIterableAdapter = /** @class */ (function () {", + " function PolygonIterableAdapter() {", + " this.objects = [];", + " this.objectsItr = this.objects[Symbol.iterator]();", + " this.polygonsItr = [][Symbol.iterator]();", + " this.pointIterableAdapter = new PointIterableAdapter();", + " this.result = {", + " value: this.pointIterableAdapter,", + " done: false,", + " };", + " }", + " PolygonIterableAdapter.prototype.set = function (objects) {", + " this.objects = objects;", + " };", + " PolygonIterableAdapter.prototype[Symbol.iterator] = function () {", + " this.objectsItr = this.objects[Symbol.iterator]();", + " this.polygonsItr = [][Symbol.iterator]();", + " return this;", + " };", + " PolygonIterableAdapter.prototype.next = function () {", + " var polygonNext = this.polygonsItr.next();", + " while (polygonNext.done) {", + " var objectNext = this.objectsItr.next();", + " if (objectNext.done) {", + " // IteratorReturnResult require a defined value", + " // even though the spec state otherwise.", + " // So, this class can't be typed as an iterable.", + " this.result.value = undefined;", + " this.result.done = true;", + " return this.result;", + " }", + " this.polygonsItr = objectNext.value.getHitBoxes().values();", + " polygonNext = this.polygonsItr.next();", + " }", + " this.pointIterableAdapter.set(polygonNext.value.vertices);", + " this.result.value = this.pointIterableAdapter;", + " this.result.done = false;", + " return this.result;", + " };", + " return PolygonIterableAdapter;", + "}());", + "/**", + " * Iterable that adapts coordinates from `[int, int]` to `{x: int, y: int}`.", + " *", + " * This is an allocation free iterable", + " * that can only do one iteration at a time.", + " */", + "var PointIterableAdapter = /** @class */ (function () {", + " function PointIterableAdapter() {", + " this.vertices = [];", + " this.verticesItr = this.vertices[Symbol.iterator]();", + " this.result = {", + " value: { x: 0, y: 0 },", + " done: false,", + " };", + " }", + " PointIterableAdapter.prototype.set = function (vertices) {", + " this.vertices = vertices;", + " };", + " PointIterableAdapter.prototype[Symbol.iterator] = function () {", + " this.verticesItr = this.vertices[Symbol.iterator]();", + " return this;", + " };", + " PointIterableAdapter.prototype.next = function () {", + " var next = this.verticesItr.next();", + " if (next.done) {", + " return next;", + " }", + " this.result.value.x = next.value[0];", + " this.result.value.y = next.value[1];", + " return this.result;", + " };", + " return PointIterableAdapter;", + "}());", + "", + "/*", + "GDevelop - NavMesh Pathfinding Behavior Extension", + " */", + "/**", + " * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to", + " * follow a path computed to avoid obstacles.", + " */", + "var NavMeshRenderer = /** @class */ (function () {", + " function NavMeshRenderer() {", + " /** Used to draw traces for debugging */", + " this._lastUsedObstacleCellPadding = null;", + " }", + " NavMeshRenderer.prototype.setLastUsedObstacleCellPadding = function (lastUsedObstacleCellPadding) {", + " this._lastUsedObstacleCellPadding = lastUsedObstacleCellPadding;", + " };", + " NavMeshRenderer.prototype.render = function (instanceContainer, shapePainter) {", + " if (this._lastUsedObstacleCellPadding === null) {", + " return;", + " }", + " var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);", + " if (!manager) {", + " return;", + " }", + " var isometricRatio = manager.configuration._getIsometricRatio();", + " // TODO find a way to rebuild drawing only when necessary.", + " // Draw the navigation mesh on a shape painter object for debugging purpose", + " var navMesh = manager.getNavMesh(this._lastUsedObstacleCellPadding);", + " for (var _i = 0, _a = navMesh.getPolygons(); _i < _a.length; _i++) {", + " var navPoly = _a[_i];", + " var polygon = navPoly.getPoints();", + " if (polygon.length === 0)", + " continue;", + " for (var index = 1; index < polygon.length; index++) {", + " // It helps to spot vertices with 180° between edges.", + " shapePainter.drawCircle(polygon[index].x, polygon[index].y / isometricRatio, 3);", + " }", + " }", + " for (var _b = 0, _c = navMesh.getPolygons(); _b < _c.length; _b++) {", + " var navPoly = _c[_b];", + " var polygon = navPoly.getPoints();", + " if (polygon.length === 0)", + " continue;", + " shapePainter.beginFillPath(polygon[0].x, polygon[0].y / isometricRatio);", + " for (var index = 1; index < polygon.length; index++) {", + " shapePainter.drawPathLineTo(polygon[index].x, polygon[index].y / isometricRatio);", + " }", + " shapePainter.closePath();", + " shapePainter.endFillPath();", + " }", + " };", + " return NavMeshRenderer;", + "}());", + "", + "/**", + " * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to", + " * follow a path computed to avoid obstacles.", + " */", + "var PathFollower = /** @class */ (function () {", + " function PathFollower(configuration) {", + " // Attributes used for traveling on the path:", + " this._path = [];", + " this._speed = 0;", + " this._distanceOnSegment = 0;", + " this._totalSegmentDistance = 0;", + " this._currentSegment = 0;", + " this._movementAngle = 0;", + " this.configuration = configuration;", + " }", + " PathFollower.prototype.setSpeed = function (speed) {", + " this._speed = speed;", + " };", + " PathFollower.prototype.getSpeed = function () {", + " return this._speed;", + " };", + " PathFollower.prototype.getMovementAngle = function () {", + " return this._movementAngle;", + " };", + " PathFollower.prototype.movementAngleIsAround = function (degreeAngle, tolerance) {", + " return (Math.abs(gdjs.evtTools.common.angleDifference(this._movementAngle, degreeAngle)) <= tolerance);", + " };", + " PathFollower.prototype.getNodeX = function (index) {", + " if (index < this._path.length) {", + " return this._path[index][0];", + " }", + " return 0;", + " };", + " PathFollower.prototype.getNodeY = function (index) {", + " if (index < this._path.length) {", + " return this._path[index][1];", + " }", + " return 0;", + " };", + " PathFollower.prototype.getNextNodeIndex = function () {", + " return Math.min(this._currentSegment + 1, this._path.length - 1);", + " };", + " PathFollower.prototype.getNodeCount = function () {", + " return this._path.length;", + " };", + " PathFollower.prototype.getNextNodeX = function () {", + " if (this._path.length === 0) {", + " return 0;", + " }", + " var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);", + " return this._path[nextIndex][0];", + " };", + " PathFollower.prototype.getNextNodeY = function () {", + " if (this._path.length === 0) {", + " return 0;", + " }", + " var nextIndex = Math.min(this._currentSegment + 1, this._path.length - 1);", + " return this._path[nextIndex][1];", + " };", + " PathFollower.prototype.getPreviousNodeX = function () {", + " if (this._path.length === 0) {", + " return 0;", + " }", + " var previousIndex = Math.min(this._currentSegment, this._path.length - 1);", + " return this._path[previousIndex][0];", + " };", + " PathFollower.prototype.getPreviousNodeY = function () {", + " if (this._path.length === 0) {", + " return 0;", + " }", + " var previousIndex = Math.min(this._currentSegment, this._path.length - 1);", + " return this._path[previousIndex][1];", + " };", + " PathFollower.prototype.getDestinationX = function () {", + " if (this._path.length === 0) {", + " return 0;", + " }", + " return this._path[this._path.length - 1][0];", + " };", + " PathFollower.prototype.getDestinationY = function () {", + " if (this._path.length === 0) {", + " return 0;", + " }", + " return (this._path[this._path.length - 1][1]);", + " };", + " /**", + " * Return true if the object reached its destination.", + " */", + " PathFollower.prototype.destinationReached = function () {", + " return this._currentSegment >= this._path.length - 1;", + " };", + " /**", + " * Compute and move on the path to the specified destination.", + " */", + " PathFollower.prototype.setPath = function (path) {", + " this._path = path;", + " this._enterSegment(0);", + " };", + " PathFollower.prototype._enterSegment = function (segmentNumber) {", + " if (this._path.length === 0) {", + " return;", + " }", + " this._currentSegment = segmentNumber;", + " if (this._currentSegment < this._path.length - 1) {", + " var pathX = this._path[this._currentSegment + 1][0] -", + " this._path[this._currentSegment][0];", + " var pathY = this._path[this._currentSegment + 1][1] -", + " this._path[this._currentSegment][1];", + " this._totalSegmentDistance = Math.sqrt(pathX * pathX + pathY * pathY);", + " this._distanceOnSegment = 0;", + " this._movementAngle =", + " (gdjs.toDegrees(Math.atan2(pathY, pathX)) + 360) % 360;", + " }", + " else {", + " this._speed = 0;", + " }", + " };", + " PathFollower.prototype.isMoving = function () {", + " return !(this._path.length === 0 || this.destinationReached());", + " };", + " PathFollower.prototype.step = function (timeDelta) {", + " if (this._path.length === 0 || this.destinationReached()) {", + " return;", + " }", + " // Update the speed of the object", + " var previousSpeed = this._speed;", + " var maxSpeed = this.configuration._getMaxSpeed();", + " if (this._speed !== maxSpeed) {", + " this._speed += this.configuration._getAcceleration() * timeDelta;", + " if (this._speed > maxSpeed) {", + " this._speed = maxSpeed;", + " }", + " }", + " // Update the time on the segment and change segment if needed", + " // Use a Verlet integration to be frame rate independent.", + " this._distanceOnSegment +=", + " ((this._speed + previousSpeed) / 2) * timeDelta;", + " var remainingDistanceOnSegment = this._totalSegmentDistance - this._distanceOnSegment;", + " if (remainingDistanceOnSegment <= 0 &&", + " this._currentSegment < this._path.length) {", + " this._enterSegment(this._currentSegment + 1);", + " this._distanceOnSegment = -remainingDistanceOnSegment;", + " }", + " };", + " PathFollower.prototype.getX = function () {", + " return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][0], this._path[this._currentSegment + 1][0], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][0];", + " };", + " PathFollower.prototype.getY = function () {", + " return this._currentSegment < this._path.length - 1 ? gdjs.evtTools.common.lerp(this._path[this._currentSegment][1], this._path[this._currentSegment + 1][1], this._distanceOnSegment / this._totalSegmentDistance) : this._path[this._path.length - 1][1];", + " };", + " return PathFollower;", + "}());", + "", + "/**", + " * NavMeshPathfindingRuntimeBehavior represents a behavior allowing objects to", + " * follow a path computed to avoid obstacles.", + " */", + "var NavMeshPathfindingBehavior = /** @class */ (function () {", + " function NavMeshPathfindingBehavior(behavior) {", + " // Attributes used for traveling on the path:", + " this._pathFound = false;", + " this.behavior = behavior;", + " this.pathFollower = new PathFollower(behavior);", + " this.navMeshRenderer = new NavMeshRenderer();", + " }", + " /**", + " * Return true if the latest call to moveTo succeeded.", + " */", + " NavMeshPathfindingBehavior.prototype.pathFound = function () {", + " return this._pathFound;", + " };", + " /**", + " * Compute and move on the path to the specified destination.", + " */", + " NavMeshPathfindingBehavior.prototype.moveTo = function (instanceContainer, x, y) {", + " var owner = this.behavior.owner;", + " var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);", + " if (!manager) {", + " this._pathFound = true;", + " this.pathFollower.setPath([[owner.getX(), owner.getY()], [x, y]]);", + " return;", + " }", + " var isometricRatio = manager.configuration._getIsometricRatio();", + " var cellSize = manager.configuration._getCellSize();", + " var collisionShape = this.behavior._getCollisionShape();", + " var extraBorder = this.behavior._getExtraBorder();", + " var radiusSqMax = 0;", + " if (collisionShape !== 'Dot at center') {", + " var centerX = owner.getCenterXInScene();", + " var centerY = owner.getCenterYInScene();", + " for (var _i = 0, _a = owner.getHitBoxes(); _i < _a.length; _i++) {", + " var hitBox = _a[_i];", + " for (var _b = 0, _c = hitBox.vertices; _b < _c.length; _b++) {", + " var vertex = _c[_b];", + " var deltaX = vertex[0] - centerX;", + " // to have the same unit on x and y", + " var deltaY = (vertex[1] - centerY) * isometricRatio;", + " var radiusSq = deltaX * deltaX + deltaY * deltaY;", + " radiusSqMax = Math.max(radiusSq, radiusSqMax);", + " }", + " }", + " }", + " // Round to avoid to flicker between 2 NavMesh", + " // because of trigonometry rounding errors.", + " // Round the padding on cellSize to avoid almost identical NavMesh", + " var obstacleCellPadding = Math.max(0, Math.round((Math.sqrt(radiusSqMax) + extraBorder) / cellSize));", + " this.navMeshRenderer.setLastUsedObstacleCellPadding(obstacleCellPadding);", + " var navMesh = manager.getNavMesh(obstacleCellPadding);", + " // TODO avoid the path allocation", + " var path = navMesh.findPath({", + " x: owner.getX(),", + " y: owner.getY() * isometricRatio,", + " }, { x: x, y: y * isometricRatio }) || [];", + " this._pathFound = path.length > 0;", + " this.pathFollower.setPath(path.map(function (_a) {", + " var x = _a.x, y = _a.y;", + " return [x, y];", + " }));", + " };", + " NavMeshPathfindingBehavior.prototype.doStepPreEvents = function (instanceContainer) {", + " if (this.pathFollower.destinationReached()) {", + " return;", + " }", + " var manager = NavMeshPathfindingObstaclesManager.getManager(instanceContainer);", + " if (!manager) {", + " return;", + " }", + " var isometricRatio = manager.configuration._getIsometricRatio();", + " var owner = this.behavior.owner;", + " var angleOffset = this.behavior._getAngleOffset();", + " var angularMaxSpeed = this.behavior._getMaxSpeed();", + " var rotateObject = this.behavior._getRotateObject();", + " var timeDelta = owner.getElapsedTime(instanceContainer) / 1000;", + " this.pathFollower.step(timeDelta);", + " // Position object on the segment and update its angle", + " var movementAngle = this.pathFollower.getMovementAngle();", + " if (rotateObject &&", + " owner.getAngle() !== movementAngle + angleOffset) {", + " owner.rotateTowardAngle(movementAngle + angleOffset, angularMaxSpeed, instanceContainer);", + " }", + " owner.setX(this.pathFollower.getX());", + " // In case of isometry, convert coords back in screen.", + " owner.setY(this.pathFollower.getY() / isometricRatio);", + " };", + " return NavMeshPathfindingBehavior;", + "}());", + "", + "/*", + "GDevelop - NavMesh Pathfinding Behavior Extension", + " */", + "/**", + " * NavMeshPathfindingObstacleRuntimeBehavior represents a behavior allowing objects to be", + " * considered as a obstacle by objects having Pathfinding Behavior.", + " */", + "var NavMeshPathfindingObstacleBehavior = /** @class */ (function () {", + " function NavMeshPathfindingObstacleBehavior(instanceContainer, behavior) {", + " this._oldX = 0;", + " this._oldY = 0;", + " this._oldWidth = 0;", + " this._oldHeight = 0;", + " this._registeredInManager = false;", + " this.behavior = behavior;", + " this._manager = NavMeshPathfindingObstaclesManager.getManagerOrCreate(instanceContainer, ", + " // @ts-ignore", + " behavior._sharedData);", + " //Note that we can't use getX(), getWidth()... of owner here:", + " //The owner is not yet fully constructed.", + " }", + " NavMeshPathfindingObstacleBehavior.prototype.onDestroy = function () {", + " if (this._manager && this._registeredInManager) {", + " this._manager.removeObstacle(this);", + " }", + " };", + " NavMeshPathfindingObstacleBehavior.prototype.doStepPreEvents = function (instanceContainer) {", + " var owner = this.behavior.owner;", + " //Make sure the obstacle is or is not in the obstacles manager.", + " if (!this.behavior.activated() && this._registeredInManager) {", + " this._manager.removeObstacle(this);", + " this._registeredInManager = false;", + " }", + " else {", + " if (this.behavior.activated() && !this._registeredInManager) {", + " this._manager.addObstacle(this);", + " this._registeredInManager = true;", + " }", + " }", + " //Track changes in size or position", + " if (this._oldX !== owner.getX() ||", + " this._oldY !== owner.getY() ||", + " this._oldWidth !== owner.getWidth() ||", + " this._oldHeight !== owner.getHeight()) {", + " if (this._registeredInManager) {", + " this._manager.removeObstacle(this);", + " this._manager.addObstacle(this);", + " }", + " this._oldX = owner.getX();", + " this._oldY = owner.getY();", + " this._oldWidth = owner.getWidth();", + " this._oldHeight = owner.getHeight();", + " }", + " };", + " NavMeshPathfindingObstacleBehavior.prototype.doStepPostEvents = function (instanceContainer) { };", + " NavMeshPathfindingObstacleBehavior.prototype.onActivate = function () {", + " if (this._registeredInManager) {", + " return;", + " }", + " this._manager.addObstacle(this);", + " this._registeredInManager = true;", + " };", + " NavMeshPathfindingObstacleBehavior.prototype.onDeActivate = function () {", + " if (!this._registeredInManager) {", + " return;", + " }", + " this._manager.removeObstacle(this);", + " this._registeredInManager = false;", + " };", + " return NavMeshPathfindingObstacleBehavior;", + "}());", + "", + "gdjs.__NavMeshPathfinding = gdjs.__NavMeshPathfinding || {};", + "gdjs.__NavMeshPathfinding.NavMeshPathfindingBehavior = NavMeshPathfindingBehavior;", + "gdjs.__NavMeshPathfinding.NavMeshPathfindingObstacleBehavior = NavMeshPathfindingObstacleBehavior;", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -26694,7 +31111,14 @@ }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\r\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r\nconst behavior = object.getBehavior(behaviorName);\r\nbehavior.__NavMeshPathfinding = behavior.__NavMeshPathfinding || {};\r\nbehavior.__NavMeshPathfinding.pathfinding = new gdjs.__NavMeshPathfinding.NavMeshPathfindingBehavior(behavior);\r\n", + "inlineCode": [ + "const object = objects[0];\r", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r", + "const behavior = object.getBehavior(behaviorName);\r", + "behavior.__NavMeshPathfinding = behavior.__NavMeshPathfinding || {};\r", + "behavior.__NavMeshPathfinding.pathfinding = new gdjs.__NavMeshPathfinding.NavMeshPathfindingBehavior(behavior);\r", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": true @@ -26723,7 +31147,14 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\r\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r\nconst behavior = object.getBehavior(behaviorName);\r\n\r\nbehavior.__NavMeshPathfinding.pathfinding.doStepPreEvents(runtimeScene);\r\n", + "inlineCode": [ + "const object = objects[0];\r", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r", + "const behavior = object.getBehavior(behaviorName);\r", + "\r", + "behavior.__NavMeshPathfinding.pathfinding.doStepPreEvents(runtimeScene);\r", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": true @@ -26754,7 +31185,17 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst destinationX = eventsFunctionContext.getArgument(\"DestinationX\");\nconst destinationY = eventsFunctionContext.getArgument(\"DestinationY\");\n\nbehavior.__NavMeshPathfinding.pathfinding.moveTo(runtimeScene, destinationX, destinationY);\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "const destinationX = eventsFunctionContext.getArgument(\"DestinationX\");", + "const destinationY = eventsFunctionContext.getArgument(\"DestinationY\");", + "", + "behavior.__NavMeshPathfinding.pathfinding.moveTo(runtimeScene, destinationX, destinationY);", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": true @@ -26795,7 +31236,14 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.isMoving();\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.isMoving();", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -26826,7 +31274,14 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFound();\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFound();", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -26857,7 +31312,14 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.destinationReached();\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.destinationReached();", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -26888,7 +31350,14 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNodeCount();\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNodeCount();", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -26922,7 +31391,16 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst nodeIndex = eventsFunctionContext.getArgument(\"NodeIndex\");\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNodeX(nodeIndex);\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "const nodeIndex = eventsFunctionContext.getArgument(\"NodeIndex\");", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNodeX(nodeIndex);", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -26961,7 +31439,16 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst nodeIndex = eventsFunctionContext.getArgument(\"NodeIndex\");\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNodeY(nodeIndex);\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "const nodeIndex = eventsFunctionContext.getArgument(\"NodeIndex\");", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNodeY(nodeIndex);", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -27000,7 +31487,14 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNextNodeIndex();\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getNextNodeIndex();", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -27280,7 +31774,17 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst angle = eventsFunctionContext.getArgument(\"Angle\");\nconst tolerance = eventsFunctionContext.getArgument(\"Tolerance\");\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getMovementAngle(angle, tolerance);\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "const angle = eventsFunctionContext.getArgument(\"Angle\");", + "const tolerance = eventsFunctionContext.getArgument(\"Tolerance\");", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getMovementAngle(angle, tolerance);", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -27373,7 +31877,14 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getSpeed();\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.pathfinding.pathFollower.getSpeed();", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -27398,6 +31909,7 @@ "objectGroups": [] }, { + "description": "Draw the navigation mesh used for the object.", "fullName": "Draw navigation mesh", "functionType": "Action", "group": "Debug", @@ -27406,7 +31918,18 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\nconst shapePainters = eventsFunctionContext.getObjects(\"ShapePainter\");\n\nfor (const shapePainter of shapePainters) {\n behavior.__NavMeshPathfinding.pathfinding.navMeshRenderer.render(runtimeScene, shapePainter);\n}\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "const shapePainters = eventsFunctionContext.getObjects(\"ShapePainter\");", + "", + "for (const shapePainter of shapePainters) {", + " behavior.__NavMeshPathfinding.pathfinding.navMeshRenderer.render(runtimeScene, shapePainter);", + "}", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": true @@ -27969,12 +32492,13 @@ "objectGroups": [] }, { - "fullName": "", + "description": "Enable or disable the rotation of the object when following its path.", + "fullName": "Rotate object", "functionType": "Action", "getterName": "RotateObject", "group": "Pathfinding configuration (navigation mesh)", "name": "SetRotateObject", - "sentence": "", + "sentence": "Enable the rotation of _PARAM0_ on the path: _PARAM2_", "events": [ { "type": "BuiltinCommonInstructions::Standard", @@ -28168,7 +32692,16 @@ }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\r\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r\nconst behavior = object.getBehavior(behaviorName);\r\nbehavior.__NavMeshPathfinding = behavior.__NavMeshPathfinding || {};\r\nbehavior.__NavMeshPathfinding.obstacle =\r\n new gdjs.__NavMeshPathfinding.NavMeshPathfindingObstacleBehavior(\r\n runtimeScene, behavior);\r\n", + "inlineCode": [ + "const object = objects[0];\r", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r", + "const behavior = object.getBehavior(behaviorName);\r", + "behavior.__NavMeshPathfinding = behavior.__NavMeshPathfinding || {};\r", + "behavior.__NavMeshPathfinding.obstacle =\r", + " new gdjs.__NavMeshPathfinding.NavMeshPathfindingObstacleBehavior(\r", + " runtimeScene, behavior);\r", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": true @@ -28197,7 +32730,13 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\r\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r\nconst behavior = object.getBehavior(behaviorName);\r\n\r\nbehavior.__NavMeshPathfinding.obstacle.doStepPreEvents(runtimeScene);", + "inlineCode": [ + "const object = objects[0];\r", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\r", + "const behavior = object.getBehavior(behaviorName);\r", + "\r", + "behavior.__NavMeshPathfinding.obstacle.doStepPreEvents(runtimeScene);" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": true @@ -28683,7 +33222,14 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "const object = objects[0];\nconst behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");\nconst behavior = object.getBehavior(behaviorName);\n\neventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.obstacle._manager.invalidateNavMesh();\n", + "inlineCode": [ + "const object = objects[0];", + "const behaviorName = eventsFunctionContext.getBehaviorName(\"Behavior\");", + "const behavior = object.getBehavior(behaviorName);", + "", + "eventsFunctionContext.returnValue = behavior.__NavMeshPathfinding.obstacle._manager.invalidateNavMesh();", + "" + ], "parameterObjects": "Object", "useStrict": true, "eventsSheetExpanded": false @@ -28797,7 +33343,11 @@ "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/sort-ascending.svg", "shortDescription": "Create an illusion of depth by setting the Z-order based on the Y position of the object. Useful for isometric games, 2D games with a \"Top-Down\" view, RPG...", "version": "0.0.1", - "description": "Set the depth (Z-order) of the instance to the value of its Y position in the scene, creating an illusion of depth. The origin point of the object is used to determine the Z-order.\n\nUseful for isometric games, 2D games with a \"Top-Down\" view, RPG...", + "description": [ + "Set the depth (Z-order) of the instance to the value of its Y position in the scene, creating an illusion of depth. The origin point of the object is used to determine the Z-order.", + "", + "Useful for isometric games, 2D games with a \"Top-Down\" view, RPG..." + ], "tags": [ "z-order", "y-sort", @@ -28872,7 +33422,11 @@ "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/gamepad-variant-outline.svg", "shortDescription": "Add support for gamepads (or other controllers) to your game, giving access to information such as button presses, axis positions, trigger pressure, etc...", "version": "0.3.0", - "description": "Add support for gamepads (or other controllers) to your game, giving access to information such as button presses, axis positions, axis force, trigger pressure, deadzone for each gamepad, etc...\n\nUp to 4 gamepads can be connected. For each condition or expression, you'll have to enter the index of the gamepad to read. This is 1, 2, 3 or 4.", + "description": [ + "Add support for gamepads (or other controllers) to your game, giving access to information such as button presses, axis positions, axis force, trigger pressure, deadzone for each gamepad, etc...", + "", + "Up to 4 gamepads can be connected. For each condition or expression, you'll have to enter the index of the gamepad to read. This is 1, 2, 3 or 4." + ], "origin": { "identifier": "Gamepads", "name": "gdevelop-extension-store" @@ -28899,7 +33453,44 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst trigger = eventsFunctionContext.getArgument(\"trigger\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in expression: \"Pressure on a gamepad trigger\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (trigger != \"LT\" && trigger != \"RT\" && trigger != \"L2\" && trigger != \"R2\") {\r\n console.error('Parameter trigger is not valid in expression: \"Pressure on a gamepad trigger\"');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nswitch (trigger) {\r\n case 'LT':\r\n case 'L2':\r\n eventsFunctionContext.returnValue = gamepad.buttons[6].value;\r\n break;\r\n\r\n case 'RT':\r\n case 'R2':\r\n eventsFunctionContext.returnValue = gamepad.buttons[7].value;\r\n break;\r\n\r\n default:\r\n eventsFunctionContext.returnValue = -1;\r\n break;\r\n}", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameters\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "const trigger = eventsFunctionContext.getArgument(\"trigger\").toUpperCase();\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier in expression: \"Pressure on a gamepad trigger\", is not valid number, must be between 0 and 4.');\r", + " return;\r", + "}\r", + "if (trigger != \"LT\" && trigger != \"RT\" && trigger != \"L2\" && trigger != \"R2\") {\r", + " console.error('Parameter trigger is not valid in expression: \"Pressure on a gamepad trigger\"');\r", + " return;\r", + "}\r", + "\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r", + "if (gamepad == null) return;\r", + "\r", + "switch (trigger) {\r", + " case 'LT':\r", + " case 'L2':\r", + " eventsFunctionContext.returnValue = gamepad.buttons[6].value;\r", + " break;\r", + "\r", + " case 'RT':\r", + " case 'R2':\r", + " eventsFunctionContext.returnValue = gamepad.buttons[7].value;\r", + " break;\r", + "\r", + " default:\r", + " eventsFunctionContext.returnValue = -1;\r", + " break;\r", + "}" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -28932,7 +33523,45 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\n\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier is not valid in expression: \"Value of a stick force\"');\r\n return;\r\n}\r\n\r\nif (stick !== \"LEFT\" && stick !== \"RIGHT\") {\r\n console.error('Parameter stick is not valid in expression: \"Value of a stick force\"');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n eventsFunctionContext.returnValue = gdjs.evtTools.common.clamp(Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId)) + Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId)), 0, 1);\r\n break;\r\n\r\n case 'RIGHT':\r\n eventsFunctionContext.returnValue = gdjs.evtTools.common.clamp(Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId)) + Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId)), 0, 1);\r\n break;\r\n\r\n default:\r\n eventsFunctionContext.returnValue = -1;\r\n break;\r\n}", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameters\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "const stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r", + "\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier is not valid in expression: \"Value of a stick force\"');\r", + " return;\r", + "}\r", + "\r", + "if (stick !== \"LEFT\" && stick !== \"RIGHT\") {\r", + " console.error('Parameter stick is not valid in expression: \"Value of a stick force\"');\r", + " return;\r", + "}\r", + "\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r", + "if (gamepad == null) return;\r", + "\r", + "\r", + "switch (stick) {\r", + " case 'LEFT':\r", + " eventsFunctionContext.returnValue = gdjs.evtTools.common.clamp(Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId)) + Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId)), 0, 1);\r", + " break;\r", + "\r", + " case 'RIGHT':\r", + " eventsFunctionContext.returnValue = gdjs.evtTools.common.clamp(Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId)) + Math.abs(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId)), 0, 1);\r", + " break;\r", + "\r", + " default:\r", + " eventsFunctionContext.returnValue = -1;\r", + " break;\r", + "}" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -28965,7 +33594,42 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\n\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier is not valid in expression: \"Value of a stick rotation\"');\r\n return;\r\n}\r\nif (stick !== \"LEFT\" && stick !== \"RIGHT\") {\r\n console.error('Parameter stick is not valid in expression: \"Value of a stick rotation\"');\r\n return;\r\n}\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n eventsFunctionContext.returnValue = gdjs._extensionController.axisToAngle(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId), gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId));\r\n break;\r\n\r\n case 'RIGHT':\r\n eventsFunctionContext.returnValue = gdjs._extensionController.axisToAngle(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId), gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId));\r\n break;\r\n\r\n default:\r\n eventsFunctionContext.returnValue = -1;\r\n break;\r\n}", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameters\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "const stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r", + "\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier is not valid in expression: \"Value of a stick rotation\"');\r", + " return;\r", + "}\r", + "if (stick !== \"LEFT\" && stick !== \"RIGHT\") {\r", + " console.error('Parameter stick is not valid in expression: \"Value of a stick rotation\"');\r", + " return;\r", + "}\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r", + "if (gamepad == null) return;\r", + "\r", + "switch (stick) {\r", + " case 'LEFT':\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.axisToAngle(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId), gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId));\r", + " break;\r", + "\r", + " case 'RIGHT':\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.axisToAngle(gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId), gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId));\r", + " break;\r", + "\r", + " default:\r", + " eventsFunctionContext.returnValue = -1;\r", + " break;\r", + "}" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -28998,7 +33662,117 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\nconst direction = eventsFunctionContext.getArgument(\"direction\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier is not valid in expression: \"Value of a gamepad axis\"');\r\n return;\r\n}\r\nif (stick != \"LEFT\" && stick != \"RIGHT\") {\r\n console.error('Parameter stick is not valid in expression: \"Value of a gamepad axis\"');\r\n return;\r\n}\r\nif (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\" && direction != \"HORIZONTAL\" && direction != \"VERTICAL\") {\r\n console.error('Parameter direction is not valid in expression: \"Value of a gamepad axis\"');\r\n return;\r\n}\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nlet parameterError = false;\r\nswitch (stick) {\r\n case 'LEFT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r\n }\r\n break;\r\n\r\n case \"HORIZONTAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r\n break;\r\n\r\n case \"VERTICAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r\n break;\r\n\r\n default:\r\n break;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId) < 0) {\r\n eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r\n }\r\n break;\r\n\r\n case \"HORIZONTAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r\n break;\r\n\r\n case \"VERTICAL\":\r\n eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r\n break;\r\n\r\n default:\r\n break;\r\n }\r\n break;\r\n\r\n default:\r\n break;\r\n}\r\n", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameters\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "const stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r", + "const direction = eventsFunctionContext.getArgument(\"direction\").toUpperCase();\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier is not valid in expression: \"Value of a gamepad axis\"');\r", + " return;\r", + "}\r", + "if (stick != \"LEFT\" && stick != \"RIGHT\") {\r", + " console.error('Parameter stick is not valid in expression: \"Value of a gamepad axis\"');\r", + " return;\r", + "}\r", + "if (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\" && direction != \"HORIZONTAL\" && direction != \"VERTICAL\") {\r", + " console.error('Parameter direction is not valid in expression: \"Value of a gamepad axis\"');\r", + " return;\r", + "}\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r", + "if (gamepad == null) return;\r", + "\r", + "let parameterError = false;\r", + "switch (stick) {\r", + " case 'LEFT':\r", + " switch (direction) {\r", + " case 'LEFT':\r", + " if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId) < 0) {\r", + " eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r", + " }\r", + " break;\r", + "\r", + " case 'RIGHT':\r", + " if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r", + " }\r", + " break;\r", + "\r", + " case 'UP':\r", + " if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId) < 0) {\r", + " eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r", + " }\r", + " break;\r", + "\r", + " case 'DOWN':\r", + " if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r", + " }\r", + " break;\r", + "\r", + " case \"HORIZONTAL\":\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[0], playerId);\r", + " break;\r", + "\r", + " case \"VERTICAL\":\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[1], playerId);\r", + " break;\r", + "\r", + " default:\r", + " break;\r", + " }\r", + " break;\r", + "\r", + " case 'RIGHT':\r", + " switch (direction) {\r", + " case 'LEFT':\r", + " if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId) < 0) {\r", + " eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r", + " }\r", + " break;\r", + "\r", + " case 'RIGHT':\r", + " if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r", + " }\r", + " break;\r", + "\r", + " case 'UP':\r", + " if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId) < 0) {\r", + " eventsFunctionContext.returnValue = -gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r", + " }\r", + " break;\r", + "\r", + " case 'DOWN':\r", + " if (gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r", + " }\r", + " break;\r", + "\r", + " case \"HORIZONTAL\":\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[2], playerId);\r", + " break;\r", + "\r", + " case \"VERTICAL\":\r", + " eventsFunctionContext.returnValue = gdjs._extensionController.getNormalizedAxisValue(gamepad.axes[3], playerId);\r", + " break;\r", + "\r", + " default:\r", + " break;\r", + " }\r", + " break;\r", + "\r", + " default:\r", + " break;\r", + "}\r", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29037,7 +33811,138 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst button = eventsFunctionContext.getArgument(\"button\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Gamepad button released\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (button === \"\") {\r\n console.error('Parameter button is not valid in condition: \"Gamepad button released\"');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nlet buttonId;\r\n\r\nswitch (button) {\r\n case 'A':\r\n case 'CROSS':\r\n buttonId = 0;\r\n break;\r\n case 'B':\r\n case 'CIRCLE':\r\n buttonId = 1;\r\n break;\r\n case 'X':\r\n case 'SQUARE':\r\n buttonId = 2;\r\n break;\r\n case 'Y':\r\n case 'TRIANGLE':\r\n buttonId = 3;\r\n break;\r\n case 'LB':\r\n case 'L1':\r\n buttonId = 4;\r\n break;\r\n case 'RB':\r\n case 'R1':\r\n buttonId = 5;\r\n break;\r\n case 'LT':\r\n case 'L2':\r\n buttonId = 6;\r\n break;\r\n case 'RT':\r\n case 'R2':\r\n buttonId = 7;\r\n break;\r\n\r\n case 'UP':\r\n buttonId = 12;\r\n break;\r\n case 'DOWN':\r\n buttonId = 13;\r\n break;\r\n case 'LEFT':\r\n buttonId = 14;\r\n break;\r\n case 'RIGHT':\r\n buttonId = 15;\r\n break;\r\n\r\n case 'BACK':\r\n case 'SHARE':\r\n buttonId = 8;\r\n break;\r\n case 'START':\r\n case 'OPTIONS':\r\n buttonId = 9;\r\n break;\r\n\r\n case 'CLICK_STICK_LEFT':\r\n buttonId = 10;\r\n break;\r\n case 'CLICK_STICK_RIGHT':\r\n buttonId = 11;\r\n break;\r\n\r\n //PS4\r\n case 'PS_BUTTON':\r\n buttonId = 16;\r\n break;\r\n case 'CLICK_TOUCHPAD':\r\n buttonId = 17;\r\n break;\r\n\r\n default:\r\n console.error('The button: ' + button + ' in condition: \"Gamepad button released\" is not valid.');\r\n break;\r\n}\r\n\r\nif (buttonId === undefined) {\r\n console.error('There is no buttons valid in condition: \"Gamepad button released\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\nif (gamepad.buttons == null || gamepad.buttons[buttonId] == null) {\r\n console.error('Buttons on the gamepad are not accessible in condition: \"Gamepad button released\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n//Define default value on pressed button or use previous value\r\ngdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] || { pressed: false };\r\n\r\n//Get state of button at previous frame\r\nconst previousStateButton = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed;\r\n\r\n//When previousStateButton is true and actual button state is not pressed\r\n//Player have release the button\r\nif (previousStateButton === true && gamepad.buttons[buttonId].pressed === false) {\r\n // Save the last button used for the player \r\n gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\n gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = true;\r\n eventsFunctionContext.returnValue = true;\r\n\r\n} else {\r\n gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = false;\r\n eventsFunctionContext.returnValue = false;\r\n}\r\n", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameters\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "const button = eventsFunctionContext.getArgument(\"button\").toUpperCase();\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier in condition: \"Gamepad button released\", is not valid number, must be between 0 and 4.');\r", + " return;\r", + "}\r", + "if (button === \"\") {\r", + " console.error('Parameter button is not valid in condition: \"Gamepad button released\"');\r", + " return;\r", + "}\r", + "\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r", + "if (gamepad == null) return;\r", + "\r", + "let buttonId;\r", + "\r", + "switch (button) {\r", + " case 'A':\r", + " case 'CROSS':\r", + " buttonId = 0;\r", + " break;\r", + " case 'B':\r", + " case 'CIRCLE':\r", + " buttonId = 1;\r", + " break;\r", + " case 'X':\r", + " case 'SQUARE':\r", + " buttonId = 2;\r", + " break;\r", + " case 'Y':\r", + " case 'TRIANGLE':\r", + " buttonId = 3;\r", + " break;\r", + " case 'LB':\r", + " case 'L1':\r", + " buttonId = 4;\r", + " break;\r", + " case 'RB':\r", + " case 'R1':\r", + " buttonId = 5;\r", + " break;\r", + " case 'LT':\r", + " case 'L2':\r", + " buttonId = 6;\r", + " break;\r", + " case 'RT':\r", + " case 'R2':\r", + " buttonId = 7;\r", + " break;\r", + "\r", + " case 'UP':\r", + " buttonId = 12;\r", + " break;\r", + " case 'DOWN':\r", + " buttonId = 13;\r", + " break;\r", + " case 'LEFT':\r", + " buttonId = 14;\r", + " break;\r", + " case 'RIGHT':\r", + " buttonId = 15;\r", + " break;\r", + "\r", + " case 'BACK':\r", + " case 'SHARE':\r", + " buttonId = 8;\r", + " break;\r", + " case 'START':\r", + " case 'OPTIONS':\r", + " buttonId = 9;\r", + " break;\r", + "\r", + " case 'CLICK_STICK_LEFT':\r", + " buttonId = 10;\r", + " break;\r", + " case 'CLICK_STICK_RIGHT':\r", + " buttonId = 11;\r", + " break;\r", + "\r", + " //PS4\r", + " case 'PS_BUTTON':\r", + " buttonId = 16;\r", + " break;\r", + " case 'CLICK_TOUCHPAD':\r", + " buttonId = 17;\r", + " break;\r", + "\r", + " default:\r", + " console.error('The button: ' + button + ' in condition: \"Gamepad button released\" is not valid.');\r", + " break;\r", + "}\r", + "\r", + "if (buttonId === undefined) {\r", + " console.error('There is no buttons valid in condition: \"Gamepad button released\"');\r", + " eventsFunctionContext.returnValue = false;\r", + " return;\r", + "}\r", + "\r", + "if (gamepad.buttons == null || gamepad.buttons[buttonId] == null) {\r", + " console.error('Buttons on the gamepad are not accessible in condition: \"Gamepad button released\"');\r", + " eventsFunctionContext.returnValue = false;\r", + " return;\r", + "}\r", + "\r", + "//Define default value on pressed button or use previous value\r", + "gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] || { pressed: false };\r", + "\r", + "//Get state of button at previous frame\r", + "const previousStateButton = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed;\r", + "\r", + "//When previousStateButton is true and actual button state is not pressed\r", + "//Player have release the button\r", + "if (previousStateButton === true && gamepad.buttons[buttonId].pressed === false) {\r", + " // Save the last button used for the player \r", + " gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r", + " gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = true;\r", + " eventsFunctionContext.returnValue = true;\r", + "\r", + "} else {\r", + " gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = false;\r", + " eventsFunctionContext.returnValue = false;\r", + "}\r", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29067,7 +33972,19 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\n//Player id is not valid\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in expression: \"Last pressed button (id)\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\n\r\n//Return the last button used by the player\r\neventsFunctionContext.returnValue = gdjs._extensionController.players[playerId].lastButtonUsed;", + "inlineCode": [ + "//Get function parameter\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "\r", + "//Player id is not valid\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier in expression: \"Last pressed button (id)\", is not valid number, must be between 0 and 4.');\r", + " return;\r", + "}\r", + "\r", + "//Return the last button used by the player\r", + "eventsFunctionContext.returnValue = gdjs._extensionController.players[playerId].lastButtonUsed;" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29094,7 +34011,49 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Any gamepad button pressed\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nlet buttonId;\r\nfor (let i = 0; i < gamepad.buttons.length; i++) { //For each buttons\r\n if (gamepad.buttons[i].pressed) { //One of them is pressed\r\n buttonId = i; //Save the button pressed\r\n break;\r\n }\r\n}\r\n\r\nif (buttonId === undefined) {\r\n // No buttons are pressed.\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\nif (gamepad.buttons == null || gamepad.buttons[buttonId] == null) {\r\n console.error('Buttons on the gamepad are not accessible in condition: \"Any gamepad button pressed\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n//When a button is pressed, save the button in lastButtonUsed for each players\r\nif (gamepad.buttons[buttonId].pressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\neventsFunctionContext.returnValue = gamepad.buttons[buttonId].pressed;\r\n\r\n\r\n", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameter\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier in condition: \"Any gamepad button pressed\", is not valid number, must be between 0 and 4.');\r", + " return;\r", + "}\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r", + "if (gamepad == null) return;\r", + "\r", + "let buttonId;\r", + "for (let i = 0; i < gamepad.buttons.length; i++) { //For each buttons\r", + " if (gamepad.buttons[i].pressed) { //One of them is pressed\r", + " buttonId = i; //Save the button pressed\r", + " break;\r", + " }\r", + "}\r", + "\r", + "if (buttonId === undefined) {\r", + " // No buttons are pressed.\r", + " eventsFunctionContext.returnValue = false;\r", + " return;\r", + "}\r", + "\r", + "if (gamepad.buttons == null || gamepad.buttons[buttonId] == null) {\r", + " console.error('Buttons on the gamepad are not accessible in condition: \"Any gamepad button pressed\"');\r", + " eventsFunctionContext.returnValue = false;\r", + " return;\r", + "}\r", + "\r", + "//When a button is pressed, save the button in lastButtonUsed for each players\r", + "if (gamepad.buttons[buttonId].pressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r", + "eventsFunctionContext.returnValue = gamepad.buttons[buttonId].pressed;\r", + "\r", + "\r", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29118,7 +34077,37 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst controllerType = eventsFunctionContext.getArgument(\"controller_type\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in string expression: \"Last pressed button (LastButtonString)\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (controllerType === \"\") {\r\n console.error('Parameter controller type is not valid in string expression: \"Last pressed button (LastButtonString)\"');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\nif (gamepad !== null) { //Gamepad exist\r\n //Get last btn id\r\n const lastButtonUsedID = gdjs._extensionController.players[playerId].lastButtonUsed;\r\n\r\n //Return last button as string \r\n eventsFunctionContext.returnValue = gdjs._extensionController.getInputString(controllerType, lastButtonUsedID);\r\n\r\n} else { //Gamepad dosen't exist\r\n console.error('Your controller is not supported or the gamepad wasn\\'t detected in string expression: \"Last pressed button (LastButtonString)\"');\r\n eventsFunctionContext.returnValue = \"Gamepad not connected\";\r\n}", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameters\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "const controllerType = eventsFunctionContext.getArgument(\"controller_type\").toUpperCase();\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier in string expression: \"Last pressed button (LastButtonString)\", is not valid number, must be between 0 and 4.');\r", + " return;\r", + "}\r", + "if (controllerType === \"\") {\r", + " console.error('Parameter controller type is not valid in string expression: \"Last pressed button (LastButtonString)\"');\r", + " return;\r", + "}\r", + "\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "if (gamepad !== null) { //Gamepad exist\r", + " //Get last btn id\r", + " const lastButtonUsedID = gdjs._extensionController.players[playerId].lastButtonUsed;\r", + "\r", + " //Return last button as string \r", + " eventsFunctionContext.returnValue = gdjs._extensionController.getInputString(controllerType, lastButtonUsedID);\r", + "\r", + "} else { //Gamepad dosen't exist\r", + " console.error('Your controller is not supported or the gamepad wasn\\'t detected in string expression: \"Last pressed button (LastButtonString)\"');\r", + " eventsFunctionContext.returnValue = \"Gamepad not connected\";\r", + "}" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29151,7 +34140,130 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst button = eventsFunctionContext.getArgument(\"button\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Gamepad button pressed\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (button === \"\") {\r\n console.error('Parameter button is not valid in condition: \"Gamepad button pressed\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nlet buttonId;\r\n\r\nswitch (button) {\r\n case 'A':\r\n case 'CROSS':\r\n buttonId = 0;\r\n break;\r\n case 'B':\r\n case 'CIRCLE':\r\n buttonId = 1;\r\n break;\r\n case 'X':\r\n case 'SQUARE':\r\n buttonId = 2;\r\n break;\r\n case 'Y':\r\n case 'TRIANGLE':\r\n buttonId = 3;\r\n break;\r\n case 'LB':\r\n case 'L1':\r\n buttonId = 4;\r\n break;\r\n case 'RB':\r\n case 'R1':\r\n buttonId = 5;\r\n break;\r\n case 'LT':\r\n case 'L2':\r\n buttonId = 6;\r\n break;\r\n case 'RT':\r\n case 'R2':\r\n buttonId = 7;\r\n break;\r\n\r\n case 'UP':\r\n buttonId = 12;\r\n break;\r\n case 'DOWN':\r\n buttonId = 13;\r\n break;\r\n case 'LEFT':\r\n buttonId = 14;\r\n break;\r\n case 'RIGHT':\r\n buttonId = 15;\r\n break;\r\n\r\n case 'BACK':\r\n case 'SHARE':\r\n buttonId = 8;\r\n break;\r\n case 'START':\r\n case 'OPTIONS':\r\n buttonId = 9;\r\n break;\r\n\r\n case 'CLICK_STICK_LEFT':\r\n buttonId = 10;\r\n break;\r\n case 'CLICK_STICK_RIGHT':\r\n buttonId = 11;\r\n break;\r\n\r\n //PS4\r\n case 'PS_BUTTON':\r\n buttonId = 16;\r\n break;\r\n case 'CLICK_TOUCHPAD':\r\n buttonId = 17;\r\n break;\r\n\r\n default:\r\n console.error('The button: ' + button + ' in condition: \"Gamepad button pressed\" is not valid.');\r\n eventsFunctionContext.returnValue = false;\r\n break;\r\n}\r\n\r\n\r\n\r\nif (buttonId === undefined) {\r\n console.error('There is no buttons valid in condition: \"Gamepad button pressed\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\nif (gamepad.buttons == null || gamepad.buttons[buttonId] == null) {\r\n console.error('Buttons on the gamepad are not accessible in condition: \"Gamepad button pressed\"');\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n//When a button is pressed, save the button in lastButtonUsed for each players\r\nif (gamepad.buttons[buttonId].pressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\neventsFunctionContext.returnValue = gamepad.buttons[buttonId].pressed;\r\n\r\n\r\n\r\n", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameters\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "const button = eventsFunctionContext.getArgument(\"button\").toUpperCase();\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier in condition: \"Gamepad button pressed\", is not valid number, must be between 0 and 4.');\r", + " return;\r", + "}\r", + "if (button === \"\") {\r", + " console.error('Parameter button is not valid in condition: \"Gamepad button pressed\"');\r", + " eventsFunctionContext.returnValue = false;\r", + " return;\r", + "}\r", + "\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r", + "if (gamepad == null) return;\r", + "\r", + "let buttonId;\r", + "\r", + "switch (button) {\r", + " case 'A':\r", + " case 'CROSS':\r", + " buttonId = 0;\r", + " break;\r", + " case 'B':\r", + " case 'CIRCLE':\r", + " buttonId = 1;\r", + " break;\r", + " case 'X':\r", + " case 'SQUARE':\r", + " buttonId = 2;\r", + " break;\r", + " case 'Y':\r", + " case 'TRIANGLE':\r", + " buttonId = 3;\r", + " break;\r", + " case 'LB':\r", + " case 'L1':\r", + " buttonId = 4;\r", + " break;\r", + " case 'RB':\r", + " case 'R1':\r", + " buttonId = 5;\r", + " break;\r", + " case 'LT':\r", + " case 'L2':\r", + " buttonId = 6;\r", + " break;\r", + " case 'RT':\r", + " case 'R2':\r", + " buttonId = 7;\r", + " break;\r", + "\r", + " case 'UP':\r", + " buttonId = 12;\r", + " break;\r", + " case 'DOWN':\r", + " buttonId = 13;\r", + " break;\r", + " case 'LEFT':\r", + " buttonId = 14;\r", + " break;\r", + " case 'RIGHT':\r", + " buttonId = 15;\r", + " break;\r", + "\r", + " case 'BACK':\r", + " case 'SHARE':\r", + " buttonId = 8;\r", + " break;\r", + " case 'START':\r", + " case 'OPTIONS':\r", + " buttonId = 9;\r", + " break;\r", + "\r", + " case 'CLICK_STICK_LEFT':\r", + " buttonId = 10;\r", + " break;\r", + " case 'CLICK_STICK_RIGHT':\r", + " buttonId = 11;\r", + " break;\r", + "\r", + " //PS4\r", + " case 'PS_BUTTON':\r", + " buttonId = 16;\r", + " break;\r", + " case 'CLICK_TOUCHPAD':\r", + " buttonId = 17;\r", + " break;\r", + "\r", + " default:\r", + " console.error('The button: ' + button + ' in condition: \"Gamepad button pressed\" is not valid.');\r", + " eventsFunctionContext.returnValue = false;\r", + " break;\r", + "}\r", + "\r", + "\r", + "\r", + "if (buttonId === undefined) {\r", + " console.error('There is no buttons valid in condition: \"Gamepad button pressed\"');\r", + " eventsFunctionContext.returnValue = false;\r", + " return;\r", + "}\r", + "\r", + "if (gamepad.buttons == null || gamepad.buttons[buttonId] == null) {\r", + " console.error('Buttons on the gamepad are not accessible in condition: \"Gamepad button pressed\"');\r", + " eventsFunctionContext.returnValue = false;\r", + " return;\r", + "}\r", + "\r", + "//When a button is pressed, save the button in lastButtonUsed for each players\r", + "if (gamepad.buttons[buttonId].pressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r", + "eventsFunctionContext.returnValue = gamepad.buttons[buttonId].pressed;\r", + "\r", + "\r", + "\r", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29186,7 +34298,17 @@ }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in expression: \"Gamepad deadzone for sticks\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\n///Return the deadzone value for a given player\r\neventsFunctionContext.returnValue = gdjs._extensionController.players[playerId].deadzone;", + "inlineCode": [ + "//Get function parameter\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier in expression: \"Gamepad deadzone for sticks\", is not valid number, must be between 0 and 4.');\r", + " return;\r", + "}\r", + "///Return the deadzone value for a given player\r", + "eventsFunctionContext.returnValue = gdjs._extensionController.players[playerId].deadzone;" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29213,7 +34335,21 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "//Get function parameter\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst newDeadzone = eventsFunctionContext.getArgument(\"deadzone\");\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in action: \"Set gamepad deadzone for sticks\", is not valid, must be between 0 and 4.');\r\n return;\r\n}\r\n\r\n// clamp the newDeadzone in range [0, 1].\r\n// https://github.com/4ian/GDevelop-extensions/pull/33#issuecomment-618224857\r\ngdjs._extensionController.players[playerId].deadzone = gdjs.evtTools.common.clamp(newDeadzone, 0, 1);\r\n", + "inlineCode": [ + "//Get function parameter\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "const newDeadzone = eventsFunctionContext.getArgument(\"deadzone\");\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier in action: \"Set gamepad deadzone for sticks\", is not valid, must be between 0 and 4.');\r", + " return;\r", + "}\r", + "\r", + "// clamp the newDeadzone in range [0, 1].\r", + "// https://github.com/4ian/GDevelop-extensions/pull/33#issuecomment-618224857\r", + "gdjs._extensionController.players[playerId].deadzone = gdjs.evtTools.common.clamp(newDeadzone, 0, 1);\r", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29242,7 +34378,144 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\nconst stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r\nconst direction = eventsFunctionContext.getArgument(\"direction\").toUpperCase();\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n console.error('Parameter gamepad identifier in condition: \"Gamepad stick pushed (axis)\", is not valid number, must be between 0 and 4.');\r\n return;\r\n}\r\nif (stick != \"LEFT\" && stick != \"RIGHT\") {\r\n console.error('Parameter stick in condition: \"Gamepad stick pushed (axis)\", is not valid, must be LEFT or RIGHT');\r\n return;\r\n}\r\nif (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\" && direction != \"ANY\") {\r\n console.error('Parameter deadzone in condition: \"Gamepad stick pushed (axis)\", is not valid, must be UP, DOWN, LEFT or RIGHT');\r\n return;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) {\r\n eventsFunctionContext.returnValue = false;\r\n return;\r\n}\r\n\r\n\r\n//Define in onFirstSceneLoaded function\r\nconst getNormalizedAxisValue = gdjs._extensionController.getNormalizedAxisValue;\r\n\r\nswitch (stick) {\r\n case 'LEFT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (getNormalizedAxisValue(gamepad.axes[0], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (getNormalizedAxisValue(gamepad.axes[0], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (getNormalizedAxisValue(gamepad.axes[1], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'ANY':\r\n if ( getNormalizedAxisValue(gamepad.axes[0], playerId) < 0\r\n || getNormalizedAxisValue(gamepad.axes[0], playerId) > 0\r\n || getNormalizedAxisValue(gamepad.axes[1], playerId) < 0 \r\n || getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n default:\r\n console.error('The value Direction on stick Left on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r\n eventsFunctionContext.returnValue = false;\r\n break;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n switch (direction) {\r\n case 'LEFT':\r\n if (getNormalizedAxisValue(gamepad.axes[2], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'RIGHT':\r\n if (getNormalizedAxisValue(gamepad.axes[2], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'UP':\r\n if (getNormalizedAxisValue(gamepad.axes[3], playerId) < 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'DOWN':\r\n if (getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n case 'ANY':\r\n if ( getNormalizedAxisValue(gamepad.axes[2], playerId) < 0\r\n || getNormalizedAxisValue(gamepad.axes[2], playerId) > 0\r\n || getNormalizedAxisValue(gamepad.axes[3], playerId) < 0 \r\n || getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r\n eventsFunctionContext.returnValue = true;\r\n return;\r\n }\r\n break;\r\n\r\n default:\r\n console.error('The value Direction on stick Right on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r\n eventsFunctionContext.returnValue = false;\r\n break;\r\n }\r\n break;\r\n\r\n default:\r\n console.error('The value Stick on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r\n eventsFunctionContext.returnValue = false;\r\n break;\r\n}\r\n\r\neventsFunctionContext.returnValue = false;\r\n", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameters\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "const stick = eventsFunctionContext.getArgument(\"stick\").toUpperCase();\r", + "const direction = eventsFunctionContext.getArgument(\"direction\").toUpperCase();\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + " console.error('Parameter gamepad identifier in condition: \"Gamepad stick pushed (axis)\", is not valid number, must be between 0 and 4.');\r", + " return;\r", + "}\r", + "if (stick != \"LEFT\" && stick != \"RIGHT\") {\r", + " console.error('Parameter stick in condition: \"Gamepad stick pushed (axis)\", is not valid, must be LEFT or RIGHT');\r", + " return;\r", + "}\r", + "if (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\" && direction != \"ANY\") {\r", + " console.error('Parameter deadzone in condition: \"Gamepad stick pushed (axis)\", is not valid, must be UP, DOWN, LEFT or RIGHT');\r", + " return;\r", + "}\r", + "\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r", + "if (gamepad == null) {\r", + " eventsFunctionContext.returnValue = false;\r", + " return;\r", + "}\r", + "\r", + "\r", + "//Define in onFirstSceneLoaded function\r", + "const getNormalizedAxisValue = gdjs._extensionController.getNormalizedAxisValue;\r", + "\r", + "switch (stick) {\r", + " case 'LEFT':\r", + " switch (direction) {\r", + " case 'LEFT':\r", + " if (getNormalizedAxisValue(gamepad.axes[0], playerId) < 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " case 'RIGHT':\r", + " if (getNormalizedAxisValue(gamepad.axes[0], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " case 'UP':\r", + " if (getNormalizedAxisValue(gamepad.axes[1], playerId) < 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " case 'DOWN':\r", + " if (getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " case 'ANY':\r", + " if ( getNormalizedAxisValue(gamepad.axes[0], playerId) < 0\r", + " || getNormalizedAxisValue(gamepad.axes[0], playerId) > 0\r", + " || getNormalizedAxisValue(gamepad.axes[1], playerId) < 0 \r", + " || getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " default:\r", + " console.error('The value Direction on stick Left on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r", + " eventsFunctionContext.returnValue = false;\r", + " break;\r", + " }\r", + " break;\r", + "\r", + " case 'RIGHT':\r", + " switch (direction) {\r", + " case 'LEFT':\r", + " if (getNormalizedAxisValue(gamepad.axes[2], playerId) < 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " case 'RIGHT':\r", + " if (getNormalizedAxisValue(gamepad.axes[2], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " case 'UP':\r", + " if (getNormalizedAxisValue(gamepad.axes[3], playerId) < 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " case 'DOWN':\r", + " if (getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " case 'ANY':\r", + " if ( getNormalizedAxisValue(gamepad.axes[2], playerId) < 0\r", + " || getNormalizedAxisValue(gamepad.axes[2], playerId) > 0\r", + " || getNormalizedAxisValue(gamepad.axes[3], playerId) < 0 \r", + " || getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", + " default:\r", + " console.error('The value Direction on stick Right on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r", + " eventsFunctionContext.returnValue = false;\r", + " break;\r", + " }\r", + " break;\r", + "\r", + " default:\r", + " console.error('The value Stick on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r", + " eventsFunctionContext.returnValue = false;\r", + " break;\r", + "}\r", + "\r", + "eventsFunctionContext.returnValue = false;\r", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29283,7 +34556,14 @@ }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n// Gamepads can be disconnected and become null, so we have to filter them.\r\neventsFunctionContext.returnValue = Object.keys(gamepads).filter(key => !!gamepads[key]).length;\r\n", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "// Gamepads can be disconnected and become null, so we have to filter them.\r", + "eventsFunctionContext.returnValue = Object.keys(gamepads).filter(key => !!gamepads[key]).length;\r", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29304,7 +34584,26 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameter\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in string expression: \"Gamepad type\", is not valid number, must be between 0 and 4');\n return;\n}\n\nconst gamepad = gamepads[playerId];\n\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\nif (gamepad == null) return;\n\neventsFunctionContext.returnValue = (gamepad && gamepad.id) ? gamepad.id : \"No information for player \" + (playerId + 1)\n", + "inlineCode": [ + "/** @type {Gamepad[]} */", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);", + "", + "//Get function parameter", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;", + "", + "if (playerId < 0 || playerId > 4) {", + " console.error('Parameter gamepad identifier in string expression: \"Gamepad type\", is not valid number, must be between 0 and 4');", + " return;", + "}", + "", + "const gamepad = gamepads[playerId];", + "", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.", + "if (gamepad == null) return;", + "", + "eventsFunctionContext.returnValue = (gamepad && gamepad.id) ? gamepad.id : \"No information for player \" + (playerId + 1)", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29331,7 +34630,35 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameters\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\nconst controllerType = eventsFunctionContext.getArgument(\"controller_type\").toUpperCase();\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in condition: \"Gamepad type\", is not valid number, must be between 0 and 4.');\n return;\n}\nif (controllerType === \"\") {\n console.error('Parameter type in condition: \"Gamepad type\", is not a string.');\n return;\n}\n\nconst gamepad = gamepads[playerId];\n\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\nif (gamepad == null) return;\n\n\nif (controllerType == \"XBOX\") {\n eventsFunctionContext.returnValue = gdjs._extensionController.isXbox(gamepad);\n} else {\n eventsFunctionContext.returnValue = gamepad ? gamepad.id.toUpperCase().indexOf(controllerType) !== -1 : false;\n}", + "inlineCode": [ + "/** @type {Gamepad[]} */", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);", + "", + "//Get function parameters", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;", + "const controllerType = eventsFunctionContext.getArgument(\"controller_type\").toUpperCase();", + "", + "if (playerId < 0 || playerId > 4) {", + " console.error('Parameter gamepad identifier in condition: \"Gamepad type\", is not valid number, must be between 0 and 4.');", + " return;", + "}", + "if (controllerType === \"\") {", + " console.error('Parameter type in condition: \"Gamepad type\", is not a string.');", + " return;", + "}", + "", + "const gamepad = gamepads[playerId];", + "", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.", + "if (gamepad == null) return;", + "", + "", + "if (controllerType == \"XBOX\") {", + " eventsFunctionContext.returnValue = gdjs._extensionController.isXbox(gamepad);", + "} else {", + " eventsFunctionContext.returnValue = gamepad ? gamepad.id.toUpperCase().indexOf(controllerType) !== -1 : false;", + "}" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29360,7 +34687,22 @@ "events": [ { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameter\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in condition: \"Gamepad connected\", is not valid number, must be between 0 and 4.');\n return;\n}\n\n// If gamepad was disconnected it will be null (so this will return false)\n// If gamepad was never connected it will be undefined (so this will return false)\neventsFunctionContext.returnValue = !!gamepads[playerId];", + "inlineCode": [ + "/** @type {Gamepad[]} */", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);", + "", + "//Get function parameter", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;", + "", + "if (playerId < 0 || playerId > 4) {", + " console.error('Parameter gamepad identifier in condition: \"Gamepad connected\", is not valid number, must be between 0 and 4.');", + " return;", + "}", + "", + "// If gamepad was disconnected it will be null (so this will return false)", + "// If gamepad was never connected it will be undefined (so this will return false)", + "eventsFunctionContext.returnValue = !!gamepads[playerId];" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29389,7 +34731,34 @@ }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\n//Vibration work only on game in browser.\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameters\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\nconst duration = eventsFunctionContext.getArgument(\"duration\") || 1;\n\nif (playerId < 0 || playerId > 4) {\n console.error('Parameter gamepad identifier in action: \"Gamepad connected\", is not valid number, must be between 0 and 4.');\n return;\n}\n\nconst gamepad = gamepads[playerId];\n\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\nif (gamepad == null) return;\n\nif (gamepad && gamepad.vibrationActuator) {\n gamepad.vibrationActuator.playEffect(\"dual-rumble\", {\n startDelay: 0,\n duration: duration * 1000,\n weakMagnitude: 1.0,\n strongMagnitude: 1.0\n });\n}", + "inlineCode": [ + "/** @type {Gamepad[]} */", + "//Vibration work only on game in browser.", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);", + "", + "//Get function parameters", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;", + "const duration = eventsFunctionContext.getArgument(\"duration\") || 1;", + "", + "if (playerId < 0 || playerId > 4) {", + " console.error('Parameter gamepad identifier in action: \"Gamepad connected\", is not valid number, must be between 0 and 4.');", + " return;", + "}", + "", + "const gamepad = gamepads[playerId];", + "", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.", + "if (gamepad == null) return;", + "", + "if (gamepad && gamepad.vibrationActuator) {", + " gamepad.vibrationActuator.playEffect(\"dual-rumble\", {", + " startDelay: 0,", + " duration: duration * 1000,", + " weakMagnitude: 1.0,", + " strongMagnitude: 1.0", + " });", + "}" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29422,7 +34791,122 @@ }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "//Define an new private object javascript for the gamepad extension\r\ngdjs._extensionController = {\r\n players: {\r\n 0: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n 1: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n 2: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n 3: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r\n },\r\n controllerButtonNames: { //Map associating controller button ids to button names\r\n \"XBOX\": {\r\n 0: \"A\",\r\n 1: \"B\",\r\n 2: \"X\",\r\n 3: \"Y\",\r\n 4: \"LB\",\r\n 5: \"RB\",\r\n 6: \"LT\",\r\n 7: \"RT\",\r\n 8: \"BACK\",\r\n 9: \"START\",\r\n 10: \"CLICK_STICK_LEFT\",\r\n 11: \"CLICK_STICK_RIGHT\",\r\n 12: \"UP\",\r\n 13: \"DOWN\",\r\n 14: \"LEFT\",\r\n 15: \"RIGHT\",\r\n 16: \"NONE\",\r\n 17: \"NONE\"\r\n },\r\n \"PS4\": {\r\n 0: \"CROSS\",\r\n 1: \"CIRCLE\",\r\n 2: \"SQUARE\",\r\n 3: \"TRIANGLE\",\r\n 4: \"L1\",\r\n 5: \"R1\",\r\n 6: \"L2\",\r\n 7: \"R2\",\r\n 8: \"SHARE\",\r\n 9: \"OPTIONS\",\r\n 10: \"CLICK_STICK_LEFT\",\r\n 11: \"CLICK_STICK_RIGHT\",\r\n 12: \"UP\",\r\n 13: \"DOWN\",\r\n 14: \"LEFT\",\r\n 15: \"RIGHT\",\r\n 16: \"PS_BUTTON\",\r\n 17: \"CLICK_TOUCHPAD\"\r\n }\r\n }\r\n};\r\n\r\ngdjs._extensionController.getInputString = function (type, buttonId) {\r\n const controllerButtonNames = gdjs._extensionController.controllerButtonNames;\r\n if (controllerButtonNames[type] !== undefined) {\r\n return controllerButtonNames[type][buttonId];\r\n }\r\n\r\n return \"UNKNOWN_BUTTON\";\r\n}\r\n\r\ngdjs._extensionController.axisToAngle = function (deltaX, deltaY) {\r\n const rad = Math.atan2(deltaY, deltaX);\r\n const deg = rad * (180 / Math.PI);\r\n return deg;\r\n}\r\n\r\ngdjs._extensionController.isXbox = function (gamepad) {\r\n return (gamepad ? (\r\n gamepad.id.toUpperCase().indexOf(\"XBOX\") !== -1\r\n // \"XINPUT\" cannot be used to check if it is a xbox controller is just a generic\r\n // name reported in Firefox corresponding to the driver being used by the controller\r\n // https://gamefaqs.gamespot.com/boards/916373-pc/73341312?page=1\r\n ) : false);\r\n}\r\n\r\n//Returns the new value taking into account the dead zone for the player_ID given\r\ngdjs._extensionController.getNormalizedAxisValue = function (v, player_ID) {\r\n // gdjs._extensionController = gdjs._extensionController || { deadzone: 0.2 };\r\n\r\n // Anything smaller than this is assumed to be 0,0\r\n const DEADZONE = gdjs._extensionController.players[player_ID].deadzone;\r\n\r\n if (Math.abs(v) < DEADZONE) {\r\n // In the dead zone, set to 0\r\n v = 0;\r\n\r\n if (v == null) {\r\n return 0;\r\n } else {\r\n return v;\r\n }\r\n\r\n } else {\r\n // We're outside the dead zone, but we'd like to smooth\r\n // this value out so it still runs nicely between 0..1.\r\n // That is, we don't want it to jump suddenly from 0 to\r\n // DEADZONE.\r\n\r\n // Remap v from\r\n // DEADZONE..1 to 0..(1-DEADZONE)\r\n // or from\r\n // -1..-DEADZONE to -(1-DEADZONE)..0\r\n\r\n v = v - Math.sign(v) * DEADZONE;\r\n\r\n // Remap v from\r\n // 0..(1-DEADZONE) to 0..1\r\n // or from\r\n // -(1-DEADZONE)..0 to -1..0\r\n\r\n return v / (1 - DEADZONE);\r\n }\r\n};", + "inlineCode": [ + "//Define an new private object javascript for the gamepad extension\r", + "gdjs._extensionController = {\r", + " players: {\r", + " 0: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r", + " 1: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r", + " 2: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r", + " 3: { mapping: 'DEFAULT', lastButtonUsed: -1, deadzone: 0.2, previousFrameStateButtons: {}, },\r", + " },\r", + " controllerButtonNames: { //Map associating controller button ids to button names\r", + " \"XBOX\": {\r", + " 0: \"A\",\r", + " 1: \"B\",\r", + " 2: \"X\",\r", + " 3: \"Y\",\r", + " 4: \"LB\",\r", + " 5: \"RB\",\r", + " 6: \"LT\",\r", + " 7: \"RT\",\r", + " 8: \"BACK\",\r", + " 9: \"START\",\r", + " 10: \"CLICK_STICK_LEFT\",\r", + " 11: \"CLICK_STICK_RIGHT\",\r", + " 12: \"UP\",\r", + " 13: \"DOWN\",\r", + " 14: \"LEFT\",\r", + " 15: \"RIGHT\",\r", + " 16: \"NONE\",\r", + " 17: \"NONE\"\r", + " },\r", + " \"PS4\": {\r", + " 0: \"CROSS\",\r", + " 1: \"CIRCLE\",\r", + " 2: \"SQUARE\",\r", + " 3: \"TRIANGLE\",\r", + " 4: \"L1\",\r", + " 5: \"R1\",\r", + " 6: \"L2\",\r", + " 7: \"R2\",\r", + " 8: \"SHARE\",\r", + " 9: \"OPTIONS\",\r", + " 10: \"CLICK_STICK_LEFT\",\r", + " 11: \"CLICK_STICK_RIGHT\",\r", + " 12: \"UP\",\r", + " 13: \"DOWN\",\r", + " 14: \"LEFT\",\r", + " 15: \"RIGHT\",\r", + " 16: \"PS_BUTTON\",\r", + " 17: \"CLICK_TOUCHPAD\"\r", + " }\r", + " }\r", + "};\r", + "\r", + "gdjs._extensionController.getInputString = function (type, buttonId) {\r", + " const controllerButtonNames = gdjs._extensionController.controllerButtonNames;\r", + " if (controllerButtonNames[type] !== undefined) {\r", + " return controllerButtonNames[type][buttonId];\r", + " }\r", + "\r", + " return \"UNKNOWN_BUTTON\";\r", + "}\r", + "\r", + "gdjs._extensionController.axisToAngle = function (deltaX, deltaY) {\r", + " const rad = Math.atan2(deltaY, deltaX);\r", + " const deg = rad * (180 / Math.PI);\r", + " return deg;\r", + "}\r", + "\r", + "gdjs._extensionController.isXbox = function (gamepad) {\r", + " return (gamepad ? (\r", + " gamepad.id.toUpperCase().indexOf(\"XBOX\") !== -1\r", + " // \"XINPUT\" cannot be used to check if it is a xbox controller is just a generic\r", + " // name reported in Firefox corresponding to the driver being used by the controller\r", + " // https://gamefaqs.gamespot.com/boards/916373-pc/73341312?page=1\r", + " ) : false);\r", + "}\r", + "\r", + "//Returns the new value taking into account the dead zone for the player_ID given\r", + "gdjs._extensionController.getNormalizedAxisValue = function (v, player_ID) {\r", + " // gdjs._extensionController = gdjs._extensionController || { deadzone: 0.2 };\r", + "\r", + " // Anything smaller than this is assumed to be 0,0\r", + " const DEADZONE = gdjs._extensionController.players[player_ID].deadzone;\r", + "\r", + " if (Math.abs(v) < DEADZONE) {\r", + " // In the dead zone, set to 0\r", + " v = 0;\r", + "\r", + " if (v == null) {\r", + " return 0;\r", + " } else {\r", + " return v;\r", + " }\r", + "\r", + " } else {\r", + " // We're outside the dead zone, but we'd like to smooth\r", + " // this value out so it still runs nicely between 0..1.\r", + " // That is, we don't want it to jump suddenly from 0 to\r", + " // DEADZONE.\r", + "\r", + " // Remap v from\r", + " // DEADZONE..1 to 0..(1-DEADZONE)\r", + " // or from\r", + " // -1..-DEADZONE to -(1-DEADZONE)..0\r", + "\r", + " v = v - Math.sign(v) * DEADZONE;\r", + "\r", + " // Remap v from\r", + " // 0..(1-DEADZONE) to 0..1\r", + " // or from\r", + " // -(1-DEADZONE)..0 to -1..0\r", + "\r", + " return v / (1 - DEADZONE);\r", + " }\r", + "};" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29444,7 +34928,36 @@ }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "//Each time a player press a button i save the last button pressed for the next frame\n/** @type {Gamepad[]} */\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\n\n//Get function parameter\nlet countPlayers = Object.keys(gdjs._extensionController.players).length;\n\n//Repeat for each players\nfor (let i = 0; i < countPlayers; i++) {\n let gamepad = gamepads[i]; // Get the gamepad of the player\n\n //we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\n if (gamepad == null) {\n return;\n }\n\n for (let b = 0; b < Object.keys(gamepad.buttons).length; b++) { //For each buttons\n if (gamepad.buttons[b].pressed) { //One of them is pressed\n gdjs._extensionController.players[i].lastButtonUsed = b; //Save the button pressed\n\n //Save the state of the button for the next frame.\n gdjs._extensionController.players[i].previousFrameStateButtons[b] = { pressed: true };\n }\n }\n}\n\n\n", + "inlineCode": [ + "//Each time a player press a button i save the last button pressed for the next frame", + "/** @type {Gamepad[]} */", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);", + "", + "//Get function parameter", + "let countPlayers = Object.keys(gdjs._extensionController.players).length;", + "", + "//Repeat for each players", + "for (let i = 0; i < countPlayers; i++) {", + " let gamepad = gamepads[i]; // Get the gamepad of the player", + "", + " //we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.", + " if (gamepad == null) {", + " return;", + " }", + "", + " for (let b = 0; b < Object.keys(gamepad.buttons).length; b++) { //For each buttons", + " if (gamepad.buttons[b].pressed) { //One of them is pressed", + " gdjs._extensionController.players[i].lastButtonUsed = b; //Save the button pressed", + "", + " //Save the state of the button for the next frame.", + " gdjs._extensionController.players[i].previousFrameStateButtons[b] = { pressed: true };", + " }", + " }", + "}", + "", + "", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true @@ -29467,7 +34980,54 @@ }, { "type": "BuiltinCommonInstructions::JsCode", - "inlineCode": "/** @type {Gamepad[]} */\r\nconst gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r\n\r\n//Get function parameters\r\nconst playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r\n\r\nif (playerId < 0 || playerId > 4) {\r\n\tconsole.error('Parameter gamepad identifier in condition: \"Any gamepad button released\", is not valid number, must be between 0 and 4.');\r\n\treturn;\r\n}\r\n\r\nconst gamepad = gamepads[playerId];\r\n\r\n//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r\nif (gamepad == null) return;\r\n\r\nfor (let buttonId = 0; buttonId < gamepad.buttons.length; buttonId++) { //For each buttons on current frame.\r\n\r\n\tif (buttonId === undefined) {\r\n\t\teventsFunctionContext.returnValue = false;\r\n\t\treturn;\r\n\t}\r\n\r\n\t//Get previous value or define value by default for the current button\r\n\tgdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] || { pressed: false };\r\n\r\n\t//Get state of the button at previous frame\r\n\tconst previousStateButtonIsPressed = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed;\r\n\r\n\t//Get the state of the button on the current frame.\r\n\tconst currentFrameStateButtonIsPressed = gamepad.buttons[buttonId].pressed;\r\n\r\n\t//When previousStateButtonIsPressed is true and actual button state is not pressed\r\n\t//Player have release the button\r\n\tif (previousStateButtonIsPressed === true && currentFrameStateButtonIsPressed === false) {\r\n\t\tgdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = false;\r\n\t\teventsFunctionContext.returnValue = true;\r\n\t\t//break;\r\n\t\treturn;\r\n\t} else {\r\n\t\teventsFunctionContext.returnValue = false;\r\n\t}\r\n\r\n\tif (currentFrameStateButtonIsPressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r\n}\r\n", + "inlineCode": [ + "/** @type {Gamepad[]} */\r", + "const gamepads = navigator.getGamepads ? navigator.getGamepads() : (navigator.webkitGetGamepads ? navigator.webkitGetGamepads() : []);\r", + "\r", + "//Get function parameters\r", + "const playerId = eventsFunctionContext.getArgument(\"player_ID\") - 1;\r", + "\r", + "if (playerId < 0 || playerId > 4) {\r", + "\tconsole.error('Parameter gamepad identifier in condition: \"Any gamepad button released\", is not valid number, must be between 0 and 4.');\r", + "\treturn;\r", + "}\r", + "\r", + "const gamepad = gamepads[playerId];\r", + "\r", + "//we need keep this condition because when use have not yet plug her controller we can't get the controller in the gamepad variable.\r", + "if (gamepad == null) return;\r", + "\r", + "for (let buttonId = 0; buttonId < gamepad.buttons.length; buttonId++) { //For each buttons on current frame.\r", + "\r", + "\tif (buttonId === undefined) {\r", + "\t\teventsFunctionContext.returnValue = false;\r", + "\t\treturn;\r", + "\t}\r", + "\r", + "\t//Get previous value or define value by default for the current button\r", + "\tgdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId] || { pressed: false };\r", + "\r", + "\t//Get state of the button at previous frame\r", + "\tconst previousStateButtonIsPressed = gdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed;\r", + "\r", + "\t//Get the state of the button on the current frame.\r", + "\tconst currentFrameStateButtonIsPressed = gamepad.buttons[buttonId].pressed;\r", + "\r", + "\t//When previousStateButtonIsPressed is true and actual button state is not pressed\r", + "\t//Player have release the button\r", + "\tif (previousStateButtonIsPressed === true && currentFrameStateButtonIsPressed === false) {\r", + "\t\tgdjs._extensionController.players[playerId].previousFrameStateButtons[buttonId].pressed = false;\r", + "\t\teventsFunctionContext.returnValue = true;\r", + "\t\t//break;\r", + "\t\treturn;\r", + "\t} else {\r", + "\t\teventsFunctionContext.returnValue = false;\r", + "\t}\r", + "\r", + "\tif (currentFrameStateButtonIsPressed) gdjs._extensionController.players[playerId].lastButtonUsed = buttonId;\r", + "}\r", + "" + ], "parameterObjects": "", "useStrict": true, "eventsSheetExpanded": true From 4b3cd65f013d7a928f6d6355cfd014add90ff3f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Fri, 2 Dec 2022 19:41:34 +0100 Subject: [PATCH 05/11] Define a default value for shared properties. --- examples/isometric-game/isometric-game.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index d279a4940..89a8550a8 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -26645,7 +26645,6 @@ { "type": "BuiltinCommonInstructions::JsCode", "inlineCode": [ - "", "// This code has been built from https://github.com/D8H/NavMesh-GDevelop-Extension", "// If you need to make any modification, please open a PR on github.", "", @@ -33278,7 +33277,7 @@ "name": "CellSize" }, { - "value": "", + "value": "0", "type": "Number", "label": "Area left bound", "description": "The left bound of the area where objects can go in the scene.", @@ -33288,7 +33287,7 @@ "name": "AreaLeftBound" }, { - "value": "", + "value": "0", "type": "Number", "label": "Area top bound", "description": "The top bound of the area where objects can go in the scene.", @@ -33298,7 +33297,7 @@ "name": "AreaTopBound" }, { - "value": "", + "value": "0", "type": "Number", "label": "Area right bound", "description": "The right bound of the area where objects can go in the scene (default to the game resolution).", @@ -33308,7 +33307,7 @@ "name": "AreaRightBound" }, { - "value": "", + "value": "0", "type": "Number", "label": "Area bottom bound", "description": "The bottom bound of the area where objects can go in the scene (default to the game resolution).", From 1d9b3558c790e660909219de606706613269f439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 4 Dec 2022 23:48:57 +0100 Subject: [PATCH 06/11] Use extensions for the camera. --- examples/isometric-game/isometric-game.json | 5647 ++++++++++++++++++- 1 file changed, 5562 insertions(+), 85 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index 89a8550a8..20b62194a 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -8273,6 +8273,58 @@ "CollisionShape": "Dot at center", "ExtraBorder": 20 }, + { + "name": "SmoothCamera", + "type": "SmoothCamera::SmoothCamera", + "LeftwardSpeed": 0.95, + "RightwardSpeed": 0.95, + "UpwardSpeed": 0.95, + "DownwardSpeed": 0.95, + "FollowOnX": true, + "FollowOnY": true, + "FollowFreeAreaLeft": 0, + "FollowFreeAreaRight": 0, + "FollowFreeAreaTop": 0, + "FollowFreeAreaBottom": 0, + "CameraOffsetX": 0, + "CameraOffsetY": 0, + "CameraDelay": 0, + "ForecastTime": 0, + "ForecastHistoryDuration": 0, + "LogLeftwardSpeed": 0, + "LogRightwardSpeed": 0, + "LogDownwardSpeed": 0, + "LogUpwardSpeed": 0, + "DelayedCenterX": 0, + "DelayedCenterY": 0, + "ForecastHistoryMeanX": 0, + "ForecastHistoryMeanY": 0, + "ForecastHistoryVarianceX": 0, + "ForecastHistoryCovariance": 0, + "ForecastHistoryLinearA": 0, + "ForecastHistoryLinearB": 0, + "ForecastedX": 0, + "ForecastedY": 0, + "ProjectedNewestX": 0, + "ProjectedNewestY": 0, + "ProjectedOldestX": 0, + "ProjectedOldestY": 0, + "ForecastHistoryVarianceY": 0, + "Index": 0, + "CameraDelayCatchUpSpeed": 0, + "CameraExtraDelay": 0, + "WaitingSpeedXMax": 0, + "WaitingSpeedYMax": 0, + "WaitingEnd": 0, + "CameraDelayCatchUpDuration": 0, + "LeftwardSpeedMax": 9000, + "RightwardSpeedMax": 9000, + "UpwardSpeedMax": 9000, + "DownwardSpeedMax": 9000, + "OldX": 9000, + "OldY": 9000, + "IsCalledManually": false + }, { "name": "TopDownMovement", "type": "TopDownMovementBehavior::TopDownMovementBehavior", @@ -23341,7 +23393,7 @@ "directions": [ { "looping": true, - "timeBetweenFrames": 0.03, + "timeBetweenFrames": 0.03999999910593033, "sprites": [ { "hasCustomCollisionMask": false, @@ -25747,6 +25799,18 @@ } ], "actions": [ + { + "type": { + "value": "CentreCamera" + }, + "parameters": [ + "", + "Mindy", + "", + "\"\"", + "0" + ] + }, { "type": { "value": "HideLayer" @@ -25867,50 +25931,31 @@ }, { "type": { - "inverted": true, - "value": "BehaviorActivated" - }, - "parameters": [ - "Mindy", - "NavMeshPathfindingBehavior" - ] - } - ], - "actions": [ - { - "type": { - "value": "ChangeAnimation" - }, - "parameters": [ - "Mindy", - "=", - "0" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "inverted": true, - "value": "TopDownMovementBehavior::IsMoving" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - }, - { - "type": { - "inverted": true, - "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" + "value": "BuiltinCommonInstructions::Or" }, - "parameters": [ - "Mindy", - "NavMeshPathfindingBehavior", - "" + "parameters": [], + "subInstructions": [ + { + "type": { + "inverted": true, + "value": "BehaviorActivated" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior" + ] + }, + { + "type": { + "inverted": true, + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "" + ] + } ] } ], @@ -26174,44 +26219,80 @@ "source": "", "type": "BuiltinCommonInstructions::Group", "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Handle collision (no conditions is necessary, the actions takes care of handling everything)", + "comment2": "" + }, { "type": "BuiltinCommonInstructions::Standard", "conditions": [], "actions": [ { "type": { - "value": "CentreCamera" + "value": "SeparateFromObjects" }, "parameters": [ - "", "Mindy", - "yes", - "\"\"", - "0" + "Obstacle" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::MoveCameraCloser" + }, + "parameters": [ + "Mindy", + "SmoothCamera", + "" ] }, { "type": { - "value": "CentreCamera" + "value": "CopyCameraSettings::CopyCameraSettings" }, "parameters": [ "", - "Mindy", - "yes", + "\"\"", + "0", "\"Level1\"", - "0" + "0", + "", + "", + "no", + "no", + "" ] }, { "type": { - "value": "CentreCamera" + "value": "CopyCameraSettings::CopyCameraSettings" }, "parameters": [ "", - "Mindy", - "yes", + "\"\"", + "0", "\"Level2\"", - "0" + "0", + "", + "", + "no", + "no", + "" ] } ] @@ -26219,34 +26300,6 @@ ], "parameters": [] }, - { - "type": "BuiltinCommonInstructions::Comment", - "color": { - "b": 109, - "g": 230, - "r": 255, - "textB": 0, - "textG": 0, - "textR": 0 - }, - "comment": "Handle collision (no conditions is necessary, the actions takes care of handling everything)", - "comment2": "" - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "SeparateFromObjects" - }, - "parameters": [ - "Mindy", - "Obstacle" - ] - } - ] - }, { "colorB": 228, "colorG": 176, @@ -26563,6 +26616,10 @@ "CellSize": 10, "AreaLeftBound": -300 }, + { + "name": "SmoothCamera", + "type": "SmoothCamera::SmoothCamera" + }, { "name": "TopDownMovement", "type": "TopDownMovementBehavior::TopDownMovementBehavior" @@ -26583,6 +26640,5426 @@ } ], "eventsFunctionsExtensions": [ + { + "author": "", + "category": "Camera", + "extensionNamespace": "", + "fullName": "Copy camera settings", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0ibWRpLWxheWVycy10cmlwbGUtb3V0bGluZSIgd2lkdGg9IjI0IiBoZWlnaHQ9IjI0IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxwYXRoIGQ9Ik0xMiAxNi41NEwxOS4zNyAxMC44TDIxIDEyLjA3TDEyIDE5LjA3TDMgMTIuMDdMNC42MiAxMC44MUwxMiAxNi41NE0xMiAxNEwzIDdMMTIgMEwyMSA3TDEyIDE0TTEyIDIuNTNMNi4yNiA3TDEyIDExLjQ3TDE3Ljc0IDdMMTIgMi41M00xMiAyMS40N0wxOS4zNyAxNS43M0wyMSAxN0wxMiAyNEwzIDE3TDQuNjIgMTUuNzRMMTIgMjEuNDciIC8+PC9zdmc+", + "name": "CopyCameraSettings", + "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/layers-triple-outline.svg", + "shortDescription": "Copy the camera settings of a layer and apply them to another layer.", + "version": "1.0.0", + "description": [ + "Useful when multiple layers need to use the same camera values.", + "", + "How to use:", + "- Run the \"Copy camera settings\" action after all other camera actions have been performed", + "", + "Tips:", + "- Do not use on layers that implement a parallax effect" + ], + "origin": { + "identifier": "CopyCameraSettings", + "name": "gdevelop-extension-store" + }, + "tags": [ + "camera", + "clone", + "zoom", + "position", + "layer", + "angle", + "copy" + ], + "authorIds": [ + "gqDaZjCfevOOxBYkK6zlhtZnXCg1" + ], + "dependencies": [], + "eventsFunctions": [ + { + "description": "Copy camera settings of a layer and apply them to another layer.", + "fullName": "Copy camera settings", + "functionType": "Action", + "name": "CopyCameraSettings", + "sentence": "Copy camera settings of _PARAM1_ layer and apply them to _PARAM3_ layer (X position: _PARAM5_, Y position: _PARAM6_, Zoom: _PARAM7_, Angle: _PARAM8_)", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"CloneX\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraX" + }, + "parameters": [ + "", + "=", + "CameraX(GetArgumentAsString(\"SourceLayer\"),GetArgumentAsNumber(\"SourceCamera\"))", + "GetArgumentAsString(\"DestinationLayer\")", + "GetArgumentAsNumber(\"DestinationCamera\")" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"CloneY\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraY" + }, + "parameters": [ + "", + "=", + "CameraY(GetArgumentAsString(\"SourceLayer\"),GetArgumentAsNumber(\"SourceCamera\"))", + "GetArgumentAsString(\"DestinationLayer\")", + "GetArgumentAsNumber(\"DestinationCamera\")" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"CloneZoom\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "ZoomCamera" + }, + "parameters": [ + "", + "CameraZoom(GetArgumentAsString(\"SourceLayer\"),GetArgumentAsNumber(\"SourceCamera\"))", + "GetArgumentAsString(\"DestinationLayer\")", + "GetArgumentAsNumber(\"DestinationCamera\")" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"CloneAngle\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraAngle" + }, + "parameters": [ + "", + "=", + "CameraAngle(GetArgumentAsString(\"SourceLayer\"),GetArgumentAsNumber(\"SourceCamera\"))", + "GetArgumentAsString(\"DestinationLayer\")", + "GetArgumentAsNumber(\"DestinationCamera\")" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Source layer", + "name": "SourceLayer", + "type": "layer" + }, + { + "description": "Source camera", + "name": "SourceCamera", + "type": "expression" + }, + { + "description": "Destination layer", + "name": "DestinationLayer", + "type": "layer" + }, + { + "description": "Destination camera", + "name": "DestinationCamera", + "type": "expression" + }, + { + "defaultValue": "yes", + "description": "Clone X position", + "name": "CloneX", + "optional": true, + "type": "yesorno" + }, + { + "defaultValue": "yes", + "description": "Clone Y position", + "name": "CloneY", + "optional": true, + "type": "yesorno" + }, + { + "defaultValue": "yes", + "description": "Clone zoom", + "name": "CloneZoom", + "optional": true, + "type": "yesorno" + }, + { + "defaultValue": "yes", + "description": "Clone angle", + "name": "CloneAngle", + "optional": true, + "type": "yesorno" + } + ], + "objectGroups": [] + } + ], + "eventsBasedBehaviors": [], + "eventsBasedObjects": [] + }, + { + "author": "", + "category": "Camera", + "extensionNamespace": "", + "fullName": "Smooth Camera", + "helpPath": "", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQoJLnN0MXtmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjEwO30NCjwvc3R5bGU+DQo8cGF0aCBjbGFzcz0ic3QwIiBkPSJNMjQsMTNoLTZjLTEuMSwwLTItMC45LTItMlY1YzAtMS4xLDAuOS0yLDItMmg2YzEuMSwwLDIsMC45LDIsMnY2QzI2LDEyLjEsMjUuMSwxMywyNCwxM3oiLz4NCjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yNiw4djEwYzAsMS4xLTAuOSwyLTIsMkg4Yy0xLjEsMC0yLTAuOS0yLTJWOGMwLTEuMSwwLjktMiwyLTJoOCIvPg0KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMjEiIGN5PSI4IiByPSIyIi8+DQo8Y2lyY2xlIGNsYXNzPSJzdDAiIGN4PSIxMSIgY3k9IjE2IiByPSIxIi8+DQo8cmVjdCB4PSI5IiB5PSI5IiBjbGFzcz0ic3QwIiB3aWR0aD0iNCIgaGVpZ2h0PSIzIi8+DQo8cG9seWxpbmUgY2xhc3M9InN0MCIgcG9pbnRzPSIyMSwyOSAyMSwyOSAxMSwyOSAxMSwyOSAiLz4NCjxwb2x5bGluZSBjbGFzcz0ic3QwIiBwb2ludHM9IjE4LDIwIDE4LDI5IDE0LDI5IDE0LDIwICIvPg0KPHJlY3QgeD0iNyIgeT0iMyIgY2xhc3M9InN0MCIgd2lkdGg9IjQiIGhlaWdodD0iMyIvPg0KPC9zdmc+DQo=", + "name": "SmoothCamera", + "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/Line Hero Pack/Master/SVG/Computers and Hardware/Computers and Hardware_camcoder_gopro_go_pro_camera.svg", + "shortDescription": "Smoothly scroll to follow an object.", + "version": "0.2.2", + "description": [ + "The camera follows an object according to:", + "- a frame rate independent catch-up speed to make the scrolling from smooth to strong", + "- a maximum speed to do linear following ([open the project online](https://editor.gdevelop.io/?project=example://platformer-with-tilemap)) or slow down the camera when teleporting the object", + "- a follow-free zone to avoid scrolling on small movements", + "- an offset to see further in one direction", + "- an extra delay and catch-up speed to give an impression of speed (useful for dash)", + "- position forecasting and delay to simulate a cameraman response time", + "", + "A platformer dedicated behavior allows to switch of settings when the character is in air or on the floor. This can be used to stabilize the camera when jumping." + ], + "origin": { + "identifier": "SmoothCamera", + "name": "gdevelop-extension-store" + }, + "tags": [ + "camera", + "scrolling", + "follow", + "smooth" + ], + "authorIds": [ + "IWykYNRvhCZBN3vEgKEbBPOR3Oc2" + ], + "dependencies": [], + "eventsFunctions": [], + "eventsBasedBehaviors": [ + { + "description": "Smoothly scroll to follow an object.", + "fullName": "Smooth Camera", + "name": "SmoothCamera", + "objectType": "", + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "onCreated", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Update private properties through setters to check their values and initialize state.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetLeftwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyLeftwardSpeed()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetRightwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyRightwardSpeed()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetUpwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyUpwardSpeed()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetDownwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyDownwardSpeed()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetLeftwardSpeedMax" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyLeftwardSpeedMax()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetRightwardSpeedMax" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyRightwardSpeedMax()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetUpwardSpeedMax" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyUpwardSpeedMax()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetDownwardSpeedMax" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyDownwardSpeedMax()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetFollowFreeAreaLeft" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyFollowFreeAreaLeft()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetFollowFreeAreaRight" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyFollowFreeAreaRight()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetFollowFreeAreaTop" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyFollowFreeAreaTop()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetFollowFreeAreaBottom" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Behavior::PropertyFollowFreeAreaBottom()", + "log(1 - )" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyCameraDelay" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyCameraDelay()" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "fullName": "", + "functionType": "Action", + "name": "doStepPreEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "SmoothCamera::SmoothCamera::PropertyIsCalledManually" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::DoMoveCameraCloser" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Move the camera closer to the object. This action must be called after the object has moved for the frame.", + "fullName": "Move the camera closer", + "functionType": "Action", + "name": "MoveCameraCloser", + "sentence": "Move the camera closer to _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "The camera following is called with an action, the call from doStepPreEvents must be disabled to avoid to do it twice.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIsCalledManually" + }, + "parameters": [ + "Object", + "Behavior", + "yes" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::DoMoveCameraCloser" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Move the camera closer to the object.", + "fullName": "Do move the camera closer", + "functionType": "Action", + "name": "DoMoveCameraCloser", + "private": true, + "sentence": "Do move the camera closer _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Delaying and forecasting can be used at the same time.\nForecasting only use the positions that are older than the one used for delaying.\nThe behavior uses a position history that is split in 2 arrays:\n- one for delaying the position (from TimeFromStart to TimeFromStart - CamearDelay)\n- one for forecasting the position (from TimeFromStart - CamearDelay to TimeFromStart - CamearDelay - ForecastHistoryDuration", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::UpdateDelayedPosition" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::UpdateForecastedPosition" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "At each frame, the camera must catchup the target by a given ratio (speed)\ncameraX(t) - targetX = (cameraX(t - 1) - targetX) * speed\n\nThe frame rate must not impact on the catch-up speed, we don't want a speed in ratio per frame but a speed ratio per second, like this:\ncameraX(t) - targetX = (cameraX(t - 1s) - targetX) * speed\n\nOk, but we still need to process each frame, we can use a exponent for this:\ncameraX(t) - targetX = (cameraX(t - timeDelta) - targetX) * speed^timeDelta\ncameraX(t) = targetX + (cameraX(t - timeDelta) - targetX) * exp(timeDelta * ln(speed))\n\npow is probably more efficient than precalculated log if the speed is changed continuously but this might be rare enough.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyFollowOnX" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyOldX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "CameraX(Object.Layer(), 0)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CameraX" + }, + "parameters": [ + "", + ">", + "Object.Behavior::FreeAreaRight()", + "Object.Layer()", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraX" + }, + "parameters": [ + "", + "=", + "Object.Behavior::FreeAreaRight()\n+ (CameraX(Object.Layer(), 0) - Object.Behavior::FreeAreaRight())\n* exp(TimeDelta() * Object.Behavior::PropertyLogLeftwardSpeed())", + "Object.Layer()", + "0" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CameraX" + }, + "parameters": [ + "", + "<", + "Object.Behavior::PropertyOldX() - Object.Behavior::PropertyLeftwardSpeedMax() * TimeDelta()", + "Object.Layer()", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraX" + }, + "parameters": [ + "", + "=", + "Object.Behavior::PropertyOldX() - Object.Behavior::PropertyLeftwardSpeedMax() * TimeDelta()", + "Object.Layer()", + "0" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CameraX" + }, + "parameters": [ + "", + "<", + "Object.Behavior::FreeAreaLeft()", + "Object.Layer()", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraX" + }, + "parameters": [ + "", + "=", + "Object.Behavior::FreeAreaLeft()\n+ (CameraX(Object.Layer(), 0) - Object.Behavior::FreeAreaLeft())\n* exp(TimeDelta() * Object.Behavior::PropertyLogRightwardSpeed())", + "Object.Layer()", + "0" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CameraX" + }, + "parameters": [ + "", + ">", + "Object.Behavior::PropertyOldX() + Object.Behavior::PropertyRightwardSpeedMax() * TimeDelta()", + "Object.Layer()", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraX" + }, + "parameters": [ + "", + "=", + "Object.Behavior::PropertyOldX() + Object.Behavior::PropertyRightwardSpeedMax() * TimeDelta()", + "Object.Layer()", + "0" + ] + } + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyFollowOnY" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyOldY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "CameraY(Object.Layer(), 0)" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CameraY" + }, + "parameters": [ + "", + ">", + "Object.Behavior::FreeAreaBottom()", + "Object.Layer()", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraY" + }, + "parameters": [ + "", + "=", + "Object.Behavior::FreeAreaBottom()\n+ (CameraY(Object.Layer(), 0) - Object.Behavior::FreeAreaBottom())\n* exp(TimeDelta() * Object.Behavior::PropertyLogUpwardSpeed())", + "Object.Layer()", + "0" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CameraY" + }, + "parameters": [ + "", + "<", + "Object.Behavior::PropertyOldY() - Object.Behavior::PropertyUpwardSpeedMax() * TimeDelta()", + "Object.Layer()", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraY" + }, + "parameters": [ + "", + "=", + "Object.Behavior::PropertyOldY() - Object.Behavior::PropertyUpwardSpeedMax() * TimeDelta()", + "Object.Layer()", + "0" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CameraY" + }, + "parameters": [ + "", + "<", + "Object.Behavior::FreeAreaTop()", + "Object.Layer()", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraY" + }, + "parameters": [ + "", + "=", + "Object.Behavior::FreeAreaTop()\n+ (CameraY(Object.Layer(), 0) - Object.Behavior::FreeAreaTop())\n* exp(TimeDelta() * Object.Behavior::PropertyLogDownwardSpeed())", + "Object.Layer()", + "0" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "CameraY" + }, + "parameters": [ + "", + ">", + "Object.Behavior::PropertyOldY() + Object.Behavior::PropertyDownwardSpeedMax() * TimeDelta()", + "Object.Layer()", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetCameraY" + }, + "parameters": [ + "", + "=", + "Object.Behavior::PropertyOldY() + Object.Behavior::PropertyDownwardSpeedMax() * TimeDelta()", + "Object.Layer()", + "0" + ] + } + ] + } + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Delay the camera according to a maximum speed and catch up the delay.", + "fullName": "Wait and catch up", + "functionType": "Action", + "name": "WaitAndCatchUp", + "sentence": "Delay the camera of _PARAM0_ during: _PARAM2_ seconds according to the maximum speed _PARAM3_;_PARAM4_ seconds and catch up in _PARAM5_ seconds", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Maybe the catch-up show be done in constant pixel speed instead of constant time speed.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyWaitingEnd" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "TimeFromStart() + GetArgumentAsNumber(\"WaitingDuration\")" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyWaitingSpeedXMax" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"WaitingSpeedXMax\")" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyWaitingSpeedYMax" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"WaitingSpeedYMax\")" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyCameraDelayCatchUpDuration" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"CatchUpDuration\")" + ] + } + ] + }, + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Wait and catch up\"", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Waiting duration (in seconds)", + "name": "WaitingDuration", + "type": "expression" + }, + { + "description": "Waiting maximum camera target speed X", + "name": "WaitingSpeedXMax", + "type": "expression" + }, + { + "description": "Waiting maximum camera target speed Y", + "name": "WaitingSpeedYMax", + "type": "expression" + }, + { + "description": "Catch up duration (in seconds)", + "name": "CatchUpDuration", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Draw the targeted and actual camera position.", + "fullName": "Draw debug", + "functionType": "Action", + "name": "DrawDebug", + "sentence": "Draw targeted and actual camera position for _PARAM0_ on _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PrimitiveDrawing::FillOpacity" + }, + "parameters": [ + "ShapePainter", + "=", + "0" + ] + } + ] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Path used by the forecasting", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "Object.VariableChildCount(__SmoothCamera.ForecastHistoryTime)", + ">", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + }, + { + "type": { + "value": "PrimitiveDrawing::OutlineColor" + }, + "parameters": [ + "ShapePainter", + "\"245;166;35\"" + ] + }, + { + "type": { + "value": "PrimitiveDrawing::BeginFillPath" + }, + "parameters": [ + "ShapePainter", + "Object.Variable(__SmoothCamera.ForecastHistoryX[0])", + "Object.Variable(__SmoothCamera.ForecastHistoryY[0])" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Repeat", + "repeatExpression": "Object.VariableChildCount(__SmoothCamera.ForecastHistoryX)", + "conditions": [], + "actions": [ + { + "type": { + "value": "PrimitiveDrawing::PathLineTo" + }, + "parameters": [ + "ShapePainter", + "Object.Variable(__SmoothCamera.ForecastHistoryX[Object.Behavior::PropertyIndex()])", + "Object.Variable(__SmoothCamera.ForecastHistoryY[Object.Behavior::PropertyIndex()])" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "1" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PrimitiveDrawing::EndFillPath" + }, + "parameters": [ + "ShapePainter" + ] + } + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Follow-free area.", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyFollowFreeAreaLeft" + }, + "parameters": [ + "Object", + "Behavior", + "!=", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyFollowFreeAreaRight" + }, + "parameters": [ + "Object", + "Behavior", + "!=", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyFollowFreeAreaTop" + }, + "parameters": [ + "Object", + "Behavior", + "!=", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyFollowFreeAreaBottom" + }, + "parameters": [ + "Object", + "Behavior", + "!=", + "0" + ] + } + ] + } + ], + "actions": [ + { + "type": { + "value": "PrimitiveDrawing::OutlineColor" + }, + "parameters": [ + "ShapePainter", + "\"126;211;33\"" + ] + }, + { + "type": { + "value": "PrimitiveDrawing::Rectangle" + }, + "parameters": [ + "ShapePainter", + "Object.Behavior::FreeAreaLeft() - 1", + "Object.Behavior::FreeAreaTop() - 1", + "Object.Behavior::FreeAreaRight() + 1", + "Object.Behavior::FreeAreaBottom() + 1" + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Linear regression vector used by the forcasting.", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PrimitiveDrawing::OutlineColor" + }, + "parameters": [ + "ShapePainter", + "\"208;2;27\"" + ] + }, + { + "type": { + "value": "PrimitiveDrawing::LineV2" + }, + "parameters": [ + "ShapePainter", + "Object.Behavior::PropertyProjectedOldestX()", + "Object.Behavior::PropertyProjectedOldestY()", + "Object.Behavior::PropertyProjectedNewestX()", + "Object.Behavior::PropertyProjectedNewestY()", + "1" + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Targeted and actual camera position", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "PrimitiveDrawing::Circle" + }, + "parameters": [ + "ShapePainter", + "Object.Behavior::PropertyForecastedX()", + "Object.Behavior::PropertyForecastedY()", + "3" + ] + }, + { + "type": { + "value": "PrimitiveDrawing::LineV2" + }, + "parameters": [ + "ShapePainter", + "CameraX(Object.Layer(), 0)", + "CameraY(Object.Layer(), 0) - 4", + "CameraX(Object.Layer(), 0)", + "CameraY(Object.Layer(), 0) + 4", + "1" + ] + }, + { + "type": { + "value": "PrimitiveDrawing::LineV2" + }, + "parameters": [ + "ShapePainter", + "CameraX(Object.Layer(), 0) - 4", + "CameraY(Object.Layer(), 0)", + "CameraX(Object.Layer(), 0) + 4", + "CameraY(Object.Layer(), 0)", + "1" + ] + } + ] + } + ], + "parameters": [] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Shape painter", + "name": "ShapePainter", + "supplementaryInformation": "PrimitiveDrawing::Drawer", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "Enable or disable the following on X axis.", + "fullName": "Follow on X", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetFollowOnX", + "sentence": "The camera follows _PARAM0_ on X axis: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyFollowOnX" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"FollowOnX\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyFollowOnX" + }, + "parameters": [ + "Object", + "Behavior", + "yes" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Follow on X axis", + "name": "FollowOnX", + "type": "yesorno" + } + ], + "objectGroups": [] + }, + { + "description": "Enable or disable the following on Y axis.", + "fullName": "Follow on Y", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetFollowOnY", + "sentence": "The camera follows _PARAM0_ on Y axis: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyFollowOnY" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "GetArgumentAsBoolean" + }, + "parameters": [ + "\"FollowOnY\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyFollowOnY" + }, + "parameters": [ + "Object", + "Behavior", + "yes" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Follow on Y axis", + "name": "FollowOnY", + "type": "yesorno" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera follow free area right border.", + "fullName": "Follow free area right border", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetFollowFreeAreaRight", + "sentence": "Change the camera follow free area right border of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyFollowFreeAreaTop" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "max(0, GetArgumentAsNumber(\"SetFollowFreeAreaRight\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Follow free area right border", + "name": "SetFollowFreeAreaRight", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera follow free area left border.", + "fullName": "Follow free area left border", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetFollowFreeAreaLeft", + "sentence": "Change the camera follow free area left border of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyFollowFreeAreaTop" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "max(0, GetArgumentAsNumber(\"SetFollowFreeAreaLeft\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Follow free area left border", + "name": "SetFollowFreeAreaLeft", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera follow free area top border.", + "fullName": "Follow free area top border", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetFollowFreeAreaTop", + "sentence": "Change the camera follow free area top border of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyFollowFreeAreaTop" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "max(0, GetArgumentAsNumber(\"FollowFreeAreaTop\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Follow free area top border", + "name": "FollowFreeAreaTop", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera follow free area bottom border.", + "fullName": "Follow free area bottom border", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetFollowFreeAreaBottom", + "sentence": "Change the camera follow free area bottom border of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyFollowFreeAreaBottom" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "max(0, GetArgumentAsNumber(\"SetFollowFreeAreaBottom\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Follow free area bottom border", + "name": "SetFollowFreeAreaBottom", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera leftward maximum speed (in pixels per second).", + "fullName": "Leftward maximum speed", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetLeftwardSpeedMax", + "sentence": "Change the camera leftward maximum speed of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyLeftwardSpeedMax" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "max(0, GetArgumentAsNumber(\"Speed\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Leftward maximum speed (in ratio per second)", + "name": "Speed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera rightward maximum speed (in pixels per second).", + "fullName": "Rightward maximum speed", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetRightwardSpeedMax", + "sentence": "Change the camera rightward maximum speed of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyLeftwardSpeedMax" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "max(0, GetArgumentAsNumber(\"Speed\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Rightward maximum speed (in pixels per second)", + "name": "Speed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera upward maximum speed (in pixels per second).", + "fullName": "Upward maximum speed", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetUpwardSpeedMax", + "sentence": "Change the camera upward maximum speed of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyUpwardSpeedMax" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "max(0, GetArgumentAsNumber(\"Speed\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Upward maximum speed (in pixels per second)", + "name": "Speed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera downward maximum speed (in pixels per second).", + "fullName": "Downward maximum speed", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetDownwardSpeedMax", + "sentence": "Change the camera downward maximum speed of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyDownwardSpeedMax" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "max(0, GetArgumentAsNumber(\"Speed\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Downward maximum speed (in pixels per second)", + "name": "Speed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera leftward catch-up speed (in ratio per second).", + "fullName": "Leftward catch-up speed", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetLeftwardSpeed", + "sentence": "Change the camera leftward catch-up speed of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyLeftwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "clamp(0, 1, GetArgumentAsNumber(\"LeftwardSpeed\"))" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyLogLeftwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "log(1 - Object.Behavior::PropertyLeftwardSpeed())" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Leftward catch-up speed (in ratio per second)", + "name": "LeftwardSpeed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera rightward catch-up speed (in ratio per second).", + "fullName": "Rightward catch-up speed", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetRightwardSpeed", + "sentence": "Change the camera rightward catch-up speed of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyRightwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "clamp(0, 1, GetArgumentAsNumber(\"RightwardSpeed\"))" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyLogRightwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "log(1 - Object.Behavior::PropertyRightwardSpeed())" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Rightward catch-up speed (in ratio per second)", + "name": "RightwardSpeed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera downward catch-up speed (in ratio per second).", + "fullName": "Downward catch-up speed", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetDownwardSpeed", + "sentence": "Change the camera downward catch-up speed of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyDownwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "clamp(0, 1, GetArgumentAsNumber(\"DownwardSpeed\"))" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyLogDownwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "log(1 - Object.Behavior::PropertyDownwardSpeed())" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Downward catch-up speed (in ratio per second)", + "name": "DownwardSpeed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera upward catch-up speed (in ratio per second).", + "fullName": "Upward catch-up speed", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetUpwardSpeed", + "sentence": "Change the camera upward catch-up speed of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyUpwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "clamp(0, 1, GetArgumentAsNumber(\"UpwardSpeed\"))" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyLogUpwardSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "log(1 - Object.Behavior::PropertyUpwardSpeed())" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Upward catch-up speed (in ratio per second)", + "name": "UpwardSpeed", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera offset on X axis of an object.", + "fullName": "Camera Offset X", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetOffsetX", + "sentence": "Change the camera offset on X axis of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyCameraOffsetX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"CameraOffsetX\")" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Camera offset X", + "name": "CameraOffsetX", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera offset on Y axis of an object.", + "fullName": "Camera Offset Y", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetOffsetY", + "sentence": "Change the camera offset on Y axis of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyCameraOffsetY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"CameraOffsetY\")" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Camera offset Y", + "name": "CameraOffsetY", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera forecast time (in seconds).", + "fullName": "Forecast time", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetForecastTime", + "sentence": "Change the camera forecast time of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastTime" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "min(0, GetArgumentAsNumber(\"ForecastTime\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Forecast time", + "name": "ForecastTime", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Change the camera delay (in seconds).", + "fullName": "Camera delay", + "functionType": "Action", + "group": "Camera configuration", + "name": "SetCameraDelay", + "sentence": "Change the camera delay of _PARAM0_: _PARAM2_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyCameraDelay" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "min(0, GetArgumentAsNumber(\"CameraDelay\"))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Camera delay", + "name": "CameraDelay", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Return follow free area left border X.", + "fullName": "Free area left", + "functionType": "Expression", + "group": "Private", + "name": "FreeAreaLeft", + "private": true, + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyForecastedX() + Object.Behavior::PropertyCameraOffsetX() - Object.Behavior::PropertyFollowFreeAreaLeft()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return follow free area right border X.", + "fullName": "Free area right", + "functionType": "Expression", + "group": "Private", + "name": "FreeAreaRight", + "private": true, + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyForecastedX() + Object.Behavior::PropertyCameraOffsetX() + Object.Behavior::PropertyFollowFreeAreaRight()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return follow free area bottom border Y.", + "fullName": "Free area bottom", + "functionType": "Expression", + "group": "Private", + "name": "FreeAreaBottom", + "private": true, + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyForecastedY() + Object.Behavior::PropertyCameraOffsetY() + Object.Behavior::PropertyFollowFreeAreaBottom()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return follow free area top border Y.", + "fullName": "Free area top", + "functionType": "Expression", + "group": "Private", + "name": "FreeAreaTop", + "private": true, + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyForecastedY() + Object.Behavior::PropertyCameraOffsetY() - Object.Behavior::PropertyFollowFreeAreaTop()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Update delayed position and delayed history. This is called in doStepPreEvents.", + "fullName": "Update delayed position", + "functionType": "Action", + "group": "Private", + "name": "UpdateDelayedPosition", + "private": true, + "sentence": "Update delayed position and delayed history of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Add the previous position to have enough (2) positions to evaluate the extra delay for waiting mode.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::IsWaiting" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + }, + { + "type": { + "value": "Egal" + }, + "parameters": [ + "Object.VariableChildCount(__SmoothCamera.ObjectTime)", + "=", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "ObjectVariablePushNumber" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectTime", + "TimeFromStart()" + ] + }, + { + "type": { + "value": "ObjectVariablePushNumber" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectX", + "Object.Behavior::PropertyDelayedCenterX()" + ] + }, + { + "type": { + "value": "ObjectVariablePushNumber" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectY", + "Object.Behavior::PropertyDelayedCenterY()" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Use the object center when no delay is asked.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyDelayedCenterX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.CenterX()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyDelayedCenterY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.CenterY()" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "SmoothCamera::SmoothCamera::IsDelayed" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::AddForecastHistoryPosition" + }, + "parameters": [ + "Object", + "Behavior", + "TimeFromStart()", + "Object.CenterX()", + "Object.CenterY()", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::IsDelayed" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::IsWaiting" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ] + } + ], + "actions": [ + { + "type": { + "value": "ObjectVariablePushNumber" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectTime", + "TimeFromStart()" + ] + }, + { + "type": { + "value": "ObjectVariablePushNumber" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectX", + "Object.CenterX()" + ] + }, + { + "type": { + "value": "ObjectVariablePushNumber" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectY", + "Object.CenterY()" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Remove history entries that are too old to be useful for delaying and pass it to the history for forecasting.", + "comment2": "" + }, + { + "infiniteLoopWarning": true, + "type": "BuiltinCommonInstructions::While", + "whileConditions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "Object.VariableChildCount(__SmoothCamera.ObjectTime)", + ">=", + "2" + ] + }, + { + "type": { + "value": "VarObjet" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectTime[1]", + "<", + "TimeFromStart() - Object.Behavior::CurrentDelay()" + ] + } + ], + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::AddForecastHistoryPosition" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Variable(__SmoothCamera.ObjectTime[0])", + "Object.Variable(__SmoothCamera.ObjectX[0])", + "Object.Variable(__SmoothCamera.ObjectY[0])", + "" + ] + }, + { + "type": { + "value": "ObjectVariableRemoveAt" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectTime", + "0" + ] + }, + { + "type": { + "value": "ObjectVariableRemoveAt" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectX", + "0" + ] + }, + { + "type": { + "value": "ObjectVariableRemoveAt" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectY", + "0" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Don't move the camera if there is not enough history.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyDelayedCenterX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Variable(__SmoothCamera.ObjectX[0])" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyDelayedCenterY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Variable(__SmoothCamera.ObjectY[0])" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "Object.VariableChildCount(__SmoothCamera.ObjectTime)", + ">=", + "2" + ] + }, + { + "type": { + "value": "VarObjet" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectTime[0]", + "<", + "TimeFromStart() - Object.Behavior::CurrentDelay()" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Add the extra delay that could be needed to respect the speed limit in waiting mode.\n\nspeedRatio = min(speedMaxX / historySpeedX, speedMaxY / historySpeedY)\ndelay += min(0, timeDelta * (1 - speedRatio))", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::IsWaiting" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyCameraExtraDelay" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "max(0, TimeDelta() * (1 - min(Object.Behavior::PropertyWaitingSpeedXMax() * abs(Object.Variable(__SmoothCamera.ObjectX[1]) - Object.Variable(__SmoothCamera.ObjectX[0])), Object.Behavior::PropertyWaitingSpeedYMax() * abs(Object.Variable(__SmoothCamera.ObjectY[1]) - Object.Variable(__SmoothCamera.ObjectY[0]))) / (Object.Variable(__SmoothCamera.ObjectTime[1]) - Object.Variable(__SmoothCamera.ObjectTime[0]))))" + ] + } + ], + "events": [ + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Extra delay: \" + ToString(Object.Behavior::PropertyCameraExtraDelay())", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "The time with delay is now between the first 2 indexes", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyDelayedCenterX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "lerp(Object.Variable(__SmoothCamera.ObjectX[1]), Object.Variable(__SmoothCamera.ObjectX[0]), ((TimeFromStart() - Object.Behavior::CurrentDelay()) - Object.Variable(__SmoothCamera.ObjectTime[1])) / (Object.Variable(__SmoothCamera.ObjectTime[0]) - Object.Variable(__SmoothCamera.ObjectTime[1])))" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyDelayedCenterY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "lerp(Object.Variable(__SmoothCamera.ObjectY[1]), Object.Variable(__SmoothCamera.ObjectY[0]), ((TimeFromStart() - Object.Behavior::CurrentDelay()) - Object.Variable(__SmoothCamera.ObjectTime[1])) / (Object.Variable(__SmoothCamera.ObjectTime[0]) - Object.Variable(__SmoothCamera.ObjectTime[1])))" + ] + } + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "SmoothCamera::SmoothCamera::IsDelayed" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + }, + { + "type": { + "inverted": true, + "value": "SmoothCamera::SmoothCamera::IsWaiting" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "ObjectVariableClearChildren" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectTime" + ] + }, + { + "type": { + "value": "ObjectVariableClearChildren" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectX" + ] + }, + { + "type": { + "value": "ObjectVariableClearChildren" + }, + "parameters": [ + "Object", + "__SmoothCamera.ObjectY" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "SmoothCamera::SmoothCamera::IsWaiting" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::Once" + }, + "parameters": [] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyCameraDelayCatchUpSpeed" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyCameraExtraDelay() / Object.Behavior::PropertyCameraDelayCatchUpDuration()" + ] + } + ], + "events": [ + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Start to catch up\"", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "SmoothCamera::SmoothCamera::IsWaiting" + }, + "parameters": [ + "Object", + "Behavior", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyCameraExtraDelay" + }, + "parameters": [ + "Object", + "Behavior", + ">", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyCameraExtraDelay" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "max(0, Object.Behavior::PropertyCameraExtraDelay() -Object.Behavior::PropertyCameraDelayCatchUpSpeed() * TimeDelta())" + ] + } + ], + "events": [ + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Catching up delay: \" + ToString(Object.Behavior::PropertyCameraExtraDelay())", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the camera following target is delayed from the object.", + "fullName": "Camera is delayed", + "functionType": "Condition", + "name": "IsDelayed", + "private": true, + "sentence": "The camera of _PARAM0_ is delayed", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "Object.Behavior::CurrentDelay()", + ">", + "0" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Return the current camera delay.", + "fullName": "Current delay", + "functionType": "Expression", + "name": "CurrentDelay", + "private": true, + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "Object.Behavior::PropertyCameraDelay() + Object.Behavior::PropertyCameraExtraDelay()" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the camera following is waiting at a reduced speed.", + "fullName": "Camera is waiting", + "functionType": "Condition", + "name": "IsWaiting", + "private": true, + "sentence": "The camera of _PARAM0_ is waiting", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyWaitingEnd" + }, + "parameters": [ + "Object", + "Behavior", + ">", + "TimeFromStart()" + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Add a position to the history for forecasting. This is called 2 times in UpadteDelayedPosition.", + "fullName": "Add forecast history position", + "functionType": "Action", + "group": "Private", + "name": "AddForecastHistoryPosition", + "private": true, + "sentence": "Add the time:_PARAM2_ and position: _PARAM3_; _PARAM4_ to the forecast history of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyForecastHistoryDuration" + }, + "parameters": [ + "Object", + "Behavior", + ">", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyForecastTime" + }, + "parameters": [ + "Object", + "Behavior", + ">", + "0" + ] + } + ] + } + ], + "actions": [ + { + "type": { + "value": "ObjectVariablePushNumber" + }, + "parameters": [ + "Object", + "__SmoothCamera.ForecastHistoryTime", + "GetArgumentAsNumber(\"Time\")" + ] + }, + { + "type": { + "value": "ObjectVariablePushNumber" + }, + "parameters": [ + "Object", + "__SmoothCamera.ForecastHistoryX", + "GetArgumentAsNumber(\"ObjectX\")" + ] + }, + { + "type": { + "value": "ObjectVariablePushNumber" + }, + "parameters": [ + "Object", + "__SmoothCamera.ForecastHistoryY", + "GetArgumentAsNumber(\"ObjectY\")" + ] + } + ], + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Remove history entries that are too old to be useful.\nKeep at least 2 positions because no forecast can be done with less positions.", + "comment2": "" + }, + { + "infiniteLoopWarning": true, + "type": "BuiltinCommonInstructions::While", + "whileConditions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "Object.VariableChildCount(__SmoothCamera.ForecastHistoryTime)", + ">=", + "3" + ] + }, + { + "type": { + "value": "VarObjet" + }, + "parameters": [ + "Object", + "__SmoothCamera.ForecastHistoryTime[0]", + "<", + "TimeFromStart() - Object.Behavior::PropertyCameraDelay() - Object.Behavior::PropertyCameraExtraDelay() - Object.Behavior::PropertyForecastHistoryDuration()" + ] + } + ], + "conditions": [], + "actions": [ + { + "type": { + "value": "ObjectVariableRemoveAt" + }, + "parameters": [ + "Object", + "__SmoothCamera.ForecastHistoryTime", + "0" + ] + }, + { + "type": { + "value": "ObjectVariableRemoveAt" + }, + "parameters": [ + "Object", + "__SmoothCamera.ForecastHistoryX", + "0" + ] + }, + { + "type": { + "value": "ObjectVariableRemoveAt" + }, + "parameters": [ + "Object", + "__SmoothCamera.ForecastHistoryY", + "0" + ] + } + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "Time", + "name": "Time", + "type": "expression" + }, + { + "description": "Object X", + "name": "ObjectX", + "type": "expression" + }, + { + "description": "Object Y", + "name": "ObjectY", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Update forecasted position. This is called in doStepPreEvents.", + "fullName": "Update forecasted position", + "functionType": "Action", + "group": "Private", + "name": "UpdateForecastedPosition", + "private": true, + "sentence": "Update forecasted position of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastedX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyDelayedCenterX()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastedY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyDelayedCenterY()" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Simple linear regression\ny = A * x + B\n\nA = Covariance / VarianceX\nB = MeanY - A * MeanX\n\nNote than we could use only one position every N positions to reduce the process time,\nbut if we really need efficient process JavaScript and circular queues are a must.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "Object.VariableChildCount(__SmoothCamera.ForecastHistoryTime)", + ">=", + "2" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyForecastHistoryDuration" + }, + "parameters": [ + "Object", + "Behavior", + ">", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::PropertyForecastTime" + }, + "parameters": [ + "Object", + "Behavior", + ">", + "0" + ] + } + ] + } + ], + "actions": [], + "events": [ + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Mean X", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryMeanX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Repeat", + "repeatExpression": "Object.VariableChildCount(__SmoothCamera.ForecastHistoryX)", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryMeanX" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "Object.Variable(__SmoothCamera.ForecastHistoryX[Object.Behavior::PropertyIndex()])" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "1" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryMeanX" + }, + "parameters": [ + "Object", + "Behavior", + "/", + "Object.VariableChildCount(__SmoothCamera.ForecastHistoryX)" + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Mean Y", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryMeanY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Repeat", + "repeatExpression": "Object.VariableChildCount(__SmoothCamera.ForecastHistoryY)", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryMeanY" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "Object.Variable(__SmoothCamera.ForecastHistoryY[Object.Behavior::PropertyIndex()])" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "1" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryMeanY" + }, + "parameters": [ + "Object", + "Behavior", + "/", + "Object.VariableChildCount(__SmoothCamera.ForecastHistoryY)" + ] + } + ] + }, + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Mean: \" + ToString(Object.Behavior::PropertyForecastHistoryMeanX()) + \" \" + ToString(Object.Behavior::PropertyForecastHistoryMeanY())", + "", + "" + ] + } + ] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Variance and Covariance", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "VarianceX = sum((X[i] - MeanX)²)\nVarianceY = sum((Y[i] - MeanY)²)\nCovariance = sum((X[i] - MeanX) * (Y[i] - MeanY))", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryVarianceX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryVarianceY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryCovariance" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "0" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Repeat", + "repeatExpression": "Object.VariableChildCount(__SmoothCamera.ForecastHistoryX)", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryVarianceX" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "pow(Object.Variable(__SmoothCamera.ForecastHistoryX[Object.Behavior::PropertyIndex()]) - Object.Behavior::PropertyForecastHistoryMeanX(), 2)" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryVarianceY" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "pow(Object.Variable(__SmoothCamera.ForecastHistoryY[Object.Behavior::PropertyIndex()]) - Object.Behavior::PropertyForecastHistoryMeanY(), 2)" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryCovariance" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "(Object.Variable(__SmoothCamera.ForecastHistoryX[Object.Behavior::PropertyIndex()]) - Object.Behavior::PropertyForecastHistoryMeanX())\n*\n(Object.Variable(__SmoothCamera.ForecastHistoryY[Object.Behavior::PropertyIndex()]) - Object.Behavior::PropertyForecastHistoryMeanY())" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "+", + "1" + ] + } + ] + }, + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Variances: \" + ToString(Object.Behavior::PropertyForecastHistoryVarianceX()) + \" \" + ToString(Object.Behavior::PropertyForecastHistoryVarianceY()) + \" \" + ToString(Object.Behavior::PropertyForecastHistoryCovariance())", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "abs(Object.Behavior::PropertyForecastHistoryVarianceX())", + "<", + "1" + ] + }, + { + "type": { + "value": "Egal" + }, + "parameters": [ + "abs(Object.Behavior::PropertyForecastHistoryVarianceY())", + "<", + "1" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastedX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyDelayedCenterX()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastedY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyDelayedCenterY()" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "abs(Object.Behavior::PropertyForecastHistoryVarianceX())", + ">=", + "1" + ] + }, + { + "type": { + "value": "Egal" + }, + "parameters": [ + "abs(Object.Behavior::PropertyForecastHistoryVarianceY())", + ">=", + "1" + ] + } + ] + } + ], + "actions": [], + "events": [ + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Linear function parameters", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "y = A * x + B\n\nA = Covariance / VarianceX\nB = MeanY - A * MeanX", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "abs(Object.Behavior::PropertyForecastHistoryVarianceX())", + ">=", + "abs(Object.Behavior::PropertyForecastHistoryVarianceY())" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryLinearA" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyForecastHistoryCovariance() / Object.Behavior::PropertyForecastHistoryVarianceX()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryLinearB" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyForecastHistoryMeanY() - Object.Behavior::PropertyForecastHistoryLinearA() * Object.Behavior::PropertyForecastHistoryMeanX()" + ] + } + ], + "events": [ + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Linear: \" + ToString(Object.Behavior::PropertyForecastHistoryLinearA()) + \" \" + ToString(Object.Behavior::PropertyForecastHistoryLinearB())", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Projection", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::ProjectHistoryEnds" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Variable(__SmoothCamera.ForecastHistoryX[0])", + "Object.Variable(__SmoothCamera.ForecastHistoryY[0])", + "Object.Variable(__SmoothCamera.ForecastHistoryX[Object.VariableChildCount(__SmoothCamera.ForecastHistoryX) - 1])", + "Object.Variable(__SmoothCamera.ForecastHistoryY[Object.VariableChildCount(__SmoothCamera.ForecastHistoryY) - 1])", + "" + ] + } + ] + } + ], + "parameters": [] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Axis permutation to avoid a ratio between 2 numbers near 0.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Egal" + }, + "parameters": [ + "abs(Object.Behavior::PropertyForecastHistoryVarianceX())", + "<", + "abs(Object.Behavior::PropertyForecastHistoryVarianceY())" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryLinearA" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyForecastHistoryCovariance() / Object.Behavior::PropertyForecastHistoryVarianceY()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastHistoryLinearB" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyForecastHistoryMeanX() - Object.Behavior::PropertyForecastHistoryLinearA() * Object.Behavior::PropertyForecastHistoryMeanY()" + ] + } + ], + "events": [ + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Linear: \" + ToString(Object.Behavior::PropertyForecastHistoryLinearA()) + \" \" + ToString(Object.Behavior::PropertyForecastHistoryLinearB())", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Projection", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::ProjectHistoryEnds" + }, + "parameters": [ + "Object", + "Behavior", + "Object.Variable(__SmoothCamera.ForecastHistoryY[0])", + "Object.Variable(__SmoothCamera.ForecastHistoryX[0])", + "Object.Variable(__SmoothCamera.ForecastHistoryY[Object.VariableChildCount(__SmoothCamera.ForecastHistoryY) - 1])", + "Object.Variable(__SmoothCamera.ForecastHistoryX[Object.VariableChildCount(__SmoothCamera.ForecastHistoryX) - 1])", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Permute back axis", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyProjectedOldestX()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyProjectedOldestX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyProjectedOldestY()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyProjectedOldestY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyIndex()" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyIndex" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyProjectedNewestX()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyProjectedNewestX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyProjectedNewestY()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyProjectedNewestY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyIndex()" + ] + } + ] + } + ], + "parameters": [] + }, + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Oldest: \" + ToString(Object.Behavior::PropertyProjectedOldestX()) + \" \" + ToString(Object.Behavior::PropertyProjectedOldestY())", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + }, + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Newest: \" + ToString(Object.Behavior::PropertyProjectedNewestX()) + \" \" + ToString(Object.Behavior::PropertyProjectedNewestY())", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + } + ] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Forcasted position", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastedX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyProjectedNewestX() + ( Object.Behavior::PropertyProjectedNewestX() - Object.Behavior::PropertyProjectedOldestX()) * Object.Behavior::ForecastTimeRatio()" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyForecastedY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "Object.Behavior::PropertyProjectedNewestY() + ( Object.Behavior::PropertyProjectedNewestY() - Object.Behavior::PropertyProjectedOldestY()) * Object.Behavior::ForecastTimeRatio()" + ] + } + ] + }, + { + "disabled": true, + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "DebuggerTools::ConsoleLog" + }, + "parameters": [ + "\"Forecasted: \" + ToString(Object.Behavior::PropertyForecastedX()) + \" \" + ToString(Object.Behavior::PropertyForecastedY())", + "\"info\"", + "\"SmoothCamera\"" + ] + } + ] + } + ], + "parameters": [] + } + ], + "parameters": [] + } + ] + } + ], + "parameters": [] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Project history ends position to have the vector on the line from linear regression. This function is only called by UpdateForecastedPosition.", + "fullName": "Project history ends", + "functionType": "Action", + "group": "Private", + "name": "ProjectHistoryEnds", + "private": true, + "sentence": "Project history oldest: _PARAM2_;_PARAM3_ and newest position: _PARAM4_;_PARAM5_ of _PARAM0_", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "Perpendicular line:\npA = -1/a; \npB = -pA * x + y\n\nIntersection:\n/ ProjectedY = a * ProjectedX + b\n\\ ProjectedY = pA * ProjectedX + b\n\nSolution that is cleaned out from indeterminism (like 0 / 0 or infinity / infinity):\nProjectedX= (x + (y - b) * a) / (a² + 1)\nProjectedY = y + (x * a - y + b) / (a² + 1)", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyProjectedNewestX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "(GetArgumentAsNumber(\"NewestX\") + (GetArgumentAsNumber(\"NewestY\") - Object.Behavior::PropertyForecastHistoryLinearB()) * Object.Behavior::PropertyForecastHistoryLinearA()) / (1 + pow(Object.Behavior::PropertyForecastHistoryLinearA(), 2))" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyProjectedNewestY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"NewestY\") + (GetArgumentAsNumber(\"NewestX\") * Object.Behavior::PropertyForecastHistoryLinearA() - GetArgumentAsNumber(\"NewestY\") \n+ Object.Behavior::PropertyForecastHistoryLinearB()) / (1 + pow(Object.Behavior::PropertyForecastHistoryLinearA(), 2))" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyProjectedOldestX" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "(GetArgumentAsNumber(\"OldestX\") + (GetArgumentAsNumber(\"OldestY\") - Object.Behavior::PropertyForecastHistoryLinearB()) * Object.Behavior::PropertyForecastHistoryLinearA()) / (1 + pow(Object.Behavior::PropertyForecastHistoryLinearA(), 2))" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetPropertyProjectedOldestY" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "GetArgumentAsNumber(\"OldestY\") + (GetArgumentAsNumber(\"OldestX\") * Object.Behavior::PropertyForecastHistoryLinearA() - GetArgumentAsNumber(\"OldestY\") \n+ Object.Behavior::PropertyForecastHistoryLinearB()) / (1 + pow(Object.Behavior::PropertyForecastHistoryLinearA(), 2))" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + }, + { + "description": "OldestX", + "name": "OldestX", + "type": "expression" + }, + { + "description": "OldestY", + "name": "OldestY", + "type": "expression" + }, + { + "description": "Newest X", + "name": "NewestX", + "type": "expression" + }, + { + "description": "Newest Y", + "name": "NewestY", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Return the ratio between forecast time and the duration of the history. This function is only called by UpdateForecastedPosition.", + "fullName": "Forecast time ratio", + "functionType": "Expression", + "group": "Private", + "name": "ForecastTimeRatio", + "private": true, + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "SetReturnNumber" + }, + "parameters": [ + "- Object.Behavior::PropertyForecastTime() / (Object.Variable(__SmoothCamera.ForecastHistoryTime[0]) - Object.Variable(__SmoothCamera.ForecastHistoryTime[Object.VariableChildCount(__SmoothCamera.ForecastHistoryTime) - 1]))" + ] + } + ] + } + ], + "expressionType": { + "type": "expression" + }, + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothCamera", + "type": "behavior" + } + ], + "objectGroups": [] + } + ], + "propertyDescriptors": [ + { + "value": "0.9", + "type": "Number", + "label": "Leftward catch-up speed (in ratio per second)", + "description": "", + "group": "Catch-up speed", + "extraInformation": [], + "hidden": false, + "name": "LeftwardSpeed" + }, + { + "value": "0.9", + "type": "Number", + "label": "Rightward catch-up speed (in ratio per second)", + "description": "", + "group": "Catch-up speed", + "extraInformation": [], + "hidden": false, + "name": "RightwardSpeed" + }, + { + "value": "0.9", + "type": "Number", + "label": "Upward catch-up speed (in ratio per second)", + "description": "", + "group": "Catch-up speed", + "extraInformation": [], + "hidden": false, + "name": "UpwardSpeed" + }, + { + "value": "0.9", + "type": "Number", + "label": "Downward catch-up speed (in ratio per second)", + "description": "", + "group": "Catch-up speed", + "extraInformation": [], + "hidden": false, + "name": "DownwardSpeed" + }, + { + "value": "true", + "type": "Boolean", + "label": "Follow on X axis", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "FollowOnX" + }, + { + "value": "true", + "type": "Boolean", + "label": "Follow on Y axis", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "FollowOnY" + }, + { + "value": "0", + "type": "Number", + "label": "Follow free area left border", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "FollowFreeAreaLeft" + }, + { + "value": "0", + "type": "Number", + "label": "Follow free area right border", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "FollowFreeAreaRight" + }, + { + "value": "0", + "type": "Number", + "label": "Follow free area top border", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "FollowFreeAreaTop" + }, + { + "value": "0", + "type": "Number", + "label": "Follow free area bottom border", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "FollowFreeAreaBottom" + }, + { + "value": "0", + "type": "Number", + "label": "Camera offset X", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "CameraOffsetX" + }, + { + "value": "0", + "type": "Number", + "label": "Camera offset Y", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "CameraOffsetY" + }, + { + "value": "0", + "type": "Number", + "label": "Camera delay (in seconds)", + "description": "", + "group": "Timing", + "extraInformation": [], + "hidden": false, + "name": "CameraDelay" + }, + { + "value": "0", + "type": "Number", + "label": "Forcast time (in seconds)", + "description": "", + "group": "Timing", + "extraInformation": [], + "hidden": false, + "name": "ForecastTime" + }, + { + "value": "0", + "type": "Number", + "label": "Forecast history duration (in second)", + "description": "", + "group": "Timing", + "extraInformation": [], + "hidden": false, + "name": "ForecastHistoryDuration" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "LogLeftwardSpeed" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "LogRightwardSpeed" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "LogDownwardSpeed" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "LogUpwardSpeed" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "DelayedCenterX" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "DelayedCenterY" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ForecastHistoryMeanX" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ForecastHistoryMeanY" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ForecastHistoryVarianceX" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ForecastHistoryCovariance" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ForecastHistoryLinearA" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ForecastHistoryLinearB" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ForecastedX" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ForecastedY" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ProjectedNewestX" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ProjectedNewestY" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ProjectedOldestX" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ProjectedOldestY" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "ForecastHistoryVarianceY" + }, + { + "value": "", + "type": "Number", + "label": "Index (local variable)", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "Index" + }, + { + "value": "0", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "CameraDelayCatchUpSpeed" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "CameraExtraDelay" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "WaitingSpeedXMax" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "WaitingSpeedYMax" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "WaitingEnd" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "CameraDelayCatchUpDuration" + }, + { + "value": "9000", + "type": "Number", + "label": "Leftward maximum speed (in pixels per second)", + "description": "", + "group": "Maximum speed", + "extraInformation": [], + "hidden": false, + "name": "LeftwardSpeedMax" + }, + { + "value": "9000", + "type": "Number", + "label": "Rightward maximum speed (in pixels per second)", + "description": "", + "group": "Maximum speed", + "extraInformation": [], + "hidden": false, + "name": "RightwardSpeedMax" + }, + { + "value": "9000", + "type": "Number", + "label": "Upward maximum speed (in pixels per second)", + "description": "", + "group": "Maximum speed", + "extraInformation": [], + "hidden": false, + "name": "UpwardSpeedMax" + }, + { + "value": "9000", + "type": "Number", + "label": "Downward maximum speed (in pixels per second)", + "description": "", + "group": "Maximum speed", + "extraInformation": [], + "hidden": false, + "name": "DownwardSpeedMax" + }, + { + "value": "", + "type": "Number", + "label": "OldX (local variable)", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "OldX" + }, + { + "value": "", + "type": "Number", + "label": "OldY (local variable)", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "OldY" + }, + { + "value": "", + "type": "Boolean", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "IsCalledManually" + } + ], + "sharedPropertyDescriptors": [] + }, + { + "description": "Smoothly scroll to follow a character and stabilize the camera when jumping.", + "fullName": "Smooth platformer camera", + "name": "SmoothPlatformerCamera", + "objectType": "", + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "doStepPreEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "PlatformBehavior::IsJumping" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + }, + { + "type": { + "inverted": true, + "value": "PlatformBehavior::IsFalling" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetFollowFreeAreaBottom" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyFloorFollowFreeAreaTop()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetFollowFreeAreaTop" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyFloorFollowFreeAreaBottom()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetUpwardSpeed" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyFloorUpwardSpeed()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetDownwardSpeed" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyFloorDownwardSpeed()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetUpwardSpeedMax" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyFloorUpwardSpeedMax()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetDownwardSpeedMax" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyFloorDownwardSpeedMax()", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "PlatformBehavior::IsJumping" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + }, + { + "type": { + "value": "PlatformBehavior::IsFalling" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + } + ], + "actions": [ + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetFollowFreeAreaBottom" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyAirFollowFreeAreaTop()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetFollowFreeAreaTop" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyAirFollowFreeAreaBottom()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetUpwardSpeed" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyAirUpwardSpeed()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetDownwardSpeed" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyAirDownwardSpeed()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetUpwardSpeedMax" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyAirUpwardSpeedMax()", + "" + ] + }, + { + "type": { + "value": "SmoothCamera::SmoothCamera::SetDownwardSpeedMax" + }, + "parameters": [ + "Object", + "SmoothCamera", + "Object.Behavior::PropertyAirDownwardSpeedMax()", + "" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "SmoothCamera::SmoothPlatformerCamera", + "type": "behavior" + } + ], + "objectGroups": [] + } + ], + "propertyDescriptors": [ + { + "value": "", + "type": "Behavior", + "label": "Platformer character behavior", + "description": "", + "group": "", + "extraInformation": [ + "PlatformBehavior::PlatformerObjectBehavior" + ], + "hidden": false, + "name": "PlatformerCharacter" + }, + { + "value": "", + "type": "Behavior", + "label": "Smooth camera behavior", + "description": "", + "group": "", + "extraInformation": [ + "SmoothCamera::SmoothCamera" + ], + "hidden": false, + "name": "SmoothCamera" + }, + { + "value": "", + "type": "Number", + "label": "", + "description": "", + "group": "", + "extraInformation": [], + "hidden": true, + "name": "JumpOriginY" + }, + { + "value": "0", + "type": "Number", + "label": "Follow free area top in the air", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "AirFollowFreeAreaTop" + }, + { + "value": "0", + "type": "Number", + "label": "Follow free area bottom in the air", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "AirFollowFreeAreaBottom" + }, + { + "value": "0", + "type": "Number", + "label": "Follow free area top on the floor", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "FloorFollowFreeAreaTop" + }, + { + "value": "0", + "type": "Number", + "label": "Follow free area bottom on the floor", + "description": "", + "group": "Position", + "extraInformation": [], + "hidden": false, + "name": "FloorFollowFreeAreaBottom" + }, + { + "value": "0.95", + "type": "Number", + "label": "Upward speed in the air (in ratio persecond)", + "description": "", + "group": "Catch-up speed", + "extraInformation": [], + "hidden": false, + "name": "AirUpwardSpeed" + }, + { + "value": "0.95", + "type": "Number", + "label": "Downward speed in the air (in ratio persecond)", + "description": "", + "group": "Catch-up speed", + "extraInformation": [], + "hidden": false, + "name": "AirDownwardSpeed" + }, + { + "value": "0.9", + "type": "Number", + "label": "Upward speed on the floor (in ratio persecond)", + "description": "", + "group": "Catch-up speed", + "extraInformation": [], + "hidden": false, + "name": "FloorUpwardSpeed" + }, + { + "value": "0.9", + "type": "Number", + "label": "Downward speed on the floor (in ratio persecond)", + "description": "", + "group": "Catch-up speed", + "extraInformation": [], + "hidden": false, + "name": "FloorDownwardSpeed" + }, + { + "value": "9000", + "type": "Number", + "label": "Upward maximum speed in the air (in pixels per second)", + "description": "", + "group": "Maximum speed", + "extraInformation": [], + "hidden": false, + "name": "AirUpwardSpeedMax" + }, + { + "value": "9000", + "type": "Number", + "label": "Downward maximum speed in the air (in pixels per second)", + "description": "", + "group": "Maximum speed", + "extraInformation": [], + "hidden": false, + "name": "AirDownwardSpeedMax" + }, + { + "value": "9000", + "type": "Number", + "label": "Upward maximum speed on the floor (in pixels per second)", + "description": "", + "group": "Maximum speed", + "extraInformation": [], + "hidden": false, + "name": "FloorUpwardSpeedMax" + }, + { + "value": "9000", + "type": "Number", + "label": "Downward maximum speed on the floor (in pixels per second)", + "description": "", + "group": "Maximum speed", + "extraInformation": [], + "hidden": false, + "name": "FloorDownwardSpeedMax" + } + ], + "sharedPropertyDescriptors": [] + } + ], + "eventsBasedObjects": [] + }, { "author": "", "category": "Movement", From 7449c49f9c28baed2b5058afce66696ac55672ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Mon, 5 Dec 2022 01:03:55 +0100 Subject: [PATCH 07/11] Clean up events. --- examples/isometric-game/isometric-game.json | 1914 ++++++++++--------- 1 file changed, 1019 insertions(+), 895 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index 20b62194a..04c8e31a8 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -4547,7 +4547,7 @@ "gridColor": 0, "gridAlpha": 0.5, "snap": true, - "zoomFactor": 0.4099999755859377, + "zoomFactor": 0.9433332611083983, "windowMask": false }, "objectsGroups": [ @@ -8358,7 +8358,7 @@ "image": "idle_move0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8400,7 +8400,7 @@ "image": "idle_move0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8442,7 +8442,7 @@ "image": "idle_move0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8484,7 +8484,7 @@ "image": "idle_move0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8526,7 +8526,7 @@ "image": "idle_move0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8568,7 +8568,7 @@ "image": "idle_move0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8610,7 +8610,7 @@ "image": "idle_move0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8652,7 +8652,7 @@ "image": "idle_move0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8694,7 +8694,7 @@ "image": "idle_move0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8736,7 +8736,7 @@ "image": "idle_move0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8778,7 +8778,7 @@ "image": "idle_move0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8820,7 +8820,7 @@ "image": "idle_move0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8862,7 +8862,7 @@ "image": "idle_move0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8904,7 +8904,7 @@ "image": "idle_move0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8946,7 +8946,7 @@ "image": "idle_move0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -8988,7 +8988,7 @@ "image": "idle_move0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9030,7 +9030,7 @@ "image": "idle_move0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9072,7 +9072,7 @@ "image": "idle_move0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9114,7 +9114,7 @@ "image": "idle_move0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9156,7 +9156,7 @@ "image": "idle_move0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9198,7 +9198,7 @@ "image": "idle_move0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9240,7 +9240,7 @@ "image": "idle_move0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9282,7 +9282,7 @@ "image": "idle_move0023.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9324,7 +9324,7 @@ "image": "idle_move0024.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9366,7 +9366,7 @@ "image": "idle_move0025.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9408,7 +9408,7 @@ "image": "idle_move0026.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9450,7 +9450,7 @@ "image": "idle_move0027.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9492,7 +9492,7 @@ "image": "idle_move0028.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9534,7 +9534,7 @@ "image": "idle_move0029.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9576,7 +9576,7 @@ "image": "idle_move0030.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9618,7 +9618,7 @@ "image": "idle_move0031.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9660,7 +9660,7 @@ "image": "idle_move0032.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9702,7 +9702,7 @@ "image": "idle_move0033.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9744,7 +9744,7 @@ "image": "idle_move0034.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9786,7 +9786,7 @@ "image": "idle_move0035.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9828,7 +9828,7 @@ "image": "idle_move0036.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9870,7 +9870,7 @@ "image": "idle_move0037.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9912,7 +9912,7 @@ "image": "idle_move0038.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9954,7 +9954,7 @@ "image": "idle_move0039.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -9996,7 +9996,7 @@ "image": "idle_move0040.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10038,7 +10038,7 @@ "image": "idle_move0041.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10080,7 +10080,7 @@ "image": "idle_move0042.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10122,7 +10122,7 @@ "image": "idle_move0043.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10164,7 +10164,7 @@ "image": "idle_move0044.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10206,7 +10206,7 @@ "image": "idle_move0045.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10248,7 +10248,7 @@ "image": "idle_move0046.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10290,7 +10290,7 @@ "image": "idle_move0047.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10332,7 +10332,7 @@ "image": "idle_move0048.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10374,7 +10374,7 @@ "image": "idle_move0049.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10416,7 +10416,7 @@ "image": "idle_move0050.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10458,7 +10458,7 @@ "image": "idle_move0051.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10500,7 +10500,7 @@ "image": "idle_move0052.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10542,7 +10542,7 @@ "image": "idle_move0053.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10584,7 +10584,7 @@ "image": "idle_move0054.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10626,7 +10626,7 @@ "image": "idle_move0055.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10668,7 +10668,7 @@ "image": "idle_move0056.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10710,7 +10710,7 @@ "image": "idle_move0057.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10752,7 +10752,7 @@ "image": "idle_move0058.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10794,7 +10794,7 @@ "image": "idle_move0059.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10836,7 +10836,7 @@ "image": "idle_move0060.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10890,7 +10890,7 @@ "image": "run_aright0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10932,7 +10932,7 @@ "image": "run_aright0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -10974,7 +10974,7 @@ "image": "run_aright0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11016,7 +11016,7 @@ "image": "run_aright0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11058,7 +11058,7 @@ "image": "run_aright0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11100,7 +11100,7 @@ "image": "run_aright0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11142,7 +11142,7 @@ "image": "run_aright0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11184,7 +11184,7 @@ "image": "run_aright0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11226,7 +11226,7 @@ "image": "run_aright0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11268,7 +11268,7 @@ "image": "run_aright0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11310,7 +11310,7 @@ "image": "run_aright0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11352,7 +11352,7 @@ "image": "run_aright0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11394,7 +11394,7 @@ "image": "run_aright0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11436,7 +11436,7 @@ "image": "run_aright0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11478,7 +11478,7 @@ "image": "run_aright0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11520,7 +11520,7 @@ "image": "run_aright0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11562,7 +11562,7 @@ "image": "run_aright0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11604,7 +11604,7 @@ "image": "run_aright0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11646,7 +11646,7 @@ "image": "run_aright0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11688,7 +11688,7 @@ "image": "run_aright0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11730,7 +11730,7 @@ "image": "run_aright0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11772,7 +11772,7 @@ "image": "run_aright0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11826,7 +11826,7 @@ "image": "run_right0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11868,7 +11868,7 @@ "image": "run_right0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11910,7 +11910,7 @@ "image": "run_right0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11952,7 +11952,7 @@ "image": "run_right0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -11994,7 +11994,7 @@ "image": "run_right0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12036,7 +12036,7 @@ "image": "run_right0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12078,7 +12078,7 @@ "image": "run_right0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12120,7 +12120,7 @@ "image": "run_right0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12162,7 +12162,7 @@ "image": "run_right0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12204,7 +12204,7 @@ "image": "run_right0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12246,7 +12246,7 @@ "image": "run_right0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12288,7 +12288,7 @@ "image": "run_right0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12330,7 +12330,7 @@ "image": "run_right0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12372,7 +12372,7 @@ "image": "run_right0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12414,7 +12414,7 @@ "image": "run_right0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12456,7 +12456,7 @@ "image": "run_right0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12498,7 +12498,7 @@ "image": "run_right0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12540,7 +12540,7 @@ "image": "run_right0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12582,7 +12582,7 @@ "image": "run_right0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12624,7 +12624,7 @@ "image": "run_right0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12666,7 +12666,7 @@ "image": "run_right0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12708,7 +12708,7 @@ "image": "run_right0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12762,7 +12762,7 @@ "image": "run_down0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12804,7 +12804,7 @@ "image": "run_down0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12846,7 +12846,7 @@ "image": "run_down0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12888,7 +12888,7 @@ "image": "run_down0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12930,7 +12930,7 @@ "image": "run_down0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -12972,7 +12972,7 @@ "image": "run_down0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13014,7 +13014,7 @@ "image": "run_down0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13056,7 +13056,7 @@ "image": "run_down0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13098,7 +13098,7 @@ "image": "run_down0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13140,7 +13140,7 @@ "image": "run_down0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13182,7 +13182,7 @@ "image": "run_down0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13224,7 +13224,7 @@ "image": "run_down0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13266,7 +13266,7 @@ "image": "run_down0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13308,7 +13308,7 @@ "image": "run_down0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13350,7 +13350,7 @@ "image": "run_down0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13392,7 +13392,7 @@ "image": "run_down0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13434,7 +13434,7 @@ "image": "run_down0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13476,7 +13476,7 @@ "image": "run_down0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13518,7 +13518,7 @@ "image": "run_down0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13560,7 +13560,7 @@ "image": "run_down0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13602,7 +13602,7 @@ "image": "run_down0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13644,7 +13644,7 @@ "image": "run_down0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13698,7 +13698,7 @@ "image": "run_back0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13740,7 +13740,7 @@ "image": "run_back0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13782,7 +13782,7 @@ "image": "run_back0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13824,7 +13824,7 @@ "image": "run_back0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13866,7 +13866,7 @@ "image": "run_back0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13908,7 +13908,7 @@ "image": "run_back0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13950,7 +13950,7 @@ "image": "run_back0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -13992,7 +13992,7 @@ "image": "run_back0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14034,7 +14034,7 @@ "image": "run_back0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14076,7 +14076,7 @@ "image": "run_back0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14118,7 +14118,7 @@ "image": "run_back0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14160,7 +14160,7 @@ "image": "run_back0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14202,7 +14202,7 @@ "image": "run_back0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14244,7 +14244,7 @@ "image": "run_back0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14286,7 +14286,7 @@ "image": "run_back0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14328,7 +14328,7 @@ "image": "run_back0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14370,7 +14370,7 @@ "image": "run_back0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14412,7 +14412,7 @@ "image": "run_back0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14454,7 +14454,7 @@ "image": "run_back0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14496,7 +14496,7 @@ "image": "run_back0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14538,7 +14538,7 @@ "image": "run_back0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14580,7 +14580,7 @@ "image": "run_back0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14634,7 +14634,7 @@ "image": "run_aleft0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14676,7 +14676,7 @@ "image": "run_aleft0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14718,7 +14718,7 @@ "image": "run_aleft0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14760,7 +14760,7 @@ "image": "run_aleft0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14802,7 +14802,7 @@ "image": "run_aleft0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14844,7 +14844,7 @@ "image": "run_aleft0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14886,7 +14886,7 @@ "image": "run_aleft0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14928,7 +14928,7 @@ "image": "run_aleft0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -14970,7 +14970,7 @@ "image": "run_aleft0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15012,7 +15012,7 @@ "image": "run_aleft0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15054,7 +15054,7 @@ "image": "run_aleft0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15096,7 +15096,7 @@ "image": "run_aleft0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15138,7 +15138,7 @@ "image": "run_aleft0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15180,7 +15180,7 @@ "image": "run_aleft0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15222,7 +15222,7 @@ "image": "run_aleft0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15264,7 +15264,7 @@ "image": "run_aleft0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15306,7 +15306,7 @@ "image": "run_aleft0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15348,7 +15348,7 @@ "image": "run_aleft0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15390,7 +15390,7 @@ "image": "run_aleft0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15432,7 +15432,7 @@ "image": "run_aleft0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15474,7 +15474,7 @@ "image": "run_aleft0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15516,7 +15516,7 @@ "image": "run_aleft0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15570,7 +15570,7 @@ "image": "run_left0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15612,7 +15612,7 @@ "image": "run_left0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15654,7 +15654,7 @@ "image": "run_left0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15696,7 +15696,7 @@ "image": "run_left0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15738,7 +15738,7 @@ "image": "run_left0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15780,7 +15780,7 @@ "image": "run_left0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15822,7 +15822,7 @@ "image": "run_left0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15864,7 +15864,7 @@ "image": "run_left0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15906,7 +15906,7 @@ "image": "run_left0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15948,7 +15948,7 @@ "image": "run_left0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -15990,7 +15990,7 @@ "image": "run_left0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16032,7 +16032,7 @@ "image": "run_left0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16074,7 +16074,7 @@ "image": "run_left0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16116,7 +16116,7 @@ "image": "run_left0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16158,7 +16158,7 @@ "image": "run_left0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16200,7 +16200,7 @@ "image": "run_left0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16242,7 +16242,7 @@ "image": "run_left0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16284,7 +16284,7 @@ "image": "run_left0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16326,7 +16326,7 @@ "image": "run_left0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16368,7 +16368,7 @@ "image": "run_left0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16410,7 +16410,7 @@ "image": "run_left0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16452,7 +16452,7 @@ "image": "run_left0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16506,7 +16506,7 @@ "image": "run_up0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16548,7 +16548,7 @@ "image": "run_up0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16590,7 +16590,7 @@ "image": "run_up0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16632,7 +16632,7 @@ "image": "run_up0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16674,7 +16674,7 @@ "image": "run_up0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16716,7 +16716,7 @@ "image": "run_up0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16758,7 +16758,7 @@ "image": "run_up0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16800,7 +16800,7 @@ "image": "run_up0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16842,7 +16842,7 @@ "image": "run_up0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16884,7 +16884,7 @@ "image": "run_up0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16926,7 +16926,7 @@ "image": "run_up0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -16968,7 +16968,7 @@ "image": "run_up0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17010,7 +17010,7 @@ "image": "run_up0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17052,7 +17052,7 @@ "image": "run_up0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17094,7 +17094,7 @@ "image": "run_up0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17136,7 +17136,7 @@ "image": "run_up0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17178,7 +17178,7 @@ "image": "run_up0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17220,7 +17220,7 @@ "image": "run_up0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17262,7 +17262,7 @@ "image": "run_up0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17304,7 +17304,7 @@ "image": "run_up0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17346,7 +17346,7 @@ "image": "run_up0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17388,7 +17388,7 @@ "image": "run_up0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17442,7 +17442,7 @@ "image": "run_top0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17484,7 +17484,7 @@ "image": "run_top0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17526,7 +17526,7 @@ "image": "run_top0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17568,7 +17568,7 @@ "image": "run_top0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17610,7 +17610,7 @@ "image": "run_top0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17652,7 +17652,7 @@ "image": "run_top0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17694,7 +17694,7 @@ "image": "run_top0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17736,7 +17736,7 @@ "image": "run_top0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17778,7 +17778,7 @@ "image": "run_top0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17820,7 +17820,7 @@ "image": "run_top0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17862,7 +17862,7 @@ "image": "run_top0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17904,7 +17904,7 @@ "image": "run_top0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17946,7 +17946,7 @@ "image": "run_top0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -17988,7 +17988,7 @@ "image": "run_top0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18030,7 +18030,7 @@ "image": "run_top0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18072,7 +18072,7 @@ "image": "run_top0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18114,7 +18114,7 @@ "image": "run_top0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18156,7 +18156,7 @@ "image": "run_top0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18198,7 +18198,7 @@ "image": "run_top0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18240,7 +18240,7 @@ "image": "run_top0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18282,7 +18282,7 @@ "image": "run_top0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18324,7 +18324,7 @@ "image": "run_top0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18378,7 +18378,7 @@ "image": "idle_talk0001.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18420,7 +18420,7 @@ "image": "idle_talk0002.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18462,7 +18462,7 @@ "image": "idle_talk0003.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18504,7 +18504,7 @@ "image": "idle_talk0004.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18546,7 +18546,7 @@ "image": "idle_talk0005.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18588,7 +18588,7 @@ "image": "idle_talk0006.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18630,7 +18630,7 @@ "image": "idle_talk0007.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18672,7 +18672,7 @@ "image": "idle_talk0008.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18714,7 +18714,7 @@ "image": "idle_talk0009.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18756,7 +18756,7 @@ "image": "idle_talk0010.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18798,7 +18798,7 @@ "image": "idle_talk0011.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18840,7 +18840,7 @@ "image": "idle_talk0012.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18882,7 +18882,7 @@ "image": "idle_talk0013.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18924,7 +18924,7 @@ "image": "idle_talk0014.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -18966,7 +18966,7 @@ "image": "idle_talk0015.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19008,7 +19008,7 @@ "image": "idle_talk0016.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19050,7 +19050,7 @@ "image": "idle_talk0017.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19092,7 +19092,7 @@ "image": "idle_talk0018.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19134,7 +19134,7 @@ "image": "idle_talk0019.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19176,7 +19176,7 @@ "image": "idle_talk0020.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19218,7 +19218,7 @@ "image": "idle_talk0021.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19260,7 +19260,7 @@ "image": "idle_talk0022.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19302,7 +19302,7 @@ "image": "idle_talk0023.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19344,7 +19344,7 @@ "image": "idle_talk0024.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19386,7 +19386,7 @@ "image": "idle_talk0025.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19428,7 +19428,7 @@ "image": "idle_talk0026.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19470,7 +19470,7 @@ "image": "idle_talk0027.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19512,7 +19512,7 @@ "image": "idle_talk0028.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19554,7 +19554,7 @@ "image": "idle_talk0029.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19596,7 +19596,7 @@ "image": "idle_talk0030.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19638,7 +19638,7 @@ "image": "idle_talk0031.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19680,7 +19680,7 @@ "image": "idle_talk0032.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19722,7 +19722,7 @@ "image": "idle_talk0033.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19764,7 +19764,7 @@ "image": "idle_talk0034.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19806,7 +19806,7 @@ "image": "idle_talk0035.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19848,7 +19848,7 @@ "image": "idle_talk0036.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19890,7 +19890,7 @@ "image": "idle_talk0037.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19932,7 +19932,7 @@ "image": "idle_talk0038.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -19974,7 +19974,7 @@ "image": "idle_talk0039.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20016,7 +20016,7 @@ "image": "idle_talk0040.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20058,7 +20058,7 @@ "image": "idle_talk0041.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20100,7 +20100,7 @@ "image": "idle_talk0042.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20142,7 +20142,7 @@ "image": "idle_talk0043.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20184,7 +20184,7 @@ "image": "idle_talk0044.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20226,7 +20226,7 @@ "image": "idle_talk0045.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20268,7 +20268,7 @@ "image": "idle_talk0046.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20310,7 +20310,7 @@ "image": "idle_talk0047.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20352,7 +20352,7 @@ "image": "idle_talk0048.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20394,7 +20394,7 @@ "image": "idle_talk0049.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20436,7 +20436,7 @@ "image": "idle_talk0050.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20478,7 +20478,7 @@ "image": "idle_talk0051.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20520,7 +20520,7 @@ "image": "idle_talk0052.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20562,7 +20562,7 @@ "image": "idle_talk0053.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20604,7 +20604,7 @@ "image": "idle_talk0054.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20646,7 +20646,7 @@ "image": "idle_talk0055.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20688,7 +20688,7 @@ "image": "idle_talk0056.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20730,7 +20730,7 @@ "image": "idle_talk0057.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20772,7 +20772,7 @@ "image": "idle_talk0058.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20814,7 +20814,7 @@ "image": "idle_talk0059.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20856,7 +20856,7 @@ "image": "idle_talk0060.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20898,7 +20898,7 @@ "image": "idle_talk0061.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20940,7 +20940,7 @@ "image": "idle_talk0062.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -20982,7 +20982,7 @@ "image": "idle_talk0063.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21024,7 +21024,7 @@ "image": "idle_talk0064.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21066,7 +21066,7 @@ "image": "idle_talk0065.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21108,7 +21108,7 @@ "image": "idle_talk0066.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21150,7 +21150,7 @@ "image": "idle_talk0067.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21192,7 +21192,7 @@ "image": "idle_talk0068.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21234,7 +21234,7 @@ "image": "idle_talk0069.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21276,7 +21276,7 @@ "image": "idle_talk0070.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21318,7 +21318,7 @@ "image": "idle_talk0071.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21360,7 +21360,7 @@ "image": "idle_talk0072.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21402,7 +21402,7 @@ "image": "idle_talk0073.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21444,7 +21444,7 @@ "image": "idle_talk0074.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21486,7 +21486,7 @@ "image": "idle_talk0075.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21528,7 +21528,7 @@ "image": "idle_talk0076.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21570,7 +21570,7 @@ "image": "idle_talk0077.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21612,7 +21612,7 @@ "image": "idle_talk0078.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21654,7 +21654,7 @@ "image": "idle_talk0079.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21696,7 +21696,7 @@ "image": "idle_talk0080.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21738,7 +21738,7 @@ "image": "idle_talk0081.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21780,7 +21780,7 @@ "image": "idle_talk0082.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21822,7 +21822,7 @@ "image": "idle_talk0083.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21864,7 +21864,7 @@ "image": "idle_talk0084.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21906,7 +21906,7 @@ "image": "idle_talk0085.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21948,7 +21948,7 @@ "image": "idle_talk0086.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -21990,7 +21990,7 @@ "image": "idle_talk0087.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22032,7 +22032,7 @@ "image": "idle_talk0088.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22074,7 +22074,7 @@ "image": "idle_talk0089.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22116,7 +22116,7 @@ "image": "idle_talk0090.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22158,7 +22158,7 @@ "image": "idle_talk0091.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22200,7 +22200,7 @@ "image": "idle_talk0092.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22242,7 +22242,7 @@ "image": "idle_talk0093.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22284,7 +22284,7 @@ "image": "idle_talk0094.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22326,7 +22326,7 @@ "image": "idle_talk0095.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22368,7 +22368,7 @@ "image": "idle_talk0096.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22410,7 +22410,7 @@ "image": "idle_talk0097.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22452,7 +22452,7 @@ "image": "idle_talk0098.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22494,7 +22494,7 @@ "image": "idle_talk0099.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22536,7 +22536,7 @@ "image": "idle_talk0100.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22578,7 +22578,7 @@ "image": "idle_talk0101.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22620,7 +22620,7 @@ "image": "idle_talk0102.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22662,7 +22662,7 @@ "image": "idle_talk0103.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22704,7 +22704,7 @@ "image": "idle_talk0104.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22746,7 +22746,7 @@ "image": "idle_talk0105.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22788,7 +22788,7 @@ "image": "idle_talk0106.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22830,7 +22830,7 @@ "image": "idle_talk0107.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22872,7 +22872,7 @@ "image": "idle_talk0108.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22914,7 +22914,7 @@ "image": "idle_talk0109.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22956,7 +22956,7 @@ "image": "idle_talk0110.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -22998,7 +22998,7 @@ "image": "idle_talk0111.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -23040,7 +23040,7 @@ "image": "idle_talk0112.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -23082,7 +23082,7 @@ "image": "idle_talk0113.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -23124,7 +23124,7 @@ "image": "idle_talk0114.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -23166,7 +23166,7 @@ "image": "idle_talk0115.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -23208,7 +23208,7 @@ "image": "idle_talk0116.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -23250,7 +23250,7 @@ "image": "idle_talk0117.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -23292,7 +23292,7 @@ "image": "idle_talk0118.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -23334,7 +23334,7 @@ "image": "idle_talk0119.png", "points": [ { - "name": "footcollis", + "name": "Foot", "x": 128, "y": 220 } @@ -25134,20 +25134,20 @@ "customCollisionMask": [ [ { - "x": 129, - "y": 95.5 + "x": 133, + "y": 4 }, { - "x": 255, - "y": 169.5 + "x": 256, + "y": 83 }, { - "x": 134, - "y": 237.5 + "x": 131, + "y": 153 }, { - "x": 1, - "y": 169.5 + "x": 0, + "y": 83 } ] ] @@ -25194,20 +25194,20 @@ "customCollisionMask": [ [ { - "x": 125, - "y": 35.5 + "x": 128, + "y": 21 }, { - "x": 255, - "y": 100.5 + "x": 243, + "y": 80 }, { - "x": 130, - "y": 167.5 + "x": 129, + "y": 135 }, { - "x": 1, - "y": 103.5 + "x": 12, + "y": 79 } ] ] @@ -25242,20 +25242,20 @@ "customCollisionMask": [ [ { - "x": 125, - "y": 35.5 + "x": 128, + "y": 21 }, { - "x": 255, - "y": 100.5 + "x": 243, + "y": 80 }, { - "x": 130, - "y": 167.5 + "x": 129, + "y": 135 }, { - "x": 1, - "y": 103.5 + "x": 12, + "y": 79 } ] ] @@ -25290,20 +25290,20 @@ "customCollisionMask": [ [ { - "x": 125, - "y": 35.5 + "x": 128, + "y": 21 }, { - "x": 255, - "y": 100.5 + "x": 243, + "y": 80 }, { - "x": 130, - "y": 167.5 + "x": 129, + "y": 135 }, { - "x": 1, - "y": 103.5 + "x": 12, + "y": 79 } ] ] @@ -25338,20 +25338,20 @@ "customCollisionMask": [ [ { - "x": 125, - "y": 35.5 + "x": 128, + "y": 21 }, { - "x": 255, - "y": 100.5 + "x": 243, + "y": 80 }, { - "x": 130, - "y": 167.5 + "x": 129, + "y": 135 }, { - "x": 1, - "y": 103.5 + "x": 12, + "y": 79 } ] ] @@ -25386,20 +25386,20 @@ "customCollisionMask": [ [ { - "x": 125, - "y": 35.5 + "x": 128, + "y": 21 }, { - "x": 255, - "y": 100.5 + "x": 243, + "y": 80 }, { - "x": 130, - "y": 167.5 + "x": 129, + "y": 135 }, { - "x": 1, - "y": 103.5 + "x": 12, + "y": 79 } ] ] @@ -25819,54 +25819,9 @@ "Obstacle", "\"Obstacle\"" ] - }, - { - "type": { - "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetDestination" - }, - "parameters": [ - "Mindy", - "NavMeshPathfindingBehavior", - "Mindy.X()", - "Mindy.Y()", - "Mindy.Y()" - ] - }, - { - "type": { - "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::DrawNavMesh" - }, - "parameters": [ - "Mindy", - "NavMeshPathfindingBehavior", - "NavMesh", - "\"255;187;0\"" - ] } - ] - }, - { - "colorB": 228, - "colorG": 176, - "colorR": 74, - "creationTime": 0, - "name": "Mindy", - "source": "", - "type": "BuiltinCommonInstructions::Group", + ], "events": [ - { - "type": "BuiltinCommonInstructions::Comment", - "color": { - "b": 254, - "g": 19, - "r": 144, - "textB": 255, - "textG": 255, - "textR": 255 - }, - "comment": "Mindy animations", - "comment2": "" - }, { "type": "BuiltinCommonInstructions::Comment", "color": { @@ -25877,393 +25832,285 @@ "textG": 0, "textR": 0 }, - "comment": "When moving, set the animation according to the direction of the movement (we can automatically compute it. For example, if the angle is 180 degrees (i.e: left), the direction will be 1 + 180/45 = 5). This allow us to make only one action, rather than adding conditions for every direction.\nThis supposes that animation #0 is the Idle animation, and the others animation are in the right order (animation #1 is the right, and then ordered in a clock wise manner).", + "comment": "Enable to display the navigation mesh used for path finding.", "comment2": "" }, { + "disabled": true, "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "TopDownMovementBehavior::IsMoving" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - } - ], + "conditions": [], "actions": [ { "type": { - "value": "ChangeAnimation" + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetDestination" }, "parameters": [ "Mindy", - "=", - "1 + mod(round((Mindy.TopDownMovement::Angle())/45+1), 8)" + "NavMeshPathfindingBehavior", + "Mindy.X()", + "Mindy.Y()", + "Mindy.Y()" ] }, { "type": { - "value": "ActivateBehavior" + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::DrawNavMesh" }, "parameters": [ "Mindy", "NavMeshPathfindingBehavior", - "no" + "NavMesh", + "\"255;187;0\"" ] } ] + } + ] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Mindy movement", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Gamepad Control", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "CharacterMovement::MoveWithGamepad" + }, + "parameters": [ + "", + "Mindy", + "TopDownMovement", + "" + ] + } + ] + } + ], + "parameters": [] }, { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Mouse or touch Control", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ { - "type": { - "inverted": true, - "value": "TopDownMovementBehavior::IsMoving" - }, - "parameters": [ - "Mindy", - "TopDownMovement" + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "MouseButtonPressed" + }, + "parameters": [ + "", + "Left" + ] + } + ], + "actions": [ + { + "type": { + "value": "ActivateBehavior" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "yes" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetDestination" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "MouseX(\"\", 0)", + "MouseY(\"\", 0)", + "MouseY(\"\", 0)" + ] + } ] }, { - "type": { - "value": "BuiltinCommonInstructions::Or" + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 }, - "parameters": [], - "subInstructions": [ + "comment": "Gamepad and keyboard have the priority over the mouse or touch.", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ { "type": { - "inverted": true, - "value": "BehaviorActivated" + "value": "TopDownMovementBehavior::IsMoving" }, "parameters": [ "Mindy", - "NavMeshPathfindingBehavior" + "TopDownMovement" ] - }, + } + ], + "actions": [ { "type": { - "inverted": true, - "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" + "value": "ActivateBehavior" }, "parameters": [ "Mindy", "NavMeshPathfindingBehavior", - "" + "no" ] } ] } ], - "actions": [ + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Animation", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ { - "type": { - "value": "ChangeAnimation" - }, - "parameters": [ - "Mindy", - "=", - "0" + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "CharacterMovement::ChooseAnimationDirection" + }, + "parameters": [ + "", + "Mindy", + "NavMeshPathfindingBehavior", + "TopDownMovement", + "" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "CharacterMovement::IsMoving" + }, + "parameters": [ + "Mindy", + "Mindy", + "NavMeshPathfindingBehavior", + "TopDownMovement", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "ChangeAnimation" + }, + "parameters": [ + "Mindy", + "=", + "0" + ] + } ] } - ] - }, + ], + "parameters": [] + } + ], + "parameters": [] + }, + { + "colorB": 228, + "colorG": 176, + "colorR": 74, + "creationTime": 0, + "name": "Camera and collision", + "source": "", + "type": "BuiltinCommonInstructions::Group", + "events": [ { "type": "BuiltinCommonInstructions::Comment", "color": { - "b": 254, - "g": 19, - "r": 144, - "textB": 255, - "textG": 255, - "textR": 255 + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 }, - "comment": "Mouse or touch Control", + "comment": "Handle collision (no conditions is necessary, the actions takes care of handling everything)", "comment2": "" }, { "type": "BuiltinCommonInstructions::Standard", - "conditions": [ + "conditions": [], + "actions": [ { "type": { - "value": "MouseButtonPressed" + "value": "SeparateFromObjects" }, "parameters": [ - "", - "Left" + "Mindy", + "Obstacle" ] } - ], + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], "actions": [ { "type": { - "value": "ActivateBehavior" + "value": "SmoothCamera::SmoothCamera::MoveCameraCloser" }, "parameters": [ "Mindy", - "NavMeshPathfindingBehavior", - "yes" + "SmoothCamera", + "" ] }, { "type": { - "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::SetDestination" - }, - "parameters": [ - "Mindy", - "NavMeshPathfindingBehavior", - "MouseX(\"\", 0)", - "MouseY(\"\", 0)", - "MouseY(\"\", 0)" - ] - }, - { - "type": { - "value": "ModVarObjet" - }, - "parameters": [ - "Mindy", - "nextNodeIndex", - "=", - "0" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" - }, - "parameters": [ - "Mindy", - "NavMeshPathfindingBehavior" - ] - }, - { - "type": { - "value": "VarObjet" - }, - "parameters": [ - "Mindy", - "nextNodeIndex", - "!=", - "Mindy.NavMeshPathfindingBehavior::NextNodeIndex()" - ] - } - ], - "actions": [ - { - "type": { - "value": "ModVarObjet" - }, - "parameters": [ - "Mindy", - "nextNodeIndex", - "=", - "Mindy.NavMeshPathfindingBehavior::NextNodeIndex()" - ] - }, - { - "type": { - "value": "ChangeAnimation" - }, - "parameters": [ - "Mindy", - "=", - "1 + mod(round((Mindy.NavMeshPathfindingBehavior::MovementAngle())/45), 8)" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Comment", - "color": { - "b": 254, - "g": 19, - "r": 144, - "textB": 255, - "textG": 255, - "textR": 255 - }, - "comment": "Gamepad Control", - "comment2": "" - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "Gamepads::C_Axis_pushed" - }, - "parameters": [ - "", - "1", - "\"LEFT\"", - "\"UP\"", - "" - ] - } - ], - "actions": [ - { - "type": { - "value": "TopDownMovementBehavior::SimulateUpKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "Gamepads::C_Axis_pushed" - }, - "parameters": [ - "", - "1", - "\"LEFT\"", - "\"DOWN\"", - "" - ] - } - ], - "actions": [ - { - "type": { - "value": "TopDownMovementBehavior::SimulateDownKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "Gamepads::C_Axis_pushed" - }, - "parameters": [ - "", - "1", - "\"LEFT\"", - "\"LEFT\"", - "" - ] - } - ], - "actions": [ - { - "type": { - "value": "TopDownMovementBehavior::SimulateLeftKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "Gamepads::C_Axis_pushed" - }, - "parameters": [ - "", - "1", - "\"LEFT\"", - "\"RIGHT\"", - "" - ] - } - ], - "actions": [ - { - "type": { - "value": "TopDownMovementBehavior::SimulateRightKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - } - ] - } - ], - "parameters": [] - }, - { - "colorB": 228, - "colorG": 176, - "colorR": 74, - "creationTime": 0, - "name": "Camera and collision", - "source": "", - "type": "BuiltinCommonInstructions::Group", - "events": [ - { - "type": "BuiltinCommonInstructions::Comment", - "color": { - "b": 109, - "g": 230, - "r": 255, - "textB": 0, - "textG": 0, - "textR": 0 - }, - "comment": "Handle collision (no conditions is necessary, the actions takes care of handling everything)", - "comment2": "" - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "SeparateFromObjects" - }, - "parameters": [ - "Mindy", - "Obstacle" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "SmoothCamera::SmoothCamera::MoveCameraCloser" - }, - "parameters": [ - "Mindy", - "SmoothCamera", - "" - ] - }, - { - "type": { - "value": "CopyCameraSettings::CopyCameraSettings" + "value": "CopyCameraSettings::CopyCameraSettings" }, "parameters": [ "", @@ -26348,7 +26195,6 @@ "colorG": 176, "colorR": 74, "creationTime": 0, - "disabled": true, "name": "Sound", "source": "", "type": "BuiltinCommonInstructions::Group", @@ -26358,158 +26204,83 @@ "conditions": [ { "type": { - "value": "BuiltinCommonInstructions::Or" + "value": "CharacterMovement::IsMoving" }, - "parameters": [], - "subInstructions": [ - { - "type": { - "value": "TopDownMovementBehavior::IsMoving" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - }, - { - "type": { - "value": "BuiltinCommonInstructions::And" - }, - "parameters": [], - "subInstructions": [ - { - "type": { - "value": "BehaviorActivated" - }, - "parameters": [ - "Mindy", - "NavMeshPathfindingBehavior" - ] - }, - { - "type": { - "value": "AjoutObjConcern" - }, - "parameters": [ - "", - "Mindy" - ] - }, - { - "type": { - "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" - }, - "parameters": [ - "Mindy", - "NavMeshPathfindingBehavior", - "" - ] - }, - { - "type": { - "value": "AjoutObjConcern" - }, - "parameters": [ - "", - "Mindy" - ] - } - ] - } + "parameters": [ + "Mindy", + "Mindy", + "NavMeshPathfindingBehavior", + "TopDownMovement", + "" ] + }, + { + "type": { + "value": "CollisionPoint" + }, + "parameters": [ + "FloorGrass", + "Mindy.PointX(\"Foot\")", + "Mindy.PointY(\"Foot\")" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::Once" + }, + "parameters": [] } ], - "actions": [], - "events": [ + "actions": [ { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "CollisionPoint" - }, - "parameters": [ - "FloorGrass", - "Mindy.PointX(\"footcollis\")", - "Mindy.PointY(\"footcollis\")" - ] - }, - { - "type": { - "value": "BuiltinCommonInstructions::Once" - }, - "parameters": [] - } - ], - "actions": [ - { - "type": { - "value": "PlaySoundCanal" - }, - "parameters": [ - "", - "grass_light_steps.mp3", - "1", - "yes", - "5", - "" - ] - } + "type": { + "value": "PlaySoundCanal" + }, + "parameters": [ + "", + "grass_light_steps.mp3", + "1", + "yes", + "5", + "" ] - }, + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ { "type": { "value": "CollisionPoint" }, "parameters": [ "Floor", - "Mindy.PointX(\"footcollis\")", - "Mindy.PointY(\"footcollis\")" + "Mindy.PointX(\"Foot\")", + "Mindy.PointY(\"Foot\")" ] }, { "type": { - "value": "BuiltinCommonInstructions::Once" - }, - "parameters": [] - } - ], - "actions": [ - { - "type": { - "value": "StopSoundCanal" + "inverted": true, + "value": "CharacterMovement::IsMoving" }, "parameters": [ - "", - "1" + "Mindy", + "Mindy", + "NavMeshPathfindingBehavior", + "TopDownMovement", + "" ] } ] } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "inverted": true, - "value": "TopDownMovementBehavior::IsMoving" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - }, - { - "type": { - "value": "BuiltinCommonInstructions::Once" - }, - "parameters": [] - } ], "actions": [ { @@ -26640,6 +26411,359 @@ } ], "eventsFunctionsExtensions": [ + { + "author": "", + "category": "", + "extensionNamespace": "", + "fullName": "CharacterMovement", + "helpPath": "", + "iconUrl": "", + "name": "CharacterMovement", + "previewIconUrl": "", + "shortDescription": "", + "version": "", + "description": "Originally automatically extracted from events of the project", + "tags": [], + "authorIds": [], + "dependencies": [], + "eventsFunctions": [ + { + "description": "Move the object according to gamepad inputs.", + "fullName": "Move with gamepad", + "functionType": "Action", + "name": "MoveWithGamepad", + "sentence": "Move _PARAM1_ according to gamepad inputs", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "1", + "\"RIGHT\"", + "\"RIGHT\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateRightKey" + }, + "parameters": [ + "Mindy", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "1", + "\"LEFT\"", + "\"LEFT\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateLeftKey" + }, + "parameters": [ + "Mindy", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateStick" + }, + "parameters": [ + "Mindy", + "TopDownMovement", + "Gamepads::StickRotationValue(1, \"Left\")", + "Gamepads::StickForce(1, \"Left\")" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "1", + "\"DOWN\"", + "\"DOWN\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateDownKey" + }, + "parameters": [ + "Mindy", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "1", + "\"UP\"", + "\"UP\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateUpKey" + }, + "parameters": [ + "Mindy", + "TopDownMovement" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Mindy", + "supplementaryInformation": "Sprite", + "type": "objectList" + }, + { + "description": "Top-down movement", + "name": "TopDownMovement", + "supplementaryInformation": "TopDownMovementBehavior::TopDownMovementBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Choose the animation according to the object movement.", + "fullName": "Choose animation according to movement", + "functionType": "Action", + "name": "ChooseAnimationDirection", + "sentence": "Choose the animation of _PARAM1_ according to the movement", + "events": [ + { + "type": "BuiltinCommonInstructions::Comment", + "color": { + "b": 109, + "g": 230, + "r": 255, + "textB": 0, + "textG": 0, + "textR": 0 + }, + "comment": "When moving, set the animation according to the direction of the movement (we can automatically compute it. For example, if the angle is 180 degrees (i.e: left), the direction will be 1 + 180/45 = 5). This allow us to make only one action, rather than adding conditions for every direction.\nThis supposes that animation #0 is the Idle animation, and the others animation are in the right order (animation #1 is the right, and then ordered in a clock wise manner).", + "comment2": "" + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "TopDownMovementBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "TopDownMovement" + ] + } + ], + "actions": [ + { + "type": { + "value": "ChangeAnimation" + }, + "parameters": [ + "Mindy", + "=", + "1 + mod(round((Mindy.TopDownMovement::Angle())/45+1), 8)" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior" + ] + } + ], + "actions": [ + { + "type": { + "value": "ChangeAnimation" + }, + "parameters": [ + "Mindy", + "=", + "1 + mod(round((Mindy.NavMeshPathfindingBehavior::MovementAngle())/45), 8)" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Mindy", + "supplementaryInformation": "Sprite", + "type": "objectList" + }, + { + "description": "Pathinding", + "name": "NavMeshPathfindingBehavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + }, + { + "description": "Top-down movement", + "name": "TopDownMovement", + "supplementaryInformation": "TopDownMovementBehavior::TopDownMovementBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + }, + { + "description": "Check if the object is moving.", + "fullName": "Is moving", + "functionType": "Condition", + "name": "IsMoving", + "sentence": "_PARAM1_ is moving", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "BuiltinCommonInstructions::Or" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "TopDownMovementBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "TopDownMovement" + ] + }, + { + "type": { + "value": "BuiltinCommonInstructions::And" + }, + "parameters": [], + "subInstructions": [ + { + "type": { + "value": "BehaviorActivated" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior" + ] + }, + { + "type": { + "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior", + "" + ] + } + ] + } + ] + } + ], + "actions": [ + { + "type": { + "value": "SetReturnBoolean" + }, + "parameters": [ + "True" + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Mindy", + "supplementaryInformation": "Sprite", + "type": "objectList" + }, + { + "description": "Pathinding", + "name": "NavMeshPathfindingBehavior", + "supplementaryInformation": "NavMeshPathfinding::NavMeshPathfindingBehavior", + "type": "behavior" + }, + { + "description": "Top-down movement", + "name": "TopDownMovement", + "supplementaryInformation": "TopDownMovementBehavior::TopDownMovementBehavior", + "type": "behavior" + } + ], + "objectGroups": [] + } + ], + "eventsBasedBehaviors": [], + "eventsBasedObjects": [] + }, { "author": "", "category": "Camera", From 2f933deaf23730504bcd8e3369b34135774ecbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sat, 10 Dec 2022 21:29:54 +0100 Subject: [PATCH 08/11] Add a behavior for gamepad control. --- examples/isometric-game/isometric-game.json | 1587 ++++++++++++++++--- 1 file changed, 1368 insertions(+), 219 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index 04c8e31a8..7c51c898e 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -8325,6 +8325,16 @@ "OldY": 9000, "IsCalledManually": false }, + { + "name": "TopDownGamepadMapper", + "type": "Gamepads::TopDownGamepadMapper", + "TopDownMovement": "TopDownMovement", + "GamepadIdentifier": 1, + "UseArrows": true, + "UseLeftStick": true, + "UseRightStick": false, + "StickMode": "360°" + }, { "name": "TopDownMovement", "type": "TopDownMovementBehavior::TopDownMovementBehavior", @@ -25876,35 +25886,6 @@ "source": "", "type": "BuiltinCommonInstructions::Group", "events": [ - { - "colorB": 228, - "colorG": 176, - "colorR": 74, - "creationTime": 0, - "name": "Gamepad Control", - "source": "", - "type": "BuiltinCommonInstructions::Group", - "events": [ - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "CharacterMovement::MoveWithGamepad" - }, - "parameters": [ - "", - "Mindy", - "TopDownMovement", - "" - ] - } - ] - } - ], - "parameters": [] - }, { "colorB": 228, "colorG": 176, @@ -26391,6 +26372,10 @@ "name": "SmoothCamera", "type": "SmoothCamera::SmoothCamera" }, + { + "name": "TopDownGamepadMapper", + "type": "Gamepads::TopDownGamepadMapper" + }, { "name": "TopDownMovement", "type": "TopDownMovementBehavior::TopDownMovementBehavior" @@ -26427,155 +26412,6 @@ "authorIds": [], "dependencies": [], "eventsFunctions": [ - { - "description": "Move the object according to gamepad inputs.", - "fullName": "Move with gamepad", - "functionType": "Action", - "name": "MoveWithGamepad", - "sentence": "Move _PARAM1_ according to gamepad inputs", - "events": [ - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "Gamepads::C_Button_pressed" - }, - "parameters": [ - "", - "1", - "\"RIGHT\"", - "\"RIGHT\"" - ] - } - ], - "actions": [ - { - "type": { - "value": "TopDownMovementBehavior::SimulateRightKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "Gamepads::C_Button_pressed" - }, - "parameters": [ - "", - "1", - "\"LEFT\"", - "\"LEFT\"" - ] - } - ], - "actions": [ - { - "type": { - "value": "TopDownMovementBehavior::SimulateLeftKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [], - "actions": [ - { - "type": { - "value": "TopDownMovementBehavior::SimulateStick" - }, - "parameters": [ - "Mindy", - "TopDownMovement", - "Gamepads::StickRotationValue(1, \"Left\")", - "Gamepads::StickForce(1, \"Left\")" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "Gamepads::C_Button_pressed" - }, - "parameters": [ - "", - "1", - "\"DOWN\"", - "\"DOWN\"" - ] - } - ], - "actions": [ - { - "type": { - "value": "TopDownMovementBehavior::SimulateDownKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - } - ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "value": "Gamepads::C_Button_pressed" - }, - "parameters": [ - "", - "1", - "\"UP\"", - "\"UP\"" - ] - } - ], - "actions": [ - { - "type": { - "value": "TopDownMovementBehavior::SimulateUpKey" - }, - "parameters": [ - "Mindy", - "TopDownMovement" - ] - } - ] - } - ], - "parameters": [ - { - "description": "Object", - "name": "Mindy", - "supplementaryInformation": "Sprite", - "type": "objectList" - }, - { - "description": "Top-down movement", - "name": "TopDownMovement", - "supplementaryInformation": "TopDownMovementBehavior::TopDownMovementBehavior", - "type": "behavior" - } - ], - "objectGroups": [] - }, { "description": "Choose the animation according to the object movement.", "fullName": "Choose animation according to movement", @@ -39013,7 +38849,7 @@ }, { "author": "Bouh", - "category": "Input", + "category": "", "extensionNamespace": "", "fullName": "Gamepads (controllers)", "helpPath": "/all-features/gamepad", @@ -39177,9 +39013,9 @@ "type": "expression" }, { - "description": "Stick: \"Left\" or \"Right\"", + "description": "Stick: \"LEFT\" or \"RIGHT\"", "name": "stick", - "supplementaryInformation": "[\"Left\",\"Right\"]", + "supplementaryInformation": "[\"LEFT\",\"RIGHT\"]", "type": "stringWithSelector" } ], @@ -39245,9 +39081,9 @@ "type": "expression" }, { - "description": "Stick: \"Left\" or \"Right\"", + "description": "Stick: \"LEFT\" or \"RIGHT\"", "name": "stick", - "supplementaryInformation": "[\"Left\",\"Right\"]", + "supplementaryInformation": "[\"LEFT\",\"RIGHT\"]", "type": "stringWithSelector" } ], @@ -39388,15 +39224,15 @@ "type": "expression" }, { - "description": "Stick: \"Left\" or \"Right\"", + "description": "Stick: \"LEFT\" or \"RIGHT\"", "name": "stick", - "supplementaryInformation": "[\"Left\",\"Right\"]", + "supplementaryInformation": "[\"LEFT\",\"RIGHT\"]", "type": "stringWithSelector" }, { "description": "Direction", "name": "direction", - "supplementaryInformation": "[\"Up\",\"Down\",\"Left\",\"Right\",\"Horizontal\",\"Vertical\"]", + "supplementaryInformation": "[\"UP\",\"DOWN\",\"LEFT\",\"RIGHT\",\"HORIZONTAL\",\"VERTICAL\"]", "type": "stringWithSelector" } ], @@ -39557,7 +39393,7 @@ { "description": "Name of the button", "name": "button", - "supplementaryInformation": "[\"A\",\"Cross\",\"B\",\"Circle\",\"X\",\"Square\",\"Y\",\"Triangle\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"Up\",\"Down\",\"Left\",\"Right\",\"Back\",\"Share\",\"Start\",\"Options\",\"Click_Stick_Left\",\"Click_Stick_Right\",\"PS_Button\",\"Click_Touchpad\"]", + "supplementaryInformation": "[\"A\",\"CROSS\",\"B\",\"CIRCLE\",\"X\",\"SQUARE\",\"Y\",\"TRIANGLE\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"UP\",\"DOWN\",\"LEFT\",\"RIGHT\",\"BACK\",\"SHARE\",\"START\",\"OPTIONS\",\"CLICK_STICK_LEFT\",\"CLICK_STICK_RIGHT\",\"PS_BUTTON\",\"CLICK_TOUCHPAD\"]", "type": "stringWithSelector" } ], @@ -39725,7 +39561,7 @@ { "description": "Controller type", "name": "controller_type", - "supplementaryInformation": "[\"Xbox\",\"PS4\"]", + "supplementaryInformation": "[\"XBOX\",\"PS4\"]", "type": "stringWithSelector" } ], @@ -39878,7 +39714,7 @@ { "description": "Name of the button", "name": "button", - "supplementaryInformation": "[\"A\",\"Cross\",\"B\",\"Circle\",\"X\",\"Square\",\"Y\",\"Triangle\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"Up\",\"Down\",\"Left\",\"Right\",\"BAck\",\"Share\",\"Start\",\"Options\",\"Click_Stick_Left\",\"Click_Stick_Right\",\"PS_Button\",\"Click_Touchpad\"]", + "supplementaryInformation": "[\"A\",\"CROSS\",\"B\",\"CIRCLE\",\"X\",\"SQUARE\",\"Y\",\"TRIANGLE\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"UP\",\"DOWN\",\"LEFT\",\"RIGHT\",\"BACK\",\"SHARE\",\"START\",\"OPTIONS\",\"CLICK_STICK_LEFT\",\"CLICK_STICK_RIGHT\",\"PS_BUTTON\",\"CLICK_TOUCHPAD\"]", "type": "stringWithSelector" } ], @@ -39995,7 +39831,7 @@ " console.error('Parameter stick in condition: \"Gamepad stick pushed (axis)\", is not valid, must be LEFT or RIGHT');\r", " return;\r", "}\r", - "if (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\" && direction != \"ANY\") {\r", + "if (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\") {\r", " console.error('Parameter deadzone in condition: \"Gamepad stick pushed (axis)\", is not valid, must be UP, DOWN, LEFT or RIGHT');\r", " return;\r", "}\r", @@ -40043,16 +39879,6 @@ " }\r", " break;\r", "\r", - " case 'ANY':\r", - " if ( getNormalizedAxisValue(gamepad.axes[0], playerId) < 0\r", - " || getNormalizedAxisValue(gamepad.axes[0], playerId) > 0\r", - " || getNormalizedAxisValue(gamepad.axes[1], playerId) < 0 \r", - " || getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r", - " eventsFunctionContext.returnValue = true;\r", - " return;\r", - " }\r", - " break;\r", - "\r", " default:\r", " console.error('The value Direction on stick Left on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r", " eventsFunctionContext.returnValue = false;\r", @@ -40090,16 +39916,6 @@ " }\r", " break;\r", "\r", - " case 'ANY':\r", - " if ( getNormalizedAxisValue(gamepad.axes[2], playerId) < 0\r", - " || getNormalizedAxisValue(gamepad.axes[2], playerId) > 0\r", - " || getNormalizedAxisValue(gamepad.axes[3], playerId) < 0 \r", - " || getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r", - " eventsFunctionContext.returnValue = true;\r", - " return;\r", - " }\r", - " break;\r", - "\r", " default:\r", " console.error('The value Direction on stick Right on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r", " eventsFunctionContext.returnValue = false;\r", @@ -40128,15 +39944,15 @@ "type": "expression" }, { - "description": "Stick: \"Left\" or \"Right\"", + "description": "Stick: \"LEFT\" or \"RIGHT\"", "name": "stick", - "supplementaryInformation": "[\"Left\",\"Right\"]", + "supplementaryInformation": "[\"LEFT\",\"RIGHT\"]", "type": "stringWithSelector" }, { "description": "Direction", "name": "direction", - "supplementaryInformation": "[\"Up\",\"Down\",\"Left\",\"Right\",\"Any\"]", + "supplementaryInformation": "[\"UP\",\"DOWN\",\"LEFT\",\"RIGHT\"]", "type": "stringWithSelector" } ], @@ -40271,7 +40087,7 @@ "type": "expression" }, { - "description": "Type: \"Xbox\", \"PS4\", \"Steam\" or \"PS3\" (among other)", + "description": "Type: \"Xbox\", \"PS4\", \"STEAM\" or \"PS3\" (among other)", "name": "controller_type", "type": "string" } @@ -40461,10 +40277,8 @@ "\r", "gdjs._extensionController.isXbox = function (gamepad) {\r", " return (gamepad ? (\r", - " gamepad.id.toUpperCase().indexOf(\"XBOX\") !== -1\r", - " // \"XINPUT\" cannot be used to check if it is a xbox controller is just a generic\r", - " // name reported in Firefox corresponding to the driver being used by the controller\r", - " // https://gamefaqs.gamespot.com/boards/916373-pc/73341312?page=1\r", + " gamepad.id.toUpperCase().indexOf(\"XBOX\") !== -1 ||\r", + " gamepad.id.toUpperCase().indexOf(\"XINPUT\") !== -1\r", " ) : false);\r", "}\r", "\r", @@ -40643,7 +40457,1342 @@ "objectGroups": [] } ], - "eventsBasedBehaviors": [], + "eventsBasedBehaviors": [ + { + "description": "Control a platformer character with a gamepad.", + "fullName": "Platformer gamepad mapper", + "name": "PlatformerGamepadMapper", + "objectType": "", + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "doStepPreEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Controller_X_is_connected" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyUseArrows" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateLeftKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateRightKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"UP\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateUpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + }, + { + "type": { + "value": "PlatformBehavior::SimulateLadderKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"DOWN\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateDownKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyUseLeftStick" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Left\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateLeftKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Right\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateRightKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Up\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateUpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + }, + { + "type": { + "value": "PlatformBehavior::SimulateLadderKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Down\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateDownKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyUseRightStick" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Left\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateLeftKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Right\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateRightKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Up\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateUpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + }, + { + "type": { + "value": "PlatformBehavior::SimulateLadderKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Down\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateDownKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyJumpButton" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"A or Cross\"" + ] + }, + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"A\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateJumpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyJumpButton" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"B or Circle\"" + ] + }, + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"B\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateJumpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyJumpButton" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"X or Square\"" + ] + }, + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"X\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateJumpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyJumpButton" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Y or Triangle\"" + ] + }, + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"Y\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateJumpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "Gamepads::PlatformerGamepadMapper", + "type": "behavior" + } + ], + "objectGroups": [] + } + ], + "propertyDescriptors": [ + { + "value": "", + "type": "Behavior", + "label": "Platformer character behavior", + "description": "", + "group": "", + "extraInformation": [ + "PlatformBehavior::PlatformerObjectBehavior" + ], + "hidden": false, + "name": "PlatformerCharacter" + }, + { + "value": "1", + "type": "Number", + "label": "Gamepad identifier (1, 2, 3 or 4)", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "GamepadIdentifier" + }, + { + "value": "true", + "type": "Boolean", + "label": "Use directional pad", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "UseArrows" + }, + { + "value": "true", + "type": "Boolean", + "label": "Use left stick", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "UseLeftStick" + }, + { + "value": "", + "type": "Boolean", + "label": "Use right stick", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "UseRightStick" + }, + { + "value": "A or Cross", + "type": "Choice", + "label": "Jump button", + "description": "", + "group": "", + "extraInformation": [ + "A or Cross", + "B or Circle", + "X or Square", + "Y or Triangle" + ], + "hidden": false, + "name": "JumpButton" + } + ], + "sharedPropertyDescriptors": [] + }, + { + "description": "Control a top-down character with a gamepad.", + "fullName": "Top-down gamepad mapper", + "name": "TopDownGamepadMapper", + "objectType": "", + "eventsFunctions": [ + { + "fullName": "", + "functionType": "Action", + "name": "doStepPreEvents", + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Controller_X_is_connected" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyUseArrows" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateLeftKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateRightKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"UP\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateUpKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"DOWN\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateDownKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyUseLeftStick" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::TopDownGamepadMapper::PropertyStickMode" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Analog\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateStick" + }, + "parameters": [ + "Object", + "TopDownMovement", + "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"LEFT\")", + "Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"LEFT\")" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::TopDownGamepadMapper::PropertyStickMode" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"360°\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateStick" + }, + "parameters": [ + "Object", + "TopDownMovement", + "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"LEFT\")", + "sign(Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"LEFT\"))" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::TopDownGamepadMapper::PropertyStickMode" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"8 Directions\"" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Left\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateLeftKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Right\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateRightKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Up\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateUpKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LEFT\"", + "\"Down\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateDownKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + } + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyUseRightStick" + }, + "parameters": [ + "Object", + "Behavior" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::TopDownGamepadMapper::PropertyStickMode" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"Analog\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateStick" + }, + "parameters": [ + "Object", + "TopDownMovement", + "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"RIGHT\")", + "Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"RIGHT\")" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::TopDownGamepadMapper::PropertyStickMode" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"360°\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateStick" + }, + "parameters": [ + "Object", + "TopDownMovement", + "sign(Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"RIGHT\"))", + "1" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::TopDownGamepadMapper::PropertyStickMode" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"8 Directions\"" + ] + } + ], + "actions": [], + "events": [ + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Left\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateLeftKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Right\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateRightKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Up\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateUpKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::C_Axis_pushed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RIGHT\"", + "\"Down\"", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "TopDownMovementBehavior::SimulateDownKey" + }, + "parameters": [ + "Object", + "TopDownMovement" + ] + } + ] + } + ] + } + ] + } + ] + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "object" + }, + { + "description": "Behavior", + "name": "Behavior", + "supplementaryInformation": "Gamepads::TopDownGamepadMapper", + "type": "behavior" + } + ], + "objectGroups": [] + } + ], + "propertyDescriptors": [ + { + "value": "", + "type": "Behavior", + "label": "Top-down movement behavior", + "description": "", + "group": "", + "extraInformation": [ + "TopDownMovementBehavior::TopDownMovementBehavior" + ], + "hidden": false, + "name": "TopDownMovement" + }, + { + "value": "1", + "type": "Number", + "label": "Gamepad identifier (1, 2, 3 or 4)", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "GamepadIdentifier" + }, + { + "value": "true", + "type": "Boolean", + "label": "Use directional pad", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "UseArrows" + }, + { + "value": "true", + "type": "Boolean", + "label": "Use left stick", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "UseLeftStick" + }, + { + "value": "", + "type": "Boolean", + "label": "Use right stick", + "description": "", + "group": "", + "extraInformation": [], + "hidden": false, + "name": "UseRightStick" + }, + { + "value": "Analog", + "type": "Choice", + "label": "Stick mode", + "description": "", + "group": "", + "extraInformation": [ + "Analog", + "360°", + "8 Directions" + ], + "hidden": false, + "name": "StickMode" + } + ], + "sharedPropertyDescriptors": [] + } + ], "eventsBasedObjects": [] } ], From a56d1e6b45cc70e315e053f0fe39c848e4591a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sat, 10 Dec 2022 22:06:11 +0100 Subject: [PATCH 09/11] Review changes --- examples/isometric-game/isometric-game.json | 1870 ++++++++++++++++++- 1 file changed, 1828 insertions(+), 42 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index 7c51c898e..4ef72d890 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -8371,6 +8371,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8413,6 +8418,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8455,6 +8465,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8497,6 +8512,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8539,6 +8559,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8581,6 +8606,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8623,6 +8653,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8665,6 +8700,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8707,6 +8747,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8749,6 +8794,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8791,6 +8841,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8833,6 +8888,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8875,6 +8935,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8917,6 +8982,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -8959,6 +9029,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9001,6 +9076,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9043,6 +9123,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9085,6 +9170,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9127,6 +9217,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9169,6 +9264,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9211,6 +9311,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9253,6 +9358,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9295,6 +9405,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9337,6 +9452,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9379,6 +9499,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9421,6 +9546,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9463,6 +9593,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9505,6 +9640,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9547,6 +9687,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9589,6 +9734,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9631,6 +9781,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9673,6 +9828,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9715,6 +9875,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9757,6 +9922,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9799,6 +9969,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9841,6 +10016,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9883,6 +10063,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9925,6 +10110,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -9967,6 +10157,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10009,6 +10204,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10051,6 +10251,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10093,6 +10298,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10135,6 +10345,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10177,6 +10392,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10219,6 +10439,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10261,6 +10486,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10303,6 +10533,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10345,6 +10580,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10387,6 +10627,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10429,6 +10674,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10471,6 +10721,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10513,6 +10768,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10555,6 +10815,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10597,6 +10862,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10639,6 +10909,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10681,6 +10956,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10723,6 +11003,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10765,6 +11050,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10807,6 +11097,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10849,6 +11144,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10903,6 +11203,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10945,6 +11250,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -10987,6 +11297,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11029,6 +11344,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11071,6 +11391,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11113,6 +11438,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11155,6 +11485,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11197,6 +11532,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11239,6 +11579,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11281,6 +11626,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11323,6 +11673,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11365,6 +11720,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11407,6 +11767,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11449,6 +11814,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11491,6 +11861,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11533,6 +11908,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11575,6 +11955,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11617,6 +12002,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11659,6 +12049,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11701,6 +12096,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11743,6 +12143,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11785,6 +12190,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11839,6 +12249,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11881,6 +12296,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11923,6 +12343,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -11965,7 +12390,12 @@ "name": "Foot", "x": 128, "y": 220 - } + }, + { + "name": "Head", + "x": 128, + "y": 64 + } ], "originPoint": { "name": "origine", @@ -12007,6 +12437,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12049,6 +12484,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12091,6 +12531,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12133,6 +12578,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12175,6 +12625,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12217,6 +12672,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12259,6 +12719,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12301,6 +12766,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12343,6 +12813,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12385,6 +12860,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12427,6 +12907,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12469,6 +12954,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12511,6 +13001,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12553,6 +13048,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12595,6 +13095,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12637,6 +13142,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12679,6 +13189,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12721,6 +13236,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12775,6 +13295,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12817,6 +13342,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12859,6 +13389,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12901,6 +13436,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12943,6 +13483,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -12985,6 +13530,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13027,6 +13577,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13069,6 +13624,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13111,6 +13671,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13153,6 +13718,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13195,6 +13765,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13237,6 +13812,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13279,6 +13859,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13321,6 +13906,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13363,6 +13953,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13405,6 +14000,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13447,6 +14047,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13489,6 +14094,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13531,6 +14141,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13573,6 +14188,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13615,6 +14235,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13657,6 +14282,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13711,6 +14341,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13753,6 +14388,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13795,6 +14435,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13837,6 +14482,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13879,6 +14529,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13921,6 +14576,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -13963,6 +14623,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14005,6 +14670,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14047,6 +14717,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14089,6 +14764,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14131,6 +14811,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14173,6 +14858,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14215,6 +14905,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14257,6 +14952,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14299,6 +14999,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14341,6 +15046,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14383,6 +15093,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14425,6 +15140,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14467,6 +15187,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14509,6 +15234,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14551,6 +15281,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14593,6 +15328,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14647,6 +15387,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14689,6 +15434,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14731,6 +15481,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14773,6 +15528,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14815,6 +15575,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14857,6 +15622,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14899,6 +15669,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14941,6 +15716,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -14983,6 +15763,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15025,6 +15810,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15067,6 +15857,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15109,6 +15904,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15151,6 +15951,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15193,6 +15998,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15235,6 +16045,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15277,6 +16092,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15319,6 +16139,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15361,6 +16186,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15403,6 +16233,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15445,6 +16280,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15487,6 +16327,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15529,6 +16374,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15583,6 +16433,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15625,6 +16480,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15667,6 +16527,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15709,6 +16574,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15751,6 +16621,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15793,6 +16668,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15835,6 +16715,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15877,6 +16762,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15919,6 +16809,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -15961,6 +16856,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16003,6 +16903,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16045,6 +16950,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16087,6 +16997,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16129,6 +17044,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16171,6 +17091,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16213,6 +17138,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16255,6 +17185,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16297,6 +17232,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16339,6 +17279,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16381,6 +17326,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16423,6 +17373,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16465,6 +17420,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16519,6 +17479,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16561,6 +17526,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16603,6 +17573,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16645,6 +17620,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16687,6 +17667,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16729,6 +17714,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16771,6 +17761,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16813,6 +17808,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16855,6 +17855,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16897,6 +17902,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16939,6 +17949,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -16981,6 +17996,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17023,6 +18043,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17065,6 +18090,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17107,6 +18137,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17149,6 +18184,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17191,6 +18231,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17233,6 +18278,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17275,6 +18325,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17317,6 +18372,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17359,6 +18419,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17401,6 +18466,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17455,6 +18525,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17497,6 +18572,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17539,6 +18619,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17581,6 +18666,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17623,6 +18713,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17665,6 +18760,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17707,6 +18807,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17749,6 +18854,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17791,6 +18901,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17833,6 +18948,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17875,6 +18995,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17917,6 +19042,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -17959,6 +19089,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18001,6 +19136,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18043,6 +19183,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18085,6 +19230,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18127,6 +19277,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18169,6 +19324,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18211,6 +19371,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18253,6 +19418,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18295,6 +19465,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18337,6 +19512,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18391,6 +19571,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18433,6 +19618,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18475,6 +19665,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18517,6 +19712,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18559,6 +19759,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18601,6 +19806,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18643,6 +19853,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18685,6 +19900,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18727,6 +19947,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18769,6 +19994,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18811,6 +20041,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18853,6 +20088,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18895,6 +20135,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18937,6 +20182,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -18979,6 +20229,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19021,6 +20276,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19063,6 +20323,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19105,6 +20370,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19147,6 +20417,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19189,6 +20464,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19231,6 +20511,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19273,6 +20558,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19315,6 +20605,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19357,6 +20652,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19399,6 +20699,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19441,6 +20746,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19483,6 +20793,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19525,6 +20840,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19567,6 +20887,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19609,6 +20934,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19651,6 +20981,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19693,6 +21028,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19735,6 +21075,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19777,6 +21122,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19819,6 +21169,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19861,6 +21216,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19903,6 +21263,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19945,6 +21310,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -19987,6 +21357,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20029,6 +21404,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20071,6 +21451,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20113,6 +21498,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20155,6 +21545,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20197,6 +21592,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20239,6 +21639,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20281,6 +21686,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20323,6 +21733,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20365,6 +21780,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20407,6 +21827,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20449,6 +21874,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20491,6 +21921,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20533,6 +21968,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20575,6 +22015,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20617,6 +22062,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20659,6 +22109,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20701,6 +22156,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20743,6 +22203,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20785,6 +22250,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20827,6 +22297,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20868,7 +22343,12 @@ { "name": "Foot", "x": 128, - "y": 220 + "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20911,6 +22391,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20953,6 +22438,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -20995,6 +22485,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21037,6 +22532,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21079,6 +22579,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21121,6 +22626,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21163,6 +22673,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21205,6 +22720,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21247,6 +22767,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21289,6 +22814,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21331,6 +22861,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21373,6 +22908,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21415,6 +22955,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21457,6 +23002,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21499,6 +23049,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21541,6 +23096,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21583,6 +23143,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21625,6 +23190,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21667,6 +23237,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21709,6 +23284,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21751,6 +23331,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21793,6 +23378,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21835,6 +23425,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21877,6 +23472,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21919,6 +23519,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -21961,6 +23566,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22003,6 +23613,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22045,6 +23660,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22087,6 +23707,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22129,6 +23754,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22171,6 +23801,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22213,6 +23848,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22255,6 +23895,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22297,6 +23942,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22339,6 +23989,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22381,6 +24036,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22423,6 +24083,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22465,6 +24130,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22507,6 +24177,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22549,6 +24224,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22591,6 +24271,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22633,6 +24318,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22675,6 +24365,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22717,6 +24412,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22759,6 +24459,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22801,6 +24506,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22843,6 +24553,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22885,6 +24600,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22927,6 +24647,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -22969,6 +24694,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -23011,6 +24741,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -23053,6 +24788,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -23095,6 +24835,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -23137,6 +24882,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -23179,6 +24929,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -23221,6 +24976,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -23263,6 +25023,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -23305,6 +25070,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -23347,6 +25117,11 @@ "name": "Foot", "x": 128, "y": 220 + }, + { + "name": "Head", + "x": 128, + "y": 64 } ], "originPoint": { @@ -26001,36 +27776,6 @@ ] } ] - }, - { - "type": "BuiltinCommonInstructions::Standard", - "conditions": [ - { - "type": { - "inverted": true, - "value": "CharacterMovement::IsMoving" - }, - "parameters": [ - "Mindy", - "Mindy", - "NavMeshPathfindingBehavior", - "TopDownMovement", - "" - ] - } - ], - "actions": [ - { - "type": { - "value": "ChangeAnimation" - }, - "parameters": [ - "Mindy", - "=", - "0" - ] - } - ] } ], "parameters": [] @@ -26160,8 +27905,8 @@ }, "parameters": [ "Bat", - "Mindy.PointX(\"origin\") - 40", - "Mindy.PointY(\"origin\") - 60", + "Mindy.PointX(\"Head\")", + "Mindy.PointY(\"Head\")", "150", "0" ] @@ -26231,6 +27976,15 @@ { "type": "BuiltinCommonInstructions::Standard", "conditions": [ + { + "type": { + "value": "SoundPlaying" + }, + "parameters": [ + "", + "1" + ] + }, { "type": { "value": "BuiltinCommonInstructions::Or" @@ -26387,14 +28141,7 @@ ] } ], - "externalEvents": [ - { - "associatedLayout": "Level 1", - "lastChangeTimeStamp": 0, - "name": "Base_Event", - "events": [] - } - ], + "externalEvents": [], "eventsFunctionsExtensions": [ { "author": "", @@ -26461,6 +28208,15 @@ { "type": "BuiltinCommonInstructions::Standard", "conditions": [ + { + "type": { + "value": "BehaviorActivated" + }, + "parameters": [ + "Mindy", + "NavMeshPathfindingBehavior" + ] + }, { "type": { "value": "NavMeshPathfinding::NavMeshPathfindingBehavior::IsMoving" @@ -26483,6 +28239,36 @@ ] } ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "inverted": true, + "value": "CharacterMovement::IsMoving" + }, + "parameters": [ + "Mindy", + "Mindy", + "NavMeshPathfindingBehavior", + "TopDownMovement", + "" + ] + } + ], + "actions": [ + { + "type": { + "value": "ChangeAnimation" + }, + "parameters": [ + "Mindy", + "=", + "0" + ] + } + ] } ], "parameters": [ From 6ca679b5aed1f755c15b097531ddb2ed7c3888ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 11 Dec 2022 19:15:23 +0100 Subject: [PATCH 10/11] Upgrade Gamepad extension after its rebase. --- examples/isometric-game/isometric-game.json | 294 ++++++++++++++++---- 1 file changed, 236 insertions(+), 58 deletions(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index 4ef72d890..279923ad5 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -40635,7 +40635,7 @@ }, { "author": "Bouh", - "category": "", + "category": "Input", "extensionNamespace": "", "fullName": "Gamepads (controllers)", "helpPath": "/all-features/gamepad", @@ -40643,7 +40643,7 @@ "name": "Gamepads", "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/gamepad-variant-outline.svg", "shortDescription": "Add support for gamepads (or other controllers) to your game, giving access to information such as button presses, axis positions, trigger pressure, etc...", - "version": "0.3.0", + "version": "0.4.0", "description": [ "Add support for gamepads (or other controllers) to your game, giving access to information such as button presses, axis positions, axis force, trigger pressure, deadzone for each gamepad, etc...", "", @@ -40799,9 +40799,9 @@ "type": "expression" }, { - "description": "Stick: \"LEFT\" or \"RIGHT\"", + "description": "Stick: \"Left\" or \"Right\"", "name": "stick", - "supplementaryInformation": "[\"LEFT\",\"RIGHT\"]", + "supplementaryInformation": "[\"Left\",\"Right\"]", "type": "stringWithSelector" } ], @@ -40867,9 +40867,9 @@ "type": "expression" }, { - "description": "Stick: \"LEFT\" or \"RIGHT\"", + "description": "Stick: \"Left\" or \"Right\"", "name": "stick", - "supplementaryInformation": "[\"LEFT\",\"RIGHT\"]", + "supplementaryInformation": "[\"Left\",\"Right\"]", "type": "stringWithSelector" } ], @@ -41010,15 +41010,15 @@ "type": "expression" }, { - "description": "Stick: \"LEFT\" or \"RIGHT\"", + "description": "Stick: \"Left\" or \"Right\"", "name": "stick", - "supplementaryInformation": "[\"LEFT\",\"RIGHT\"]", + "supplementaryInformation": "[\"Left\",\"Right\"]", "type": "stringWithSelector" }, { "description": "Direction", "name": "direction", - "supplementaryInformation": "[\"UP\",\"DOWN\",\"LEFT\",\"RIGHT\",\"HORIZONTAL\",\"VERTICAL\"]", + "supplementaryInformation": "[\"Up\",\"Down\",\"Left\",\"Right\",\"Horizontal\",\"Vertical\"]", "type": "stringWithSelector" } ], @@ -41179,7 +41179,7 @@ { "description": "Name of the button", "name": "button", - "supplementaryInformation": "[\"A\",\"CROSS\",\"B\",\"CIRCLE\",\"X\",\"SQUARE\",\"Y\",\"TRIANGLE\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"UP\",\"DOWN\",\"LEFT\",\"RIGHT\",\"BACK\",\"SHARE\",\"START\",\"OPTIONS\",\"CLICK_STICK_LEFT\",\"CLICK_STICK_RIGHT\",\"PS_BUTTON\",\"CLICK_TOUCHPAD\"]", + "supplementaryInformation": "[\"A\",\"Cross\",\"B\",\"Circle\",\"X\",\"Square\",\"Y\",\"Triangle\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"Up\",\"Down\",\"Left\",\"Right\",\"Back\",\"Share\",\"Start\",\"Options\",\"Click_Stick_Left\",\"Click_Stick_Right\",\"PS_Button\",\"Click_Touchpad\"]", "type": "stringWithSelector" } ], @@ -41347,7 +41347,7 @@ { "description": "Controller type", "name": "controller_type", - "supplementaryInformation": "[\"XBOX\",\"PS4\"]", + "supplementaryInformation": "[\"Xbox\",\"PS4\"]", "type": "stringWithSelector" } ], @@ -41500,7 +41500,7 @@ { "description": "Name of the button", "name": "button", - "supplementaryInformation": "[\"A\",\"CROSS\",\"B\",\"CIRCLE\",\"X\",\"SQUARE\",\"Y\",\"TRIANGLE\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"UP\",\"DOWN\",\"LEFT\",\"RIGHT\",\"BACK\",\"SHARE\",\"START\",\"OPTIONS\",\"CLICK_STICK_LEFT\",\"CLICK_STICK_RIGHT\",\"PS_BUTTON\",\"CLICK_TOUCHPAD\"]", + "supplementaryInformation": "[\"A\",\"Cross\",\"B\",\"Circle\",\"X\",\"Square\",\"Y\",\"Triangle\",\"LB\",\"L1\",\"RB\",\"R1\",\"LT\",\"L2\",\"RT\",\"R2\",\"Up\",\"Down\",\"Left\",\"Right\",\"BAck\",\"Share\",\"Start\",\"Options\",\"Click_Stick_Left\",\"Click_Stick_Right\",\"PS_Button\",\"Click_Touchpad\"]", "type": "stringWithSelector" } ], @@ -41617,7 +41617,7 @@ " console.error('Parameter stick in condition: \"Gamepad stick pushed (axis)\", is not valid, must be LEFT or RIGHT');\r", " return;\r", "}\r", - "if (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\") {\r", + "if (direction != \"UP\" && direction != \"DOWN\" && direction != \"LEFT\" && direction != \"RIGHT\" && direction != \"ANY\") {\r", " console.error('Parameter deadzone in condition: \"Gamepad stick pushed (axis)\", is not valid, must be UP, DOWN, LEFT or RIGHT');\r", " return;\r", "}\r", @@ -41665,6 +41665,16 @@ " }\r", " break;\r", "\r", + " case 'ANY':\r", + " if ( getNormalizedAxisValue(gamepad.axes[0], playerId) < 0\r", + " || getNormalizedAxisValue(gamepad.axes[0], playerId) > 0\r", + " || getNormalizedAxisValue(gamepad.axes[1], playerId) < 0 \r", + " || getNormalizedAxisValue(gamepad.axes[1], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", " default:\r", " console.error('The value Direction on stick Left on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r", " eventsFunctionContext.returnValue = false;\r", @@ -41702,6 +41712,16 @@ " }\r", " break;\r", "\r", + " case 'ANY':\r", + " if ( getNormalizedAxisValue(gamepad.axes[2], playerId) < 0\r", + " || getNormalizedAxisValue(gamepad.axes[2], playerId) > 0\r", + " || getNormalizedAxisValue(gamepad.axes[3], playerId) < 0 \r", + " || getNormalizedAxisValue(gamepad.axes[3], playerId) > 0) {\r", + " eventsFunctionContext.returnValue = true;\r", + " return;\r", + " }\r", + " break;\r", + "\r", " default:\r", " console.error('The value Direction on stick Right on the condition: \"Gamepad stick pushed (axis)\" is not valid.');\r", " eventsFunctionContext.returnValue = false;\r", @@ -41730,15 +41750,15 @@ "type": "expression" }, { - "description": "Stick: \"LEFT\" or \"RIGHT\"", + "description": "Stick: \"Left\" or \"Right\"", "name": "stick", - "supplementaryInformation": "[\"LEFT\",\"RIGHT\"]", + "supplementaryInformation": "[\"Left\",\"Right\"]", "type": "stringWithSelector" }, { "description": "Direction", "name": "direction", - "supplementaryInformation": "[\"UP\",\"DOWN\",\"LEFT\",\"RIGHT\"]", + "supplementaryInformation": "[\"Up\",\"Down\",\"Left\",\"Right\",\"Any\"]", "type": "stringWithSelector" } ], @@ -41873,7 +41893,7 @@ "type": "expression" }, { - "description": "Type: \"Xbox\", \"PS4\", \"STEAM\" or \"PS3\" (among other)", + "description": "Type: \"Xbox\", \"PS4\", \"Steam\" or \"PS3\" (among other)", "name": "controller_type", "type": "string" } @@ -42063,8 +42083,10 @@ "\r", "gdjs._extensionController.isXbox = function (gamepad) {\r", " return (gamepad ? (\r", - " gamepad.id.toUpperCase().indexOf(\"XBOX\") !== -1 ||\r", - " gamepad.id.toUpperCase().indexOf(\"XINPUT\") !== -1\r", + " gamepad.id.toUpperCase().indexOf(\"XBOX\") !== -1\r", + " // \"XINPUT\" cannot be used to check if it is a xbox controller is just a generic\r", + " // name reported in Firefox corresponding to the driver being used by the controller\r", + " // https://gamefaqs.gamespot.com/boards/916373-pc/73341312?page=1\r", " ) : false);\r", "}\r", "\r", @@ -42297,7 +42319,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Left\"" ] } @@ -42324,7 +42346,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Left\"" ] } @@ -42351,7 +42373,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"UP\"", + "\"Up\"", "\"Left\"" ] } @@ -42387,7 +42409,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"DOWN\"", + "\"Down\"", "\"Left\"" ] } @@ -42431,7 +42453,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Left\"", "" ] @@ -42459,7 +42481,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Right\"", "" ] @@ -42487,7 +42509,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Up\"", "" ] @@ -42524,7 +42546,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Down\"", "" ] @@ -42569,7 +42591,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Left\"", "" ] @@ -42597,7 +42619,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Right\"", "" ] @@ -42625,7 +42647,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Up\"", "" ] @@ -42662,7 +42684,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Down\"", "" ] @@ -42833,6 +42855,158 @@ ] } ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyJumpButton" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"LB or L1\"" + ] + }, + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LB\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateJumpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyJumpButton" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"RB or R1\"" + ] + }, + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RB\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateJumpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyJumpButton" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"LT or L2\"" + ] + }, + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"LT\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateJumpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] + }, + { + "type": "BuiltinCommonInstructions::Standard", + "conditions": [ + { + "type": { + "value": "Gamepads::PlatformerGamepadMapper::PropertyJumpButton" + }, + "parameters": [ + "Object", + "Behavior", + "=", + "\"RT or R2\"" + ] + }, + { + "type": { + "value": "Gamepads::C_Button_pressed" + }, + "parameters": [ + "", + "Object.Behavior::PropertyGamepadIdentifier()", + "\"RT\"", + "\"Left\"" + ] + } + ], + "actions": [ + { + "type": { + "value": "PlatformBehavior::SimulateJumpKey" + }, + "parameters": [ + "Object", + "PlatformerCharacter" + ] + } + ] } ] } @@ -42881,7 +43055,7 @@ "type": "Boolean", "label": "Use directional pad", "description": "", - "group": "", + "group": "Controls", "extraInformation": [], "hidden": false, "name": "UseArrows" @@ -42891,7 +43065,7 @@ "type": "Boolean", "label": "Use left stick", "description": "", - "group": "", + "group": "Controls", "extraInformation": [], "hidden": false, "name": "UseLeftStick" @@ -42901,7 +43075,7 @@ "type": "Boolean", "label": "Use right stick", "description": "", - "group": "", + "group": "Controls", "extraInformation": [], "hidden": false, "name": "UseRightStick" @@ -42911,12 +43085,16 @@ "type": "Choice", "label": "Jump button", "description": "", - "group": "", + "group": "Controls", "extraInformation": [ "A or Cross", "B or Circle", "X or Square", - "Y or Triangle" + "Y or Triangle", + "LB or L1", + "RB or R1", + "LT or L2", + "RT or R2" ], "hidden": false, "name": "JumpButton" @@ -42977,7 +43155,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Left\"" ] } @@ -43004,7 +43182,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Left\"" ] } @@ -43031,7 +43209,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"UP\"", + "\"Up\"", "\"Left\"" ] } @@ -43058,7 +43236,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"DOWN\"", + "\"Down\"", "\"Left\"" ] } @@ -43115,8 +43293,8 @@ "parameters": [ "Object", "TopDownMovement", - "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"LEFT\")", - "Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"LEFT\")" + "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"Left\")", + "Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"Left\")" ] } ] @@ -43145,7 +43323,7 @@ "Object", "TopDownMovement", "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"LEFT\")", - "sign(Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"LEFT\"))" + "sign(Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"Left\"))" ] } ] @@ -43177,7 +43355,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Left\"", "" ] @@ -43205,7 +43383,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Right\"", "" ] @@ -43233,7 +43411,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Up\"", "" ] @@ -43261,7 +43439,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"LEFT\"", + "\"Left\"", "\"Down\"", "" ] @@ -43321,8 +43499,8 @@ "parameters": [ "Object", "TopDownMovement", - "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"RIGHT\")", - "Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"RIGHT\")" + "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"Right\")", + "Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"Right\")" ] } ] @@ -43350,7 +43528,7 @@ "parameters": [ "Object", "TopDownMovement", - "sign(Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"RIGHT\"))", + "sign(Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"Right\"))", "1" ] } @@ -43383,7 +43561,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Left\"", "" ] @@ -43411,7 +43589,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Right\"", "" ] @@ -43439,7 +43617,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Up\"", "" ] @@ -43467,7 +43645,7 @@ "parameters": [ "", "Object.Behavior::PropertyGamepadIdentifier()", - "\"RIGHT\"", + "\"Right\"", "\"Down\"", "" ] @@ -43536,7 +43714,7 @@ "type": "Boolean", "label": "Use directional pad", "description": "", - "group": "", + "group": "Contols", "extraInformation": [], "hidden": false, "name": "UseArrows" @@ -43546,7 +43724,7 @@ "type": "Boolean", "label": "Use left stick", "description": "", - "group": "", + "group": "Contols", "extraInformation": [], "hidden": false, "name": "UseLeftStick" @@ -43556,7 +43734,7 @@ "type": "Boolean", "label": "Use right stick", "description": "", - "group": "", + "group": "Contols", "extraInformation": [], "hidden": false, "name": "UseRightStick" @@ -43566,7 +43744,7 @@ "type": "Choice", "label": "Stick mode", "description": "", - "group": "", + "group": "Contols", "extraInformation": [ "Analog", "360°", From b7016bd41ec2cb842990499f2334ca3645b710ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Davy=20H=C3=A9lard?= Date: Sun, 11 Dec 2022 19:52:25 +0100 Subject: [PATCH 11/11] Typo in Gamepad extension --- examples/isometric-game/isometric-game.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/isometric-game/isometric-game.json b/examples/isometric-game/isometric-game.json index 279923ad5..6ec3839ef 100644 --- a/examples/isometric-game/isometric-game.json +++ b/examples/isometric-game/isometric-game.json @@ -43322,7 +43322,7 @@ "parameters": [ "Object", "TopDownMovement", - "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"LEFT\")", + "Gamepads::StickRotationValue(Object.Behavior::PropertyGamepadIdentifier(), \"Left\")", "sign(Gamepads::StickForce(Object.Behavior::PropertyGamepadIdentifier(), \"Left\"))" ] }