From d173cf8b1e3a037c6d1d192e966a941d11f2b2fb Mon Sep 17 00:00:00 2001 From: "adnan.mujkic" Date: Fri, 25 Sep 2026 22:39:30 +0200 Subject: [PATCH] Finish map functionality, progress on base character template, tweak Eleri --- Config/DefaultEngine.ini | 3 +- .../Blueprints/BP_OrthoMapCaptureActor.uasset | 4 +- Content/Input/IA_ToggleMap.uasset | 3 + Content/Input/IMC_ActionContext.uasset | 4 +- Content/Input/IMC_MovementContext.uasset | 4 +- Content/Input/IMC_UIActionsContext.uasset | 4 +- Content/Input/UI/IAUI_PointerMove.uasset | 3 + Content/Input/UI/IAUI_Zoom.uasset | 3 + Content/Input/UI/IMCUI_MainUi.uasset | 4 +- .../WorldMap_Persistent_GameWorld.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_0_0.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_0_1.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_0_2.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_0_3.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_0_4.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_0_5.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_0_6.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_0_7.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_1_0.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_1_1.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_1_2.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_1_3.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_1_4.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_1_5.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_1_6.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_1_7.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_2_0.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_2_1.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_2_2.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_2_3.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_2_4.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_2_5.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_2_6.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_2_7.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_3_0.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_3_1.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_3_2.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_3_3.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_3_4.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_3_5.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_3_6.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_3_7.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_4_0.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_4_1.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_4_2.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_4_3.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_4_4.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_4_5.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_4_6.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_4_7.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_5_0.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_5_1.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_5_2.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_5_3.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_5_4.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_5_5.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_5_6.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_5_7.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_6_0.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_6_1.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_6_2.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_6_3.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_6_4.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_6_5.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_6_6.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_6_7.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_7_0.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_7_1.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_7_2.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_7_3.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_7_4.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_7_5.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_7_6.uasset | 4 +- ...p_Persistent_GameWorld_Piece8x8_7_7.uasset | 4 +- .../Materials/Sky/M_Pixel_SkyMaterial.uasset | 4 +- Content/ST_WordsForUI.uasset | 4 +- Content/Textures/Map/RT_MapCapture.uasset | 4 +- Content/Textures/UI/T_MapMarkerArrow.png | 3 + Content/Textures/UI/T_MapMarkerArrow.uasset | 3 + .../Blueprints/MainPlayerController.uasset | 4 +- Content/UI/Calendar/WBP_CalendarScreen.uasset | 4 +- Content/UI/MainUI.uasset | 4 +- Content/UI/WBP_MapWidget.uasset | 3 + .../Character/CharacterTemplate.aseprite | Bin 160970 -> 163568 bytes Dev/Aseprite/Character/CharacterTemplate.png | 3 + Dev/Aseprite/Character/Trish.aseprite | Bin 0 -> 163568 bytes Dev/Aseprite/Eleri/Eleri.aseprite | Bin 161427 -> 160954 bytes .../DialogueSystem/DialogueWidget.cpp | 2 +- .../DialogueSystem/DialogueWidget.h | 2 +- .../Private/EleriPlayerController.cpp | 59 +- .../Private/UI/EleriBaseWidget.cpp | 2 +- .../Private/UI/MainGameWidget.cpp | 5 + .../Public/EleriPlayerController.h | 12 + .../ProjectEleri/Public/UI/EleriBaseWidget.h | 8 +- .../ProjectEleri/Public/UI/MainGameWidget.h | 12 +- Source/ProjectEleri/UI/MapWidget.cpp | 1352 +++++++++++++++++ Source/ProjectEleri/UI/MapWidget.h | 535 +++++++ 97 files changed, 2150 insertions(+), 167 deletions(-) create mode 100644 Content/Input/IA_ToggleMap.uasset create mode 100644 Content/Input/UI/IAUI_PointerMove.uasset create mode 100644 Content/Input/UI/IAUI_Zoom.uasset create mode 100644 Content/Textures/UI/T_MapMarkerArrow.png create mode 100644 Content/Textures/UI/T_MapMarkerArrow.uasset create mode 100644 Content/UI/WBP_MapWidget.uasset create mode 100644 Dev/Aseprite/Character/CharacterTemplate.png create mode 100644 Dev/Aseprite/Character/Trish.aseprite create mode 100644 Source/ProjectEleri/UI/MapWidget.cpp create mode 100644 Source/ProjectEleri/UI/MapWidget.h diff --git a/Config/DefaultEngine.ini b/Config/DefaultEngine.ini index e904586b..bedc2c94 100644 --- a/Config/DefaultEngine.ini +++ b/Config/DefaultEngine.ini @@ -72,7 +72,7 @@ r.TranslucentSortPolicy=0 TranslucentSortAxis=(X=0.000000,Y=-1.000000,Z=0.000000) r.CustomDepth=3 r.CustomDepthTemporalAAJitter=False -r.PostProcessing.PropagateAlpha=0 +r.PostProcessing.PropagateAlpha=False r.DefaultFeature.Bloom=True r.DefaultFeature.AmbientOcclusion=True r.DefaultFeature.AmbientOcclusionStaticFraction=False @@ -198,6 +198,7 @@ r.DistanceFieldShadowing=0 r.AmbientOcclusionLevels=1 r.Shadow.CSMCaching=0 r.DOF.Recombine.Quality=1 +r.Deferred.SupportPrimitiveAlphaHoldout=False [/Script/LinuxTargetPlatform.LinuxTargetSettings] TargetArchitecture=X86_64UnknownLinuxGnu diff --git a/Content/Blueprints/BP_OrthoMapCaptureActor.uasset b/Content/Blueprints/BP_OrthoMapCaptureActor.uasset index 63aa72ab..202d3cc5 100644 --- a/Content/Blueprints/BP_OrthoMapCaptureActor.uasset +++ b/Content/Blueprints/BP_OrthoMapCaptureActor.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73b6e6d9dfefcba93bbb84f31f414bdd33759fbf0f83cb2c93515ae29394b311 -size 25834 +oid sha256:5d2168304c9a61232a253b0ffd704c790da5ca2478fa7b45061b33eada83e247 +size 26054 diff --git a/Content/Input/IA_ToggleMap.uasset b/Content/Input/IA_ToggleMap.uasset new file mode 100644 index 00000000..f6c89e61 --- /dev/null +++ b/Content/Input/IA_ToggleMap.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c0ed99d980a8c8edb5a232d9e374969369fa8cfec6d1332e97dfb86442b453f +size 1183 diff --git a/Content/Input/IMC_ActionContext.uasset b/Content/Input/IMC_ActionContext.uasset index dfa4b6ea..3107b03e 100644 --- a/Content/Input/IMC_ActionContext.uasset +++ b/Content/Input/IMC_ActionContext.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c946afe6f4a9274fe117602b7493de04df740a14860fcea2cea08b598dab8633 -size 8671 +oid sha256:147694e4db54924d7892b8df40d2eb2f417c4ed038f72da7a0c41ad42b8d0ede +size 6253 diff --git a/Content/Input/IMC_MovementContext.uasset b/Content/Input/IMC_MovementContext.uasset index 0b0d8f5d..cc984892 100644 --- a/Content/Input/IMC_MovementContext.uasset +++ b/Content/Input/IMC_MovementContext.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7d00d4de3e20d4bb68be507b83ba67816f34e43b9d8be1dedb0b26e42d9d2551 -size 8383 +oid sha256:f4c84ae835b7e9510f860de5db2b6099523563124e69b40529c9f5ca6a6e688f +size 8842 diff --git a/Content/Input/IMC_UIActionsContext.uasset b/Content/Input/IMC_UIActionsContext.uasset index 64433c4e..12478016 100644 --- a/Content/Input/IMC_UIActionsContext.uasset +++ b/Content/Input/IMC_UIActionsContext.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cef04032cc8b1d51c6179927ae848e856968f34e7759df453db8b9635962a8a1 -size 4971 +oid sha256:11d363eb11c9e04fbd8bdeaff575d0c0a1127fb612ba1475b52ffe5371f2385d +size 6261 diff --git a/Content/Input/UI/IAUI_PointerMove.uasset b/Content/Input/UI/IAUI_PointerMove.uasset new file mode 100644 index 00000000..1476edf7 --- /dev/null +++ b/Content/Input/UI/IAUI_PointerMove.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8700c37273e41ba865b3cc2eb6365ba96d2f6770776b85376e8c5050168d2fc3 +size 1405 diff --git a/Content/Input/UI/IAUI_Zoom.uasset b/Content/Input/UI/IAUI_Zoom.uasset new file mode 100644 index 00000000..cf652b8c --- /dev/null +++ b/Content/Input/UI/IAUI_Zoom.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8cbe2bd6be9dbc470948332a4b8e09fd2e131e510379df1665fca51a44bdab0 +size 1370 diff --git a/Content/Input/UI/IMCUI_MainUi.uasset b/Content/Input/UI/IMCUI_MainUi.uasset index 4bebe555..e39d91d7 100644 --- a/Content/Input/UI/IMCUI_MainUi.uasset +++ b/Content/Input/UI/IMCUI_MainUi.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0fedbd588f8e998952828d7ba48320b99e01d5435d013a912075921ceae67ebc -size 8155 +oid sha256:b69b70e225db5b1b667ff64ba60f2e7458ebd2ddec6522a4596e2136858c3f2c +size 11943 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld.uasset index 3aeba881..6b39c8bf 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28dfec0af752b39f2c696ad7a4cb2a466f8ef88df08982232506cb4002e2c1c6 -size 1506917 +oid sha256:e46e22e39f1828ccb8482b04f16617628124a67f787811cbc6c49c55ceb48963 +size 1509110 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_0.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_0.uasset index ab072d6e..8afdca31 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_0.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_0.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38dc5cdf1693f0b438bf15c147e77f3bbc4d26f3348992ed71729bfb2bdbabd5 -size 14481 +oid sha256:c51252d0b2ad4ddf9c61e76cf8e4cff8d26682b68fa885934eabc67a47f3cb0e +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_1.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_1.uasset index 682d1f44..4eb75267 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_1.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_1.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ddde8e31bf85575b000e0327e94f09af4efa12135cd74a0ed03c172633c60d2d -size 14481 +oid sha256:77206b48c0fab8ee8b51f094004730281ed53d3ab5ccc4ce8b910bc1b7581a46 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_2.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_2.uasset index 4bf04c95..e9f0cf7b 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_2.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_2.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fc79dab8a366be2bd8ea7b93df6e9b10f6b1611bbce8227496b5fbe8178415c -size 14481 +oid sha256:7c5c8282a89f039fd3466dba218bf5532acf94c3c8fdb7441581d5915838e39b +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_3.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_3.uasset index 6f457b4e..88c3596c 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_3.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_3.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ffbe2e9b663815fe085bccb8cd5c67932200a44d83a5d6b4b21214ca3a89cd3 -size 1329691 +oid sha256:23ec6853f6482b5935f38a55d1cd25bf8e471cf0d9a63598eca0c7a8e4cd8800 +size 1331819 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_4.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_4.uasset index 179830fe..729db2fb 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_4.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_4.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdd35b706cdcc4ec4805cfa424ec150cf2a5bb905af0a4ce4d4b196b3c24d97c -size 1330759 +oid sha256:c002465e122039ff030036a5369f4c0c1f2d7858f9e3cf8b6e2f2890fd20168c +size 1332889 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_5.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_5.uasset index 8b068835..80719192 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_5.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_5.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:683087ee1e42c83fdc810372c265f613da7f894b02d4dfe056b657040952c0eb -size 14481 +oid sha256:64c7e4038327b22341d05103fa3c65a3fa6599d16be8ea74253a17d7891c1428 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_6.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_6.uasset index 3da97143..1aa50542 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_6.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_6.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43c3869514bd9266e5bbbcebeed0704c9af5d398cd3dcaa24b698d6c65963377 -size 14481 +oid sha256:f8d37e0ee0856202d3cdc83f57210258b9b103c5d53c8011a7c21d9a2ce641aa +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_7.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_7.uasset index 0fc0556d..cc8c1816 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_7.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_0_7.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1ed669addf8515a1c6e36eab077861dd69e369bcb31bd3b26026f09734587e2 -size 14481 +oid sha256:a25d933d5c320324a843b5e29b5b053f9f3b4cc81d546c3b15144da14df6e1e0 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_0.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_0.uasset index b8f8d451..459967ab 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_0.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_0.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c686536b7ea9528aa63eeec217b5d79334c89f4f39430670cdd671979dd3e379 -size 14481 +oid sha256:11172ffd29a98d0c11246bca3daf709cb5f38643019dea6808bf24025ecd3912 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_1.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_1.uasset index d5fd9dcc..d8c19e83 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_1.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_1.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78e20e051854eeb3dab190231025dbef7cdef6da2cc78b380ade254d06a8e229 -size 14481 +oid sha256:e55f11eb2d413b184184372106b1abafec387b6de7e6edc4e8ebe40777cfb7b9 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_2.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_2.uasset index 0105095d..c6922c6a 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_2.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_2.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f4ccfb096f5fa3296317195de1234de7e93114abfadb58b184e08c220c1c6c4 -size 14481 +oid sha256:81e99978194f57cb81da39d6856f420d7572675023697d124906d488e940f9fc +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_3.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_3.uasset index 03b2349f..3d59b1c0 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_3.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_3.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3626fe47ebdac7614643e107fef0d952647d39c886a42b1c502c3a2ecfba1a3c -size 3650986 +oid sha256:87036bf82221d7387c9e58b3328580a9bc14ce89de278b777dfd503a64367d8f +size 3653324 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_4.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_4.uasset index d0b69452..2f6384f2 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_4.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_4.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:badfa8a521a32680a082b2b1db532f61e8f5b0e45c7b297ce5808f5a86351842 -size 3526973 +oid sha256:e4a1d079b8f29bb6443d1fa86a3abbc7339bacdae9a366f81e0236330129740c +size 3529254 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_5.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_5.uasset index 548e4f83..dc8f8e62 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_5.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_5.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa99051714b2267547ebf20e18d64a217fde0d90bf3cedf928d2a55107e94b1c -size 14481 +oid sha256:101653d2e2b26cdcdef0751c35bec02dc160d960190beca834a7815a57798147 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_6.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_6.uasset index 9f05f0f5..4fdff7a6 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_6.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_6.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bc49ad3a585a7fd7829f936b695f795bf1b63321bb81434960ef40319116788 -size 14481 +oid sha256:95700986c53edbdd4531e1aeea24de1ff037f3c381105e2e2a995714433b6d1c +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_7.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_7.uasset index 067f548a..228d473b 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_7.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_1_7.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43dc0274fe15ae81e67fc4f590f2f4ac6c9c55bde11b38ecc630919a0587bacc -size 14481 +oid sha256:c5545444456b374164c064928b91a5804b1a07070a12af80fdba5bc7d0eaa2ef +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_0.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_0.uasset index ce2c3388..a5989d49 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_0.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_0.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38317ec637861eca39bf65fc09183a5fdcf96ac08d31dd4b6cfd94124dd6ea7b -size 14481 +oid sha256:660e5a0b3acbd28343d5b0651ab7a6d4754d4c6c449838bb32295965e3da1048 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_1.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_1.uasset index 94c9d513..aef3bb54 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_1.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_1.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93af292088c2b4289266f27b04c40318756c7c1cce1428846fc1e7073094bf22 -size 14481 +oid sha256:de7e6a4ae587c0bac09013e44fef086ebe77fd6d5c95c768e8eb1dc4a5ec7162 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_2.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_2.uasset index 8032444d..ba4d9d59 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_2.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_2.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0b0bf763550bccdb5ff544a32ab3f82013c4d0730826d707f4f6c5aca2b85fc -size 14481 +oid sha256:1e0cb00a0feb3c09d67441c432a1f1e317804727eac3e1373612363e7930166c +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_3.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_3.uasset index 9a210090..d406199a 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_3.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_3.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6968e6508faed30ed7f1c014ae9e82e8c424bb59f637290b80d95c1bed6ffff -size 1944603 +oid sha256:56277e8f56cd9269c3434560c83dccbdfd816da6e0d46e21ab89c25b798cadb7 +size 1946903 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_4.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_4.uasset index abffca88..b6602236 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_4.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_4.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3ad50066c019bae06fae00b74fa7098ed529a54347459f32c1a7a36aae8a447 -size 1690809 +oid sha256:bb9063672147300e871ee9121cc32dd3739ac942490bfee6a0a3eea14a9b0ef1 +size 1693110 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_5.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_5.uasset index 84575ac7..aa3d8de6 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_5.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_5.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cc6b7eef0cdb39dac94ce89a7c48e7318bab7d9461cc4d70f68765596a2176b -size 14481 +oid sha256:8d39aa97bf71de77f7d0439dc0b349e56ff9bae82631e00d35a7305b4da30d2d +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_6.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_6.uasset index 8d5d09ff..bcb47121 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_6.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_6.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e332ca17da1f32ce06939ae1c0e4b6332b8518c484270c27b792732ea446b60 -size 14481 +oid sha256:205e71d477a7b0b2e30073ed03f88f54f5e144809d0d1646af4004f05ec36565 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_7.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_7.uasset index 81e5cb30..99a92045 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_7.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_2_7.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8db9bbf55f3d7347637e9680522601f80e04d4779c894ccae88ba040038918e9 -size 14481 +oid sha256:af525c831cc011162db306accf2fd7ed097edb5110519696c02588548ec51d23 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_0.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_0.uasset index 3e5cb573..2b0712bd 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_0.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_0.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aabf69dc7e450f84729e4328271a48c78ac1f81275c2585c4ec59feb0e7f675d -size 14481 +oid sha256:6e1412360846cbb238f1482b6dfb5787076ad88c0f63317f257e8502c6d0fa19 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_1.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_1.uasset index adde2cf5..19793191 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_1.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_1.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0b639d06ce9b2371f5f8e34b8b7a7499e9966032ca2d49b77f3406f4bfb41fd -size 14481 +oid sha256:8b3286986e4d1833c934ab5b4a7c08c1307d2c6005a99c3998512225ed057d7a +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_2.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_2.uasset index 215bf9c0..b7da5f9f 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_2.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_2.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ec35ab7f42e04ba5a573166bed5603c8b48f9065978079314d32ae1ba5e907f -size 14481 +oid sha256:b39c566ec37934e41ba8bb0119c261a7e3841c7bd1875cf18ea0a7ff62b5317e +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_3.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_3.uasset index a1aa46f9..b7e19a7d 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_3.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_3.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b08644825dfa4ffda9725f57083a67c9b58600909743bc17dba935dd46d0a756 -size 582461 +oid sha256:3afa837b38c39497cb20183804da9953b2b54de079416983172bcdc7d2a1d1cc +size 584425 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_4.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_4.uasset index 8e75b75b..0637a813 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_4.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_4.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98be6fa20fde3f3781d3ce8251bd0655613112b96d3ec86713a89fab4cd1c274 -size 1412628 +oid sha256:1696d0e8dad16c9e613460aaa7c53f917e7264060fd5fca9d81e5d1bdfa10520 +size 1414705 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_5.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_5.uasset index 5fd9681c..53f403ae 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_5.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_5.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18fbe1ed5593c681acdc4750c15fedaaaebddfb089178f106f1c109a3d764790 -size 14481 +oid sha256:8e1117af8f28de573759609d4a9eae4b52996b33ddfac2089d7a21db6b98326f +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_6.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_6.uasset index 222314ae..8a2e577b 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_6.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_6.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:455b9863e4f42891348aba91145b8ecf67092f03d726135d0d1cb75dca1ac3ab -size 14481 +oid sha256:ce9e093d8ced53d277938e1e226fa5f5932e10e849dd2c67c63fd859776ed74f +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_7.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_7.uasset index acf00e2a..11343be4 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_7.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_3_7.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3acd4060f9a0f682ff2707cb56b1dc14ab855d0a4ba66b96c6ab79bae14586f -size 14481 +oid sha256:5e4a1d85ac3c9af7fa9c7d6fdd874f8b4be94854ccf522979bc3245c41b31386 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_0.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_0.uasset index f01b5161..c77d23b2 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_0.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_0.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4783323d80a1bfa21f44052f9ccec649950d270e024d84263c64ecea07577b08 -size 14481 +oid sha256:4bcff0306c1ef70da56e2a17d9d237fa5963a507cb5287e498bdbd4e5c60758f +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_1.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_1.uasset index b97a1778..5fd7af09 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_1.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_1.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:844befd81761d840cc5b758ba3be97f742b211bff78874460dca86fa0da133b7 -size 14481 +oid sha256:f1fe7942c0d755ed4034cd4a2e833d406a97c40f8a40d29e6fbc15592fd9f28f +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_2.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_2.uasset index c9bda29c..93d50247 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_2.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_2.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edcc1a93ad83dac44fb0816ab0283fb798a45f006ddfd47f5d9b85e870aa22d7 -size 14481 +oid sha256:64e0a9d5579a556de003c971d69e11374630939fa8559b860570d01c8681a713 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_3.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_3.uasset index 89622362..94db9cd3 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_3.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_3.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d3cd8fc3c6bd71fa376741deac5e0735c5f3d20667d8f330f3e096f86e0505f -size 990203 +oid sha256:5d8005017566ccb51ce84a45ef4e80f8e337060534edc0b90b7efcbf3f408689 +size 992203 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_4.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_4.uasset index 144409ca..a9e00fef 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_4.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_4.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de547ea3d033f018adbebd78624187f7cd6566fbc2ac2388acc64b14734cf02b -size 1757779 +oid sha256:3d88e621104835f6065eb171e9c409202b742d56d8f470bcb477b374c7f73064 +size 1759913 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_5.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_5.uasset index ecc987fc..3852912b 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_5.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_5.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f93792a60ea11e2964389b24794362090cfa91cfcc2ff84d94f46afbbab0dde -size 14481 +oid sha256:efab8882663ec5e9058d3dc77013c9c55b64602837cc3eaec6fb2c6ebade29a9 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_6.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_6.uasset index 180fac5a..ac06b70b 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_6.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_6.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1a5efa5eb0adba135b0a962c4356f6e827f6005db166bd6a3ed63969f65a49f -size 14481 +oid sha256:616e0cfe55222d75f3fcec94fbfb7f5d8e57b550a77385e0dffdd65720bd5086 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_7.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_7.uasset index 65795252..4f66963d 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_7.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_4_7.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5872aafae0bb3ee6a08e71e05b4bef09a693326cbe9f8df8edaf13affac862c -size 14481 +oid sha256:e032414065c4aff250061ecd6a56271425d313cdf88c77cacc5463378975caab +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_0.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_0.uasset index 17cea174..081abe99 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_0.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_0.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb248be77ae6d2da8d5385cf53697ac6c499cbc16eac2bcc7498544cebde4d41 -size 14481 +oid sha256:ae890d727784add5319aeece34f2afe3b9fe2489e8df9a849f5e2a5e5bdaf6b3 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_1.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_1.uasset index cc80d960..3cc58959 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_1.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_1.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69669f286b074204bc51d26a5924e6623a2528776621895dce43ae04b001b9fa -size 14481 +oid sha256:6d57c38f23eae68064c4d0f16023a6df60bcd79b979dfe9f1c48e13d4a5e8fc8 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_2.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_2.uasset index dbec9054..137690ef 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_2.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_2.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:24d184135153ed2d3ae1d5c27e47d1c409bb2da6c5b3de8ddd0b9b5383514264 -size 14481 +oid sha256:a5d8a6877a800fd53330dc0f2baccffda914f7ee7a05656344a853ae5528ffec +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_3.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_3.uasset index 7229af98..9bf08058 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_3.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_3.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4a6190a4df43ff8d5af70cfe088e35634994042778731a0ae9471632dc9c703 -size 14481 +oid sha256:a8b7bfe8fcacd624c48e6b6edd7b6c82eea43eb0aea842eec6e34fd1fe9147b0 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_4.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_4.uasset index 1901c2ec..570f10bf 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_4.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_4.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04f2403d5f49af916db546ac2f80ce5695f7454493cf8d827452816c2ab37eff -size 14481 +oid sha256:4a11ecdc259444151b3313676139c1fde1ed38fac84096b4b981098f3adf6ac3 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_5.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_5.uasset index 79124b81..5833e687 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_5.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_5.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07445349207381d6dbd0297d75b41b9544829e8278a75b6ac9ebbd5d2f1be3d4 -size 14481 +oid sha256:b72d400e3b705e45904f3a60c3c238038b5d7b013ddc72e3f957444770ef6669 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_6.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_6.uasset index f4d04479..83037177 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_6.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_6.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21ac43259fa4e50d63cbc03115b2a3b2d348fb284f6f7cac95d4392ca9d34265 -size 14481 +oid sha256:fe9fdb86181c5a2aa80cdf675b90a0866e7393625585a60f2b42180f815230af +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_7.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_7.uasset index 4581d3a2..3724a5e1 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_7.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_5_7.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcc83c5d484ba684764dc22c5c9dffcadc59d68894735503c1db41d9ab4697fe -size 14481 +oid sha256:26bb646ef61e232854356346595ba1905ce162e08ae84462a05b089e0385e05f +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_0.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_0.uasset index ff170885..aa98ca56 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_0.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_0.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c28e5355690d765a66716c9f9fcb965b94e84a0126e4ec2e16a2913338444344 -size 14481 +oid sha256:b13bd5312d664d12c3f43febf6d2df8507a08ca48c804edecf43764feebbf035 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_1.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_1.uasset index 7c9aed8a..09d4f33e 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_1.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_1.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f19223dd1ea07cf283aac50a61e8258b58c74255c906738aaa230d64f626b2c3 -size 14481 +oid sha256:653c43a186da56a69848c5681ec1f25c256d30ce92ff7cd25e87c077334ce3ef +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_2.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_2.uasset index 9d9cfbdb..f233582e 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_2.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_2.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9439bf203e95784e80d2a39b50a789dd45baba4174811d2004ba470c847b5fa7 -size 14481 +oid sha256:22087e9292b03e575c3fd061020361eabb1170732180c8b7177e30805377acea +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_3.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_3.uasset index f7e1551d..6bb37c98 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_3.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_3.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1eb8920025c605298840cefc73e4e6e73f5ca8db8d19a8d5fbb55924f4e6b15e -size 14481 +oid sha256:4ed1a5783ff1bdba7cfc828570d65e2935ca18085d909a49a19922ac97bcc236 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_4.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_4.uasset index 3225e6fd..ffbdc5ff 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_4.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_4.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:455175ec329637e04085cbe1744657f7a4ee1ce5de1a9562ffa64026396c0ac9 -size 14481 +oid sha256:5d031adf53dd9c9f11c756999e4ebfd601abd0f9ec12ad894d80c83c2d41a54c +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_5.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_5.uasset index 902392a8..8c028d1e 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_5.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_5.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c02e9ef0e6847f37d503cb5592acb620bfc708147078517e2e80337c2fc72231 -size 14481 +oid sha256:ec314196a20bb05f724b2e928e37920744897438f784ce5e0d3532c403512d2e +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_6.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_6.uasset index 21fefbeb..19f9f7e3 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_6.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_6.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a7e97c5bd5bffc7616a62df1b51402d54461d798836de157a548aa809542278 -size 14481 +oid sha256:9f44856a440ddbe7f3683fbbc3fea501f5d6a4434685e619dcb410502b4ab243 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_7.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_7.uasset index 4f1da0c8..a9d8dfe9 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_7.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_6_7.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f749879f7ff53a3b383ceeb1502cec0987640175166eb38e2067b947feba3421 -size 14481 +oid sha256:dca68a77d2d8d785ef41d7f6657e2ad8be2415b864d2b3c2d0285885858f7f8f +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_0.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_0.uasset index 47ece284..2362be61 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_0.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_0.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e96a8cddd74a82fdaaa99fa97fcaaca463b1b8c60e5668e2513b38bbae063702 -size 14481 +oid sha256:c88242aa626e915b13e2af24d4e73556b02c4b767cd66a9b0642e8c08c4e9d1d +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_1.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_1.uasset index f0329748..5a867826 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_1.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_1.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42ac206cf5c07819100dde62c09c880b3953443e44a012a46dc3307741d918f0 -size 14481 +oid sha256:2cf049dec938c1384aca8a20010aa79c67364d3d582cdc09ab8d4dd1fceb38f5 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_2.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_2.uasset index 0424ccd2..a3b3ef5f 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_2.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_2.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a90952fce2adff053e5bbdc8451a9d4e2913054deffd1c7520ffdea613ed84c -size 14481 +oid sha256:3872256474f743997a3ed8da380230b4866d5be498f4d789f94aae7f2ae5bad5 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_3.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_3.uasset index 0e369798..280b43f2 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_3.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_3.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e35722ce44ed69a3e61d3df4f21d98431349e414f8c4eb5e09e5cd7f38b11408 -size 14481 +oid sha256:0b8e69a97de126755c563128f4651012692df75c154d3e23de7aea8a6ee5cda0 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_4.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_4.uasset index fb97a849..a3697979 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_4.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_4.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2c194f5b0db89a6c494b5224cecd3f452fcbfa431418af06450451bbef59e2a -size 14481 +oid sha256:5ed0357ebc30f912aea3ea3330355e8a0257062511342c526839027a27f7dc89 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_5.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_5.uasset index 53f34bc6..33c1c136 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_5.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_5.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e76099d352bd7cc9e1fe8b8abc0e600f404c7c439978550ddcd414364e315f9 -size 14481 +oid sha256:0a7b57875618bf88cf5fa456e244afef5d90592fc08e8912125d1149549d85c0 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_6.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_6.uasset index 33b46abf..33fb2a86 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_6.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_6.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b59730a4a385f52ffcbe65ddf633cb64c3c8453f71168b8b660c46f8cad5a2a2 -size 14481 +oid sha256:febe85f0568f21f5049f4b121fb9f7ef7bec58bbf616eb5ede70550b0101c833 +size 16252 diff --git a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_7.uasset b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_7.uasset index 3473f7b1..a6aa5a4f 100644 --- a/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_7.uasset +++ b/Content/Maps/Generated/WorldMap_Persistent_GameWorld_Piece8x8_7_7.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7dcf794f6400e86c1399a65baaa46f5f4398dc54aa228eed9c6f615a0a36114 -size 14481 +oid sha256:8f09d640eb540744b3e68af37f282015a2a6ed682ae0d367b25bb5fb9fe104eb +size 16252 diff --git a/Content/Materials/Sky/M_Pixel_SkyMaterial.uasset b/Content/Materials/Sky/M_Pixel_SkyMaterial.uasset index 52b087ae..4f507c5b 100644 --- a/Content/Materials/Sky/M_Pixel_SkyMaterial.uasset +++ b/Content/Materials/Sky/M_Pixel_SkyMaterial.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3482fc4d1f97c60d083f27278cddf8e50f73408456f9a1c40e6007d5e09e5f1c -size 55891 +oid sha256:1b22dc8d9d63e91f8d37383817f9d363206df723f7fabef5b09d187a1d7d0a9e +size 55442 diff --git a/Content/ST_WordsForUI.uasset b/Content/ST_WordsForUI.uasset index 1883ca38..1f656c7a 100644 --- a/Content/ST_WordsForUI.uasset +++ b/Content/ST_WordsForUI.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d589f390b52f55104bb402686a6fe542d32edb0238fa75e71720d7d0ad0393c -size 15384 +oid sha256:4fe9535711454688a8d3bb7995a8cf6172dbf430b88a94d2b1c11eb7c47f1352 +size 15644 diff --git a/Content/Textures/Map/RT_MapCapture.uasset b/Content/Textures/Map/RT_MapCapture.uasset index 1ed23fee..30685164 100644 --- a/Content/Textures/Map/RT_MapCapture.uasset +++ b/Content/Textures/Map/RT_MapCapture.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:888964a6dcc10779999ee3c31a9c3d93d24bb55984f30da46038cf97fda2ea4a -size 18616 +oid sha256:f658787a185a142f5013bb06d9266864c802ee95f730c6047c16db1185fd9eb9 +size 10065 diff --git a/Content/Textures/UI/T_MapMarkerArrow.png b/Content/Textures/UI/T_MapMarkerArrow.png new file mode 100644 index 00000000..44328ada --- /dev/null +++ b/Content/Textures/UI/T_MapMarkerArrow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5fa0dbd879b8e51148a8a879b3348d4d67f325e8dc7d146277fcf19bd452ded +size 239 diff --git a/Content/Textures/UI/T_MapMarkerArrow.uasset b/Content/Textures/UI/T_MapMarkerArrow.uasset new file mode 100644 index 00000000..ec5da673 --- /dev/null +++ b/Content/Textures/UI/T_MapMarkerArrow.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b15129c63b2ac88e77c1680fcfbffb442c162888812ad4ce0a599b313975dce +size 11750 diff --git a/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset b/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset index 79e3cd40..15aeee65 100644 --- a/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset +++ b/Content/ThirdPersonBP/Blueprints/MainPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:108d99194a4f95318ab0e5584528da8619c9fec779abd8948c0db1ee844dc50d -size 74780 +oid sha256:65116fb35eef56f0d936b653f86324f4e7fb2b2f68fca3473ba27567ea04f3db +size 75348 diff --git a/Content/UI/Calendar/WBP_CalendarScreen.uasset b/Content/UI/Calendar/WBP_CalendarScreen.uasset index a09ec154..c76693af 100644 --- a/Content/UI/Calendar/WBP_CalendarScreen.uasset +++ b/Content/UI/Calendar/WBP_CalendarScreen.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd4f618039b61d5d4c53e320fe6481ecc416ec6e86abfc6bfb5283b22b1b0d4b -size 161047 +oid sha256:0cd8fcb2d9643c3fb54d309ce516f5e9a44983053a6984a7efc938542ca1c3fc +size 160135 diff --git a/Content/UI/MainUI.uasset b/Content/UI/MainUI.uasset index f31578e4..8c7e8bcb 100644 --- a/Content/UI/MainUI.uasset +++ b/Content/UI/MainUI.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdc73eb0baabf986091a192dafcfca7a301326a4749321358f374af670eb0dd3 -size 637366 +oid sha256:d34856c1776efcffc97e645ac867a9a4019a8d96a5c953f125ab4ccbe2bce016 +size 638510 diff --git a/Content/UI/WBP_MapWidget.uasset b/Content/UI/WBP_MapWidget.uasset new file mode 100644 index 00000000..83cbd101 --- /dev/null +++ b/Content/UI/WBP_MapWidget.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:734d6debf8745ab795371d73e6b394e70a456aee4d1e05cd1f3ff0280209b9d1 +size 13744 diff --git a/Dev/Aseprite/Character/CharacterTemplate.aseprite b/Dev/Aseprite/Character/CharacterTemplate.aseprite index 2a25f5760f247854cd5c276bbfcfbc55464f6896..f97f49a3040576b4a7d837ce6ae90b9335903c4c 100644 GIT binary patch delta 11803 zcmeI22~ZPP7{}k+92;yd61gEnj#@kuP%K&z5DOv{#1Rz_2q0C96%;K}Yd}Ta1EP zmh|75tPm#xziN0Ij%NJq0RrYEER>H=PM$uuH&U{#Y(=fk$iq}g(_h4IW6NF8FRWr} ztdELKaV!q=`gq{89C;7m00x3)V?-Q|1=7~t-8JXO=4Ms~OqdW?zX$O=`ygKz5&QI^ z8UY&XB2$yc-nw^)qbMj#mqt;3adMx$p&D7jeEs>1QzG{3UP;OWa3zFWqEqi`{_q<# zK;7%>a%&sKUew^S9u>rb>hAu+6`Vgwb!=6o*Ic;L0Y1I)Or=sr+l?yk2Qr{nu zuUe@R2J_vrH(8(ZP4E%tRK@6LmF(e!3i!J#3c?yjrZ{*HuBp(cEMMh+S^g?Nk()j= z@K>@?%KP1u)!%8&nr#t{wg()+kfN?WK76RxEkEEFo_*nt-(o^FCGnGuBO@EFLu6TNSz!E$g!x;|Bni4K=|Nrh^y(szP%}Vq znrs7_-C@SQ@2-Ivk0~ z70A;KjPfG|&o_efv1=E(r>?f2K z9b*8Z|!lo@`y*`#xrUScB_v{-EzB?#>~u zH678v((`tB)wLDEF`DQ_e`TFJeK6c#Nba=#+*<3PN`x~Pjke-s)O@;j9{?NUdepRB zU--PPPsY>h7bDfyIB2OY7s9zDzBTLvCFty0Jr*Gn@Tii;5QuK&d$#yGhI%)@b%y%$ zsI@Lpx9^U^oT+Z3FfToiRku=@=W_jrsi#572f46}TtaUpq*QTbrqTa0w4b=LJchO% zliS_v-jB-d-FWv>Txmni`Em%g%_;_X;f=&XbcR}Z+ zhFy>)J!iEv%vrZJxWg*~sMMD8iT==AtDFOEAV(W<8(LZD+=bx%y-_z)v@7LsK)|KY zQ+0V7S-`BrM|al}1Q8qhRr;dz*o*$NR-f4cDl5LozPmH3(i)kH(NVasi%jyR~vTJiw#(Q5hM{cVE00w3nZDPII&l zsqtXC$T3v5-d=NidyVd7P_V3!oBs;<_*#*NKOIO29NPrKyAV21Z0s#OE?1`>$QZdU z@?O#s=V?Akjq+`E5|p(s9-0VpkEk8G$bPnkuw)2og^U3?q|= zDl{rZ^Nt)>>C$uH@IZ^z3FgZaZhojd8C(o5oNDyJWS6OFGR5csS_==aMR*?JF&JF` zeq6MRJ!!5xUJ@u}K-sEOaRC>%nFqg_h%gvHTQ5+qwtv^P>a2tWiq`Z7zbaS?*V?5LYb+NNsQZ^VMaHi*SX zHagqFY}*@(U6GTA^Rupt9hRS~qwKc2?H~)unByuPcA8vleHG_nqdE{Pl)?HswQ;OQ z>uc(VfK!XjZ-h%T?OMf^iQFxYI~o-}pl^MV$8b7^uw4vut~biCnoSFqRP6hJwSon> zWfmJgi>5z~9q$o12|M2EKjBpDcn4m9>UNHoVJv4D{}1xG_StI12|3Yyttb!>jp_iC zR+wvH?X=>-M!DFL7w^TIv^sJj@SL@#c9=;iT1PPFk0b_pjGE{F+xvP5_NV4VMgxvP wsoLvAfrx0Y6AWLh#0=jyy^hIW{4-7SUV_mF!^H9Fc{PSF!}sCoQrG+2-`;LTK>z>% delta 10236 zcmeI2drVVj7{&lbKH`|=QG7exbfK7vGqq|o zZEc9}HTc?K9Iyf^Z0KqRpn&Lay3a*m<*GQ{KHY8J^FG(7fx@VDcr7Zkoq|-MZ0Zt= zDmr3HO=KdGU7xC3c1v}HU81rIX-U3rk;?O89iko5KSU2Ut~N?_+8VFwxuL=hi+Ifm zKeU0*_=n2_uSbv&`DA=9=kg z=TwC&Pj0xfC3Sbn=JcOKn*o@9X2spJu>ZF+(8)|LKF7=zLy-lgl2^W;aEexu7xGKiE9M_7EbmWYmL`!$y9v1x+RH;0J*gEBV_H&epWf}IJhcZ)^ z3cY$7dOl&O+$tKCKw0!_x6?5A!KYcM;WGf^-;V<>d~+Y~omR`Pr*DS4+polZP$Jx8 z4~THm5{)SlE)!0072JoHef=N+hn*ue&PtJmJ4R?;xQ z7kQf);bEfGF~ztR)ogGa=KIe2dC_1$Z+6$m=gqett;kUV(b)%7FkV|rFHqm1gR_H= z4(h-(V)iegO!X4VEarAV2^Gb~-W*%q{e6-{zK%)|FObsk)8bJZ&j?NGHFSU$pcL0= zU?oG6n30ma*~Fuk1TT<|8hM^8$81uLw=Tz5>iB7Z_z6kF?P@e@kXpRq&A=$uctI`X zK8BEk9c3Zb`T+d$foqyJFTFRyzt8lAFbPG}8KBmEnqPJi8z_cqin^;>}p-QQ6TW!u|hxr_V&MMgbgPZJv8@>FHw03`O!qx-P%_P zb@0ZLgrSH+zT1E7Y2S0*4!>4Sn%?k*^dxnSFp;R<@SRoV!+ZTzDXK4rI`XBO&;aLD zhYywJNAQYyAC`(|k;@YrF-{?r9Bwe=D-t$)#_SA#@4WbNs;FM-nZ>&OJ4KFqr^xBw zDb8R~jDWQ~LIkHld-)Y}XaK45%d?;!otQIT_uLgPzds9FiYl_DjXcGEVwf4~lW$xZ zu0f@iX3X#G>L|U<_Iorq2i@k!KHswH!lWbgo3ks~)(qRMy2y#2UOKi}@8C>2 znoC=>fn!YBTKQrp_$X-ZK!XPQfd9AQBNMO&rh(^WNnEVYy;nP1oRJ#4C?h0xzwg8H zJQIbmtE1_Wu0vc`?KOY4BapBi)&QC1!JNedp+Mdwgo_|Y4p+bs$d$|dVC={dC`qAz z5KK}~Qz(3SN6b~c1xJ)B;G=*!FXD)tCwlu*MC`3eVmIX)akw(Zp)5rrJk`O~ro^_G z(i2+C1T1i7OR{8iKhzZd%MT<2e-w9r_AppmIptPNLULtwL-9d^1+;t!LW4uHVh`h(jQc8F%gy`+o0@q!xG8BbJqvR z;MB_oC8>Oe{#)?Er72i@plC2CJ=Y604PHC5W`~Dn@Pw7hD>lLoCt~iH-mRIJn`;sd z+#Unqq)e8KVg?Eq7|RPTLOuDB2DtJco*-Ea|DS|OvLIQI9$8HGi@$=zvsnBIm9xv` diff --git a/Dev/Aseprite/Character/CharacterTemplate.png b/Dev/Aseprite/Character/CharacterTemplate.png new file mode 100644 index 00000000..46800057 --- /dev/null +++ b/Dev/Aseprite/Character/CharacterTemplate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aec558cfcf12109f1050ec76bad777461bf892c137247b92c8aaa380eb254ce9 +size 2686 diff --git a/Dev/Aseprite/Character/Trish.aseprite b/Dev/Aseprite/Character/Trish.aseprite new file mode 100644 index 0000000000000000000000000000000000000000..f97f49a3040576b4a7d837ce6ae90b9335903c4c GIT binary patch literal 163568 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 WXt-_y@1fa(RE$6L;r>bbaQ_R|WYA3j literal 0 HcmV?d00001 diff --git a/Dev/Aseprite/Eleri/Eleri.aseprite b/Dev/Aseprite/Eleri/Eleri.aseprite index c466d09b2d9ba95e7ee559daeac1d8dc4c0cf407..b36701ae6c068aa2652c6596b7c786178ea34841 100644 GIT binary patch delta 1931 zcmbRImUGujPNrQY6PfCGFG?^l{Q9WP5X`{9pgplyaX!fYW{1Uk93NG_diCE?LWTYCjUx^cDQq0gXB>_(ym)T$e0!k^ z<42}pZrvtOV5SMsFc_x;gE4jcx?;v2CdN_QC`phbBqXQv9c2_?+zd*X%lWp~K4;v+ z$QU;L;~z#*2S~R5y+8lZ|NjAha+>5Nj2RBzG^R)O2N*ISsOg|G`BhOhlc3aAX#i2|8^fQZ%;~ zN+}`|nMsyf{8WNaiGq-XkPw3`$ZRyyqCYnC`X1KHUR5tL@5SdhesK3Z&)xGIeB)tv zdI}K|A0~bTo7o`I#_%Zd?!N zheKhxDjv4R?}noVF>uc7=qhvJg-#1I5z;$o#lkC-=jT=10HrsZq3Xo|Z20mLMyS)FHsKUZ zHr(bmPs4^MJlFUwv9L%}3ay!)@S5cVblh;l(!D(N_QYcN*uWEZR$YQ-g`N>VG7`)` z&!(2aPu10Mp@RqX?a?jPb5$qU$KB*+PdCxijk?RIC{AV39;q0rr6SeqaZ#%RWMm_W zAWZmqckV<&%35-=s;ct67Z%O4tipTBXY2j8jw{U6+V#qGIN8PW)y5&8%}pKMi~g2< z?^Sh)3QI|iS@lpUmrR(?)8jQ-Yg1bZHHgkgS||Kx$KK&Z&XfP$@`@<$oIqF3`i&>L zylZHGS1(8BjX7wJoMtptzou*VFZH(TUhyzvX$!_P{yyrsp^4R#Ea2_+!lm|(*;LW5 zI42u9?Qmq(B}?^6z4VAOSGGqo;W11kPtn+OfkJ_(nV~k`*#GkO{XBa74b!wjAN`Xh S5>cXgfv)WTizl=^>hm4JgR!gt diff --git a/Source/ProjectEleri/DialogueSystem/DialogueWidget.cpp b/Source/ProjectEleri/DialogueSystem/DialogueWidget.cpp index 723a5b00..9b6d24e8 100644 --- a/Source/ProjectEleri/DialogueSystem/DialogueWidget.cpp +++ b/Source/ProjectEleri/DialogueSystem/DialogueWidget.cpp @@ -12,7 +12,7 @@ void UDialogueWidget::NativeOnInitialized() EleriPlayerController = Cast(UGameplayStatics::GetPlayerController(GetWorld(), 0)); } -void UDialogueWidget::HandleUiInputAction_Implementation(EEleriUIInputAction InputAction) +void UDialogueWidget::HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value) { UE_LOG(LogTemp, Warning, TEXT("")); } diff --git a/Source/ProjectEleri/DialogueSystem/DialogueWidget.h b/Source/ProjectEleri/DialogueSystem/DialogueWidget.h index b51f91e8..8be1aec1 100644 --- a/Source/ProjectEleri/DialogueSystem/DialogueWidget.h +++ b/Source/ProjectEleri/DialogueSystem/DialogueWidget.h @@ -29,7 +29,7 @@ public: bool IsTypingDialogue() const { return bIsTypingDialogue; } - virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction) override; + virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value) override; protected: UPROPERTY(EditAnywhere, BlueprintReadWrite) diff --git a/Source/ProjectEleri/Private/EleriPlayerController.cpp b/Source/ProjectEleri/Private/EleriPlayerController.cpp index f374cd09..4c20ad0f 100644 --- a/Source/ProjectEleri/Private/EleriPlayerController.cpp +++ b/Source/ProjectEleri/Private/EleriPlayerController.cpp @@ -14,6 +14,7 @@ #include "TimerManager.h" #include "ProjectEleri/UI/CalendarScreen.h" #include "ProjectEleri/UI/InteractionWidget.h" +#include "ProjectEleri/UI/MapWidget.h" #include "UI/AlchemyWidget.h" #include "UI/EleriBaseWidget.h" #include "ProjectEleri/GameObjects/BookActor.h" @@ -204,6 +205,10 @@ void AEleriPlayerController::SetupInputComponent() { PlayerEnhancedInputComponent->BindAction(CalendarAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnCalendarToggle); } + if (MapAction) + { + PlayerEnhancedInputComponent->BindAction(MapAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnMapToggle); + } if (InteractAction) { @@ -218,21 +223,27 @@ void AEleriPlayerController::SetupInputComponent() { PlayerEnhancedInputComponent->BindAction(WaterSeedAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnHarvestSeed); } - + if (UiBackAction) { PlayerEnhancedInputComponent->BindAction(UiBackAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerBackAction); } - if (UiTabLeftAction) { PlayerEnhancedInputComponent->BindAction(UiTabLeftAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerTabLeftAction); } - if (UiTabRightAction) { PlayerEnhancedInputComponent->BindAction(UiTabRightAction, ETriggerEvent::Started, this, &AEleriPlayerController::OnControllerTabRightAction); } + if (UiZoomAction) + { + PlayerEnhancedInputComponent->BindAction(UiZoomAction, ETriggerEvent::Triggered, this, &AEleriPlayerController::OnControllerZoomAction); + } + if (UiMovePointerAction) + { + PlayerEnhancedInputComponent->BindAction(UiMovePointerAction, ETriggerEvent::Triggered, this, &AEleriPlayerController::OnControllerMovePointerAction); + } //--------------------- // OBJECT REMOVAL INPUTS @@ -814,7 +825,7 @@ void AEleriPlayerController::OnControllerConfirmAction(const FInputActionValue & return; } - MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Select); + MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Select, Value); } void AEleriPlayerController::OnControllerBackAction(const FInputActionValue &Value) @@ -825,7 +836,7 @@ void AEleriPlayerController::OnControllerBackAction(const FInputActionValue &Val return; } - MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Back); + MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Back, Value); } void AEleriPlayerController::OnControllerTabLeftAction(const FInputActionValue &Value) @@ -835,7 +846,7 @@ void AEleriPlayerController::OnControllerTabLeftAction(const FInputActionValue & return; } - MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::TabLeft); + MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::TabLeft, Value); } void AEleriPlayerController::OnControllerTabRightAction(const FInputActionValue &Value) @@ -845,7 +856,27 @@ void AEleriPlayerController::OnControllerTabRightAction(const FInputActionValue return; } - MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::TabRight); + MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::TabRight, Value); +} + +void AEleriPlayerController::OnControllerZoomAction(const FInputActionValue& Value) +{ + if (MainGameUI->GetWidgetStack().IsEmpty()) + { + return; + } + + MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::Zoom, Value); +} + +void AEleriPlayerController::OnControllerMovePointerAction(const FInputActionValue& Value) +{ + if (MainGameUI->GetWidgetStack().IsEmpty()) + { + return; + } + + MainGameUI->GetWidgetStack()[MainGameUI->GetWidgetStack().Num() - 1]->HandleUiInputAction(EEleriUIInputAction::MovePointer, Value); } // UI BIND @@ -904,6 +935,14 @@ void AEleriPlayerController::OnCalendarToggle(const FInputActionValue &Value) OpenCalendarWindow(); } +void AEleriPlayerController::OnMapToggle(const FInputActionValue& Value) +{ + if (Paused || IsDialogueOpen() || (IsInMenu() && !MainGameUI->IsCalendarOpen()) || !MoveActorsComp || MoveActorsComp->IsRemovingMode()) + return; + + OpenMapWindow(); +} + void AEleriPlayerController::OnBookNextPage(const FInputActionValue &Value) { if (!IsValid(BookOfAlchemyManager)) @@ -1132,6 +1171,12 @@ UCalendarScreen* AEleriPlayerController::OpenCalendarWindow() return MainGameUI->GetCalendarWidget(); } +UEleriBaseWidget* AEleriPlayerController::OpenMapWindow() +{ + MainGameUI->OpenWidget(MainGameUI->GetMapWidget()); + return MainGameUI->GetMapWidget(); +} + UEleriBaseWidget* AEleriPlayerController::OpenBazarWindow() { MainGameUI->OpenWidget(MainGameUI->GetBazarWidget()); return MainGameUI->GetBazarWidget(); diff --git a/Source/ProjectEleri/Private/UI/EleriBaseWidget.cpp b/Source/ProjectEleri/Private/UI/EleriBaseWidget.cpp index 796b2c1e..0d119792 100644 --- a/Source/ProjectEleri/Private/UI/EleriBaseWidget.cpp +++ b/Source/ProjectEleri/Private/UI/EleriBaseWidget.cpp @@ -12,7 +12,7 @@ void UEleriBaseWidget::ToggleWidget_Implementation(bool bActive) { SetVisibility(bActive? ESlateVisibility::Visible : ESlateVisibility::Hidden); } -void UEleriBaseWidget::HandleUiInputAction_Implementation(EEleriUIInputAction InputAction) +void UEleriBaseWidget::HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value) { if (InputAction == EEleriUIInputAction::Back) { diff --git a/Source/ProjectEleri/Private/UI/MainGameWidget.cpp b/Source/ProjectEleri/Private/UI/MainGameWidget.cpp index 11ba1897..4b8eb10f 100644 --- a/Source/ProjectEleri/Private/UI/MainGameWidget.cpp +++ b/Source/ProjectEleri/Private/UI/MainGameWidget.cpp @@ -154,6 +154,11 @@ bool UMainGameWidget::IsBazarWidgetOpen() const return WidgetStack.Contains(Bazar_Widget); } +bool UMainGameWidget::IsMapWidgetOpen() const +{ + return WidgetStack.Contains(Map_Widget); +} + bool UMainGameWidget::IsScreenActionInputDisplayWidgetOpen() const { return ScreenActionInputDisplay_Widget->GetVisibility() == ESlateVisibility::Visible; diff --git a/Source/ProjectEleri/Public/EleriPlayerController.h b/Source/ProjectEleri/Public/EleriPlayerController.h index 3846a494..facf957a 100644 --- a/Source/ProjectEleri/Public/EleriPlayerController.h +++ b/Source/ProjectEleri/Public/EleriPlayerController.h @@ -17,6 +17,7 @@ #include "EleriPlayerController.generated.h" +class UMapWidget; class ASeedBedActor; class UItemDataAsset; @@ -197,6 +198,8 @@ public: UInputAction* OrdersAction; UPROPERTY(EditDefaultsOnly, Category = "UI Input") UInputAction* CalendarAction; + UPROPERTY(EditDefaultsOnly, Category = "UI Input") + UInputAction* MapAction; // UI INPUT ACTIONS UPROPERTY(EditDefaultsOnly, Category = "UI Input Gamepad") @@ -218,6 +221,10 @@ public: UInputAction* UiTabLeftAction; UPROPERTY(EditDefaultsOnly, Category = "UI Input Gamepad") UInputAction* UiTabRightAction; + UPROPERTY(EditDefaultsOnly, Category = "UI Input Gamepad") + UInputAction* UiZoomAction; + UPROPERTY(EditDefaultsOnly, Category = "UI Input Gamepad") + UInputAction* UiMovePointerAction; UPROPERTY(BlueprintAssignable) FOnUiButtonPressed OnUiButtonPressed; @@ -318,12 +325,15 @@ public: void OnControllerBackAction(const FInputActionValue& Value); void OnControllerTabLeftAction(const FInputActionValue& Value); void OnControllerTabRightAction(const FInputActionValue& Value); + void OnControllerZoomAction(const FInputActionValue& Value); + void OnControllerMovePointerAction(const FInputActionValue& Value); void OnMainMenuToggle(const FInputActionValue& Value); void OnInventoryToggle(const FInputActionValue& Value); void OnBookToggle(const FInputActionValue& Value); void OnOrdersToggle(const FInputActionValue& Value); void OnCalendarToggle(const FInputActionValue& Value); + void OnMapToggle(const FInputActionValue& Value); void OnBookNextPage(const FInputActionValue& Value); void OnBookPreviousPage(const FInputActionValue& Value); @@ -366,6 +376,8 @@ public: UFUNCTION(BlueprintCallable) UCalendarScreen* OpenCalendarWindow(); + UFUNCTION(BlueprintCallable) + UEleriBaseWidget* OpenMapWindow(); // Bazar UFUNCTION(BlueprintCallable) diff --git a/Source/ProjectEleri/Public/UI/EleriBaseWidget.h b/Source/ProjectEleri/Public/UI/EleriBaseWidget.h index 36534d4e..860fabed 100644 --- a/Source/ProjectEleri/Public/UI/EleriBaseWidget.h +++ b/Source/ProjectEleri/Public/UI/EleriBaseWidget.h @@ -15,7 +15,9 @@ enum class EEleriUIInputAction : uint8 Select, Back, TabLeft, - TabRight + TabRight, + Zoom, + MovePointer }; USTRUCT(BlueprintType) @@ -67,8 +69,8 @@ public: virtual void ToggleWidget_Implementation(bool bActive); UFUNCTION(BlueprintNativeEvent, BlueprintCallable) - void HandleUiInputAction(EEleriUIInputAction InputAction); - virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction); + void HandleUiInputAction(EEleriUIInputAction InputAction, FInputActionValue Value); + virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value); UFUNCTION(BlueprintCallable, Category = "Eleri|UI") void DisplayInputPrompts(const FName PromptMapKey = "Default"); diff --git a/Source/ProjectEleri/Public/UI/MainGameWidget.h b/Source/ProjectEleri/Public/UI/MainGameWidget.h index 2f2b7f81..500e8c8a 100644 --- a/Source/ProjectEleri/Public/UI/MainGameWidget.h +++ b/Source/ProjectEleri/Public/UI/MainGameWidget.h @@ -150,6 +150,12 @@ public: UFUNCTION(BlueprintCallable, BlueprintPure) bool IsBazarWidgetOpen() const; + // Map widget + UFUNCTION(BlueprintCallable, BlueprintPure) + UEleriBaseWidget* GetMapWidget() { return Map_Widget; } + UFUNCTION(BlueprintCallable, BlueprintPure) + bool IsMapWidgetOpen() const; + UFUNCTION(BlueprintCallable, BlueprintPure) UScreenActionInputDisplay* GetScreenActionInputDisplayWidget() { return ScreenActionInputDisplay_Widget; } UFUNCTION(BlueprintCallable, BlueprintPure) @@ -201,10 +207,14 @@ protected: UPROPERTY(meta = (BindWidget)) UCalendarScreen *Calendar_Widget; - // Calendar + // Bazaar UPROPERTY(meta = (BindWidget)) UEleriBaseWidget* Bazar_Widget; + // Bazaar + UPROPERTY(meta = (BindWidget)) + UEleriBaseWidget* Map_Widget; + UPROPERTY(meta = (BindWidget)) UScreenActionInputDisplay* ScreenActionInputDisplay_Widget; diff --git a/Source/ProjectEleri/UI/MapWidget.cpp b/Source/ProjectEleri/UI/MapWidget.cpp new file mode 100644 index 00000000..93538f42 --- /dev/null +++ b/Source/ProjectEleri/UI/MapWidget.cpp @@ -0,0 +1,1352 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "MapWidget.h" + +#include "Blueprint/WidgetTree.h" +#include "Components/Border.h" +#include "Components/CanvasPanel.h" +#include "Components/CanvasPanelSlot.h" +#include "Components/Image.h" +#include "Components/Overlay.h" +#include "Components/OverlaySlot.h" +#include "Components/PanelWidget.h" +#include "Engine/AssetManager.h" +#include "Engine/StreamableManager.h" +#include "Engine/Texture2D.h" +#include "GameFramework/Pawn.h" +#include "GameObjects/OrthoMapCaptureActor.h" +#include "InputCoreTypes.h" +#include "Kismet/GameplayStatics.h" +#include "Styling/SlateBrush.h" + +DEFINE_LOG_CATEGORY_STATIC(LogEleriMapWidget, Log, All); + +namespace +{ + /** Name of one piece of the bake, with the four parts of the name that vary filled in. The pattern is a + * setting rather than the name being built here, because the bake names its assets and this only has to + * agree with it. */ + FString MakePieceAssetName(const FString& Pattern, const FString& Map, int32 Grid, int32 Column, int32 Row) + { + FString AssetName = Pattern; + AssetName.ReplaceInline(TEXT("{Map}"), *Map); + AssetName.ReplaceInline(TEXT("{Grid}"), *FString::FromInt(Grid)); + AssetName.ReplaceInline(TEXT("{Col}"), *FString::FromInt(Column)); + AssetName.ReplaceInline(TEXT("{Row}"), *FString::FromInt(Row)); + + return AssetName; + } + + /** Where one piece is asked for, as a long object path. A texture's object name is its asset name, which + * is what makes this a name rather than a lookup. */ + FString MakePieceObjectPath(const FString& Folder, const FString& Pattern, const FString& Map, int32 Grid, int32 Column, int32 Row) + { + const FString AssetName = MakePieceAssetName(Pattern, Map, Grid, Column, Row); + + return FString::Printf(TEXT("%s/%s.%s"), *Folder, *AssetName, *AssetName); + } + + /** The mirroring a brush needs for a bake made with the capture actor's flip flags. Only a piece's own + * pixels are affected: where the piece sits in the grid is the mapping's business, and the mapping is + * the mapping whether the picture is mirrored in it or not. */ + ESlateBrushMirrorType::Type GetBrushMirroring(EMapPieceMirroring Mirroring) + { + switch (Mirroring) + { + case EMapPieceMirroring::Horizontal: + return ESlateBrushMirrorType::Horizontal; + + case EMapPieceMirroring::Vertical: + return ESlateBrushMirrorType::Vertical; + + case EMapPieceMirroring::Both: + return ESlateBrushMirrorType::Both; + + case EMapPieceMirroring::None: + default: + return ESlateBrushMirrorType::NoMirror; + } + } + + /** A brush that draws as a solid rounded rectangle, which is what the map's own rectangle is drawn with: no + * texture is needed for it, so a map is a map with nothing pointed at it. */ + FSlateBrush MakeRoundedBoxBrush(const FLinearColor& FillColor, const FLinearColor& OutlineColor, float CornerRadius, float OutlineWidth) + { + FSlateBrush Brush; + Brush.DrawAs = ESlateBrushDrawType::RoundedBox; + Brush.TintColor = FSlateColor(FillColor); + Brush.OutlineSettings.CornerRadii = FVector4(CornerRadius, CornerRadius, CornerRadius, CornerRadius); + Brush.OutlineSettings.Color = FSlateColor(OutlineColor); + Brush.OutlineSettings.Width = OutlineWidth; + + return Brush; + } + + /** Size the map is laid out at, in design units, before any zoom: the one place the grid and a piece's + * size are turned into a length. */ + float GetMapSizeFor(int32 GridSize, float PieceCellSize) + { + return FMath::Max(1, GridSize) * FMath::Max(1.f, PieceCellSize); + } +} + +UMapWidget::UMapWidget() +{ + // This widget ticks natively, which needs nothing set here: a widget whose class is not a blueprint generated + // class ticks under the engine's own Auto frequency, which is what the marker following the player runs on. + // There is no blueprint tick behind the map, so nothing else would keep it up to date. + + // The bake this project has: a map named after the level it was baked from, cut into 8 by 8 pieces, saved + // beside it. Every part of that is a setting, so another bake is a setting and not a code change. + MapName = TEXT("Persistent_GameWorld"); + PiecePackageFolder = TEXT("/Game/Maps/Generated"); + PieceAssetPattern = TEXT("WorldMap_{Map}_Piece{Grid}x{Grid}_{Col}_{Row}"); + GridSize = 8; + PieceLayout = EMapPieceLayout::SampledFromMapping; + PieceMirroring = EMapPieceMirroring::None; + bPreloadPiecesOnConstruct = false; + + // Zoom is relative to the map filling the shorter side of the view, so these are multiples of the whole map + // being visible: in to eight times, out to half, and a quarter larger per notch of the wheel. + DefaultZoom = 1.f; + MinZoom = 0.5f; + MaxZoom = 8.f; + ZoomStep = 1.25f; + MoveStep = 5.f; + bResetViewWhenShown = true; + PieceCellSize = 512.f; + + bFollowPlayerByDefault = true; + bCenterOnPlayerByDefault = true; + MarkerSize = 32.f; + + MarkerFacingOffset = 0.f; + + MapBackgroundColor = FLinearColor(0.03f, 0.04f, 0.05f, 0.9f); + + Zoom = 1.f; + bFollowPlayer = true; + bCenterOnPlayer = true; + bPanning = false; + + bMapReady = false; + bPiecesRequested = false; + bMarkerPlaced = false; + + // Nothing has been turned yet, and there is no map yet to say how the map lies in the world. + MarkerAngle = 0.f; + bMarkerAngleApplied = false; + + // Nothing has been said yet about the design having no marker in it. + bMissingMarkerReported = false; + MapUVPerWorldX = FVector2D::ZeroVector; + MapUVPerWorldY = FVector2D::ZeroVector; + bMapAxesResolved = false; + + bMapTransformApplied = false; + PanPointerIndex = INDEX_NONE; + LastPanScreenPosition = FVector2D::ZeroVector; + + // The view starts on the centre of the map, and the marker on the centre too, so that a map opened before + // the player has been found is a map rather than an empty view. + FocusPoint = FVector2D(GetMapDesignSize() * 0.5f); + MarkerMapUV = FVector2D(0.5f, 0.5f); +} + +TSharedRef UMapWidget::RebuildWidget() +{ + // A user widget's tree is made when it is initialized, and not before: RebuildWidget is the first call + // that is guaranteed for a widget with no blueprint generated class behind it, and the tree has to exist + // before the call below takes the root of it. + if (!WidgetTree) + { + Initialize(); + } + + EnsureWidgetTree(); + + return Super::RebuildWidget(); +} + +void UMapWidget::EnsureWidgetTree() +{ + // Called from RebuildWidget and again from everything that wants the map on screen, so the work is only + // done when there is something to do: a standing tree is the whole test. The marker is not part of it, + // because the design's marker is not the map's to make. + if (MapRoot && MapViewport && MapLayer && PieceGrid && Interaction) + { + return; + } + + if (!WidgetTree) + { + UE_LOG(LogEleriMapWidget, Error, TEXT("The map has no widget tree to build itself in, so there is nothing it can be drawn in.")); + + return; + } + + const int32 Grid = FMath::Max(1, GridSize); + const float MapSize = GetMapDesignSize(); + const float CellSize = FMath::Max(1.f, PieceCellSize); + + // Each widget is a local until the whole tree is standing, so that a build which cannot finish leaves the + // widget as it was rather than half built. The tree names them, because nothing outside the tree looks + // them up by name. + UOverlay* NewRoot = WidgetTree->ConstructWidget(UOverlay::StaticClass(), NAME_None); + UOverlay* NewViewport = WidgetTree->ConstructWidget(UOverlay::StaticClass(), NAME_None); + UCanvasPanel* NewLayer = WidgetTree->ConstructWidget(UCanvasPanel::StaticClass(), NAME_None); + + // The view the map is looked at through: it fills whatever the map is given, clips the map to itself, and + // is the geometry every zoom is measured against. + NewViewport->SetClipping(EWidgetClipping::ClipToBounds); + + if (UOverlaySlot* ViewportSlot = NewRoot->AddChildToOverlay(NewViewport)) + { + ViewportSlot->SetHorizontalAlignment(HAlign_Fill); + ViewportSlot->SetVerticalAlignment(VAlign_Fill); + } + + // The layer the map is drawn on. One transform on this one widget moves and scales the whole map, which is + // why zooming is a single write and not 64 of them. + NewLayer->SetRenderTransformPivot(FVector2D(0.5f, 0.5f)); + + if (UOverlaySlot* LayerSlot = NewViewport->AddChildToOverlay(NewLayer)) + { + LayerSlot->SetHorizontalAlignment(HAlign_Fill); + LayerSlot->SetVerticalAlignment(VAlign_Fill); + } + + // The map's own rectangle, under everything: a piece that has not arrived yet is then a pale part of the + // map rather than a hole through it. + UBorder* NewBackground = WidgetTree->ConstructWidget(UBorder::StaticClass(), NAME_None); + NewBackground->SetVisibility(ESlateVisibility::HitTestInvisible); + NewBackground->SetBrush(MakeRoundedBoxBrush(MapBackgroundColor, MapBackgroundColor, 0.f, 0.f)); + + if (UCanvasPanelSlot* BackgroundSlot = NewLayer->AddChildToCanvas(NewBackground)) + { + BackgroundSlot->SetAnchors(FAnchors(0.f, 0.f, 0.f, 0.f)); + BackgroundSlot->SetAutoSize(false); + BackgroundSlot->SetPosition(FVector2D::ZeroVector); + BackgroundSlot->SetSize(FVector2D(MapSize, MapSize)); + BackgroundSlot->SetZOrder(0); + } + + // The pieces: one image per cell, in filename order until the layout says where each of them belongs. + UCanvasPanel* NewPieceGrid = WidgetTree->ConstructWidget(UCanvasPanel::StaticClass(), NAME_None); + NewPieceGrid->SetVisibility(ESlateVisibility::HitTestInvisible); + + if (UCanvasPanelSlot* GridSlot = NewLayer->AddChildToCanvas(NewPieceGrid)) + { + GridSlot->SetAnchors(FAnchors(0.f, 0.f, 0.f, 0.f)); + GridSlot->SetAutoSize(false); + GridSlot->SetPosition(FVector2D::ZeroVector); + GridSlot->SetSize(FVector2D(MapSize, MapSize)); + GridSlot->SetZOrder(1); + } + + TArray> NewPieceImages; + NewPieceImages.Reserve(Grid * Grid); + + for (int32 Index = 0; Index < Grid * Grid; ++Index) + { + const int32 Column = Index % Grid; + const int32 Row = Index / Grid; + + UImage* PieceImage = WidgetTree->ConstructWidget(UImage::StaticClass(), NAME_None); + PieceImage->SetVisibility(ESlateVisibility::HitTestInvisible); + + if (UCanvasPanelSlot* PieceSlot = NewPieceGrid->AddChildToCanvas(PieceImage)) + { + PieceSlot->SetAnchors(FAnchors(0.f, 0.f, 0.f, 0.f)); + PieceSlot->SetAutoSize(false); + PieceSlot->SetSize(FVector2D(CellSize, CellSize)); + PieceSlot->SetPosition(FVector2D(Column * CellSize, Row * CellSize)); + } + + NewPieceImages.Add(PieceImage); + } + + // The player's marker is not built here: it is the widget named PlayerMarker in the design, and it is taken + // onto this layer by EnsureMarkerOnMap once the layer is standing. A marker built in code could only ever + // be the one thing this file knew how to draw. + + // Everything the map listens for arrives through this last widget: it lies over the whole view, so a drag + // on the map is a drag on the map and never a click on the level behind it. Nothing is drawn for it. + FSlateBrush NoBrush; + NoBrush.DrawAs = ESlateBrushDrawType::NoDrawType; + + UBorder* NewInteraction = WidgetTree->ConstructWidget(UBorder::StaticClass(), NAME_None); + NewInteraction->SetBrush(NoBrush); + + if (UOverlaySlot* InteractionSlot = NewRoot->AddChildToOverlay(NewInteraction)) + { + InteractionSlot->SetHorizontalAlignment(HAlign_Fill); + InteractionSlot->SetVerticalAlignment(VAlign_Fill); + } + + // Where the tree hangs. A blueprint subclass of this widget can hold nothing at all, and then this tree is + // the root; if it holds a panel, this tree is added to it. + UWidget* ExistingRoot = WidgetTree->RootWidget; + if (!ExistingRoot) + { + WidgetTree->RootWidget = NewRoot; + } + else if (UPanelWidget* PanelRoot = Cast(ExistingRoot)) + { + PanelRoot->AddChild(NewRoot); + } + else + { + UE_LOG(LogEleriMapWidget, Error, + TEXT("The %s this map was made in has a single widget as its root, and there is nowhere to put the map: the root needs to be empty, or a panel this map can be added to."), + *GetClass()->GetName()); + + return; + } + + MapRoot = NewRoot; + MapViewport = NewViewport; + MapLayer = NewLayer; + MapBackground = NewBackground; + PieceGrid = NewPieceGrid; + PieceImages = MoveTemp(NewPieceImages); + Interaction = NewInteraction; + + // The tree is standing but has not been laid out, so the transform is left to the next tick, which is the + // first one that can be measured against a view. + bMapTransformApplied = false; + + // A tree built again has a marker with nothing written on it, so the angle is applied once more rather than + // being taken to be already there. + bMarkerAngleApplied = false; + + UE_LOG(LogEleriMapWidget, Verbose, TEXT("The map's own tree is up: %d by %d cells of %.0f units."), Grid, Grid, CellSize); +} + +void UMapWidget::EnsureMapReady() +{ + EnsureWidgetTree(); + + if (!bMapReady) + { + if (!ResolveMapLayout()) + { + // The reason has been reported. The bake is left unresolved so that a later show tries again: a map + // opened before the world is up is worth retrying rather than worth giving up on. + return; + } + + bMapReady = true; + } + + RequestPieceLoads(); +} + +bool UMapWidget::ResolveMapLayout() +{ + // The capture actor is what says where a piece of the world lands in the baked map, and it is what the + // player marker is placed through, so the lattice goes through it too whenever it is there to be asked. + if (!IsValid(CaptureActor)) + { + CaptureActor = Cast(UGameplayStatics::GetActorOfClass(this, AOrthoMapCaptureActor::StaticClass())); + } + + bool bSampled = false; + + if (PieceLayout == EMapPieceLayout::SampledFromMapping) + { + if (CaptureActor) + { + FText Reason; + if (TrySamplePieceCells(Reason)) + { + bSampled = true; + } + else + { + UE_LOG(LogEleriMapWidget, Warning, + TEXT("%s The pieces are being placed by their filenames instead: the piece named column C row R goes in the cell C across and R down."), + *Reason.ToString()); + } + } + else + { + UE_LOG(LogEleriMapWidget, Warning, + TEXT("There is no %s in this world, so the pieces cannot be placed through the map's own world to map mapping, and the player cannot be marked on the map. The pieces are being placed by their filenames instead: the piece named column C row R goes in the cell C across and R down."), + *AOrthoMapCaptureActor::StaticClass()->GetName()); + } + } + + if (!bSampled) + { + FillPieceCellsFromFilenames(); + } + + // A mirrored map is mirrored inside its cells as well as in the layout of them, and whether the pieces + // carry that inside themselves is a property of how they were baked rather than of the mapping. Nothing + // here can ask, so a bake that is mirrored is worth one line saying so. + if (CaptureActor && PieceMirroring == EMapPieceMirroring::None && (CaptureActor->bFlipU || CaptureActor->bFlipV)) + { + UE_LOG(LogEleriMapWidget, Warning, + TEXT("The capture actor in this world has bFlipU=%d and bFlipV=%d, so the baked map is mirrored. The pieces are being drawn as their assets have them: if the map reads mirrored from cell to cell, PieceMirroring is what turns them."), + CaptureActor->bFlipU ? 1 : 0, CaptureActor->bFlipV ? 1 : 0); + } + + // How the baked map lies in the world, which is the whole of what the marker is turned by. Taken here rather + // than with the marker because it comes from the same mapping the pieces were placed through, and because a + // resolve is the moment it is worth saying once. + ResolveMapAxes(); + + if (bMapAxesResolved) + { + UE_LOG(LogEleriMapWidget, Verbose, + TEXT("The map's own axes through the world: one world unit along X moves %s of the map and one along Y moves %s, so a facing is turned into a direction on the map rather than guessed at."), + *MapUVPerWorldX.ToString(), *MapUVPerWorldY.ToString()); + } + + // Every image is put where its piece belongs, which is the one place the layout and the widgets that draw + // it are brought together. + const float CellSize = FMath::Max(1.f, PieceCellSize); + + for (int32 Index = 0; Index < PieceCells.Num(); ++Index) + { + if (!PieceImages.IsValidIndex(Index) || !PieceImages[Index]) + { + continue; + } + + if (UCanvasPanelSlot* PieceSlot = Cast(PieceImages[Index]->Slot)) + { + const FEleriMapPieceCell& Cell = PieceCells[Index]; + PieceSlot->SetPosition(FVector2D(Cell.CellColumn * CellSize, Cell.CellRow * CellSize)); + } + } + + UE_LOG(LogEleriMapWidget, Display, TEXT("The map is resolved: %d pieces of %s from %s, placed %s."), + PieceCells.Num(), *MapName, *PiecePackageFolder, + bSampled ? TEXT("through the capture actor's own mapping") : TEXT("by their filenames")); + + return true; +} + +void UMapWidget::ResolveMapAxes() +{ + MapUVPerWorldX = FVector2D::ZeroVector; + MapUVPerWorldY = FVector2D::ZeroVector; + bMapAxesResolved = false; + + if (!CaptureActor) + { + return; + } + + const FBox Region = CaptureActor->WorldMapBounds; + if (!Region.IsValid) + { + return; + } + + // Three points put through the same mapping the pieces and the marker are placed through: the centre of the + // region, and one a small step from it along each of the world's own axes. What a step along X and a step + // along Y do to the map's UV is the whole of how the map lies in the world, and taking it from the mapping + // rather than assuming it is what makes a bake that was captured turned, or mirrored with the actor's flip + // flags, come out right here: whatever the mapping does to a step is what that step is. + const FVector Centre = Region.GetCenter(); + const double Step = FMath::Max(1.0, (Region.Max.X - Region.Min.X) * 0.001); + + const FVector2D CentreUV = CaptureActor->WorldToBakedMapUV(Centre); + + // One world unit along each of the world's own axes, as the map's own UV: the two numbers every turn + // between the world and the picture is read from. + MapUVPerWorldX = (CaptureActor->WorldToBakedMapUV(Centre + FVector(Step, 0.0, 0.0)) - CentreUV) / Step; + MapUVPerWorldY = (CaptureActor->WorldToBakedMapUV(Centre + FVector(0.0, Step, 0.0)) - CentreUV) / Step; + + // A mapping that moves the map nowhere in one of the two directions has nothing to say about which way the + // map lies, and a marker turned by it would be turned by noise. + // if (MapUVPerWorldX.IsNearlyZero() || MapUVPerWorldY.IsNearlyZero()) + // { + // MapUVPerWorldX = FVector2D::ZeroVector; + // MapUVPerWorldY = FVector2D::ZeroVector; + // + // return; + // } + + bMapAxesResolved = true; +} + +void UMapWidget::FillPieceCellsFromFilenames() +{ + const int32 Grid = FMath::Max(1, GridSize); + + PieceCells.Reset(); + PieceCells.Reserve(Grid * Grid); + + // Rows of columns, which is the order the piece names run in and the order the images were built in, so + // entry N of this list and image N are always the same piece. + for (int32 Row = 0; Row < Grid; ++Row) + { + for (int32 Column = 0; Column < Grid; ++Column) + { + FEleriMapPieceCell& Cell = PieceCells.AddDefaulted_GetRef(); + Cell.SourceColumn = Column; + Cell.SourceRow = Row; + Cell.CellColumn = Column; + Cell.CellRow = Row; + Cell.ObjectPath = MakePieceObjectPath(PiecePackageFolder, PieceAssetPattern, MapName, Grid, Column, Row); + Cell.Texture = nullptr; + } + } +} + +FBox UMapWidget::GetPieceWorldBox(int32 SourceColumn, int32 SourceRow) const +{ + if (!CaptureActor) + { + return FBox(ForceInit); + } + + const FBox Region = CaptureActor->WorldMapBounds; + if (!Region.IsValid) + { + return FBox(ForceInit); + } + + // Columns along X and rows along Y from the region's own min corner, carrying the region's own Z, which is + // the way round the bake cuts its pieces. It is the one thing about the layout that is assumed rather than + // asked, because a box to put through the mapping has to come from somewhere. + const int32 Grid = FMath::Max(1, GridSize); + const double PieceSide = (Region.Max.X - Region.Min.X) / static_cast(Grid); + + const FVector PieceMin( + Region.Min.X + PieceSide * static_cast(SourceColumn), + Region.Min.Y + PieceSide * static_cast(SourceRow), + Region.Min.Z); + + return FBox(PieceMin, FVector(PieceMin.X + PieceSide, PieceMin.Y + PieceSide, Region.Max.Z)); +} + +bool UMapWidget::GetPieceCellFromMapping(int32 SourceColumn, int32 SourceRow, FIntPoint& OutCell) const +{ + if (!CaptureActor) + { + return false; + } + + const FBox PieceBox = GetPieceWorldBox(SourceColumn, SourceRow); + if (!PieceBox.IsValid) + { + return false; + } + + // The actor's mapping is the mapping the baked map was made with, so this is the answer the bake would give + // about where this piece's own box landed in the picture. + const FVector2D MapUV = CaptureActor->WorldToBakedMapUV(PieceBox.GetCenter()); + if (MapUV.X < 0.0 || MapUV.X > 1.0 || MapUV.Y < 0.0 || MapUV.Y > 1.0) + { + return false; + } + + // A piece's centre is inside the cell that piece fills, so the cell is what the centre floors into: an + // unmirrored bake puts piece column C in cell C, and a mirrored one puts it at the far end of the grid. + // Both come out of the mapping, so neither is assumed. + const int32 Grid = FMath::Max(1, GridSize); + OutCell.X = FMath::Clamp(FMath::FloorToInt(MapUV.X * Grid), 0, Grid - 1); + OutCell.Y = FMath::Clamp(FMath::FloorToInt(MapUV.Y * Grid), 0, Grid - 1); + + return true; +} + +bool UMapWidget::TrySamplePieceCells(FText& OutReason) +{ + if (!CaptureActor) + { + OutReason = NSLOCTEXT("EleriMapWidget", "NoCaptureActorToAsk", "There is no capture actor to ask where the pieces land."); + + return false; + } + + const FBox Region = CaptureActor->WorldMapBounds; + if (!Region.IsValid) + { + OutReason = NSLOCTEXT("EleriMapWidget", "NoWorldMapRegion", "The capture actor in this world has no world map region set, so there is nothing to say where a piece of the world lands in the map."); + + return false; + } + + const double RegionSide = Region.Max.X - Region.Min.X; + const double RegionHeight = Region.Max.Y - Region.Min.Y; + + // The bake cuts a square region into pieces, so a region that is not square is a region these pieces were + // not cut out of, however the pieces happen to be named. + if (RegionSide <= 0.0 || RegionHeight <= 0.0 + || !FMath::IsNearlyEqual(RegionSide, RegionHeight, FMath::Max(RegionSide, RegionHeight) * 1.e-4)) + { + OutReason = FText::Format( + NSLOCTEXT("EleriMapWidget", "RegionNotSquare", + "The capture actor's world map region is {0} by {1} world units, and the pieces were cut out of a square one."), + FText::AsNumber(FMath::RoundToInt(RegionSide)), FText::AsNumber(FMath::RoundToInt(RegionHeight))); + + return false; + } + + const int32 Grid = FMath::Max(1, GridSize); + const double PieceSide = RegionSide / static_cast(Grid); + + // The actor's own tile size against a piece of the region. The sampling below does not need it, but a piece + // that is not a whole number of the actor's tiles means these pieces are of a different grid than the actor + // describes now, which is worth knowing before the map is read. + const double TileWorldSize = CaptureActor->GetTileWorldSize(); + + if (TileWorldSize > 0.0 && PieceSide > TileWorldSize) + { + const double TilesPerPiece = PieceSide / TileWorldSize; + if (!FMath::IsNearlyEqual(TilesPerPiece, FMath::RoundToDouble(TilesPerPiece), 1.e-3)) + { + UE_LOG(LogEleriMapWidget, Warning, + TEXT("A piece covers %.2f of the capture actor's tiles (a tile is %.0f units over and a piece is %.0f), so the pieces were baked from a grid the actor no longer describes."), + TilesPerPiece, TileWorldSize, PieceSide); + } + } + + TArray Sampled; + Sampled.SetNum(Grid * Grid); + + // Every cell has to be claimed exactly once: a grid of pieces and a map are the same map, so anything else + // is a bake this widget should not draw as though it were one. + TSet UsedCells; + + for (int32 Row = 0; Row < Grid; ++Row) + { + for (int32 Column = 0; Column < Grid; ++Column) + { + FIntPoint Cell; + if (!GetPieceCellFromMapping(Column, Row, Cell)) + { + const FVector2D MapUV = CaptureActor->WorldToBakedMapUV(GetPieceWorldBox(Column, Row).GetCenter()); + + OutReason = FText::Format( + NSLOCTEXT("EleriMapWidget", "PieceOffTheMap", + "The centre of piece column {0} row {1} maps to {2} of the baked map, which is outside it: the region the actor describes and the pieces are not of the same map."), + FText::AsNumber(Column), FText::AsNumber(Row), FText::FromString(MapUV.ToString())); + + return false; + } + + const int32 CellIndex = Cell.Y * Grid + Cell.X; + if (UsedCells.Contains(CellIndex)) + { + OutReason = FText::Format( + NSLOCTEXT("EleriMapWidget", "TwoPiecesOneCell", + "Piece column {0} row {1} maps to cell column {2} row {3}, which another piece has already claimed, so the mapping and a {4} by {4} grid of pieces do not describe the same map."), + FText::AsNumber(Column), FText::AsNumber(Row), FText::AsNumber(Cell.X), FText::AsNumber(Cell.Y), FText::AsNumber(Grid)); + + return false; + } + + UsedCells.Add(CellIndex); + + FEleriMapPieceCell& SampledCell = Sampled[Row * Grid + Column]; + SampledCell.SourceColumn = Column; + SampledCell.SourceRow = Row; + SampledCell.CellColumn = Cell.X; + SampledCell.CellRow = Cell.Y; + SampledCell.ObjectPath = MakePieceObjectPath(PiecePackageFolder, PieceAssetPattern, MapName, Grid, Column, Row); + SampledCell.Texture = nullptr; + } + } + + PieceCells = MoveTemp(Sampled); + + return true; +} + +void UMapWidget::RequestPieceLoads() +{ + if (bPiecesRequested || PieceCells.IsEmpty()) + { + return; + } + + bPiecesRequested = true; + + TArray PiecePaths; + PiecePaths.Reserve(PieceCells.Num()); + + for (const FEleriMapPieceCell& Cell : PieceCells) + { + PiecePaths.Add(FSoftObjectPath(Cell.ObjectPath)); + } + + // One batched request rather than 64 of them: the pieces are wanted together, and the streamable manager is + // what knows how to spread them over frames instead of stalling the frame that opened the map. + PieceStreamHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad( + PiecePaths, + FStreamableDelegate::CreateUObject(this, &UMapWidget::ApplyLoadedPieces), + FStreamableManager::AsyncLoadHighPriority); + + UE_LOG(LogEleriMapWidget, Display, TEXT("Asked for %d pieces of %s from %s."), PiecePaths.Num(), *MapName, *PiecePackageFolder); +} + +void UMapWidget::ApplyLoadedPieces() +{ + int32 Arrived = 0; + TArray Missing; + + for (int32 Index = 0; Index < PieceCells.Num(); ++Index) + { + FEleriMapPieceCell& Cell = PieceCells[Index]; + + // Resolved from the path rather than taken from the load's own list of what it brought in, so that a + // piece something else has already loaded counts as arrived just as much as one this request fetched. + Cell.Texture = Cast(FSoftObjectPath(Cell.ObjectPath).ResolveObject()); + + if (Cell.Texture && PieceImages.IsValidIndex(Index) && PieceImages[Index]) + { + ApplyPieceBrush(PieceImages[Index], Cell.Texture); + ++Arrived; + + continue; + } + + // Only the first few are worth naming: a bake that is missing altogether would otherwise fill the log. + if (Missing.Num() < 8) + { + Missing.Add(Cell.ObjectPath); + } + } + + if (PieceCells.Num() > 0 && Arrived == PieceCells.Num()) + { + UE_LOG(LogEleriMapWidget, Display, TEXT("All %d pieces of %s are up."), Arrived, *MapName); + + return; + } + + UE_LOG(LogEleriMapWidget, Warning, TEXT("%d of %d pieces of %s did not arrive from %s: %s"), + PieceCells.Num() - Arrived, PieceCells.Num(), *MapName, *PiecePackageFolder, *FString::Join(Missing, TEXT(", "))); +} + +void UMapWidget::ApplyPieceBrush(UImage* PieceImage, UTexture2D* PieceTexture) const +{ + if (!PieceImage || !PieceTexture) + { + return; + } + + FSlateBrush Brush; + Brush.SetResourceObject(PieceTexture); + Brush.DrawAs = ESlateBrushDrawType::Image; + Brush.SetImageSize(FVector2D(PieceCellSize, PieceCellSize)); + Brush.Mirroring = GetBrushMirroring(PieceMirroring); + + // The brush is replaced whole rather than adjusted, so that every piece is drawn the same way whether it is + // the first to arrive or the last. + PieceImage->SetBrush(Brush); +} + +void UMapWidget::ReportMapLayout() +{ + const int32 Grid = FMath::Max(1, GridSize); + + int32 Arrived = 0; + for (const FEleriMapPieceCell& Cell : PieceCells) + { + if (Cell.Texture) + { + ++Arrived; + } + } + + UE_LOG(LogEleriMapWidget, Display, TEXT("Map %s: %d of %d pieces up, named %s in %s, drawn %d by %d, placed %s."), + *MapName, Arrived, PieceCells.Num(), *PieceAssetPattern, *PiecePackageFolder, Grid, Grid, + CaptureActor ? TEXT("through the capture actor's own mapping") : TEXT("by their filenames, there being no capture actor in this world")); + + if (CaptureActor) + { + const FBox Region = CaptureActor->WorldMapBounds; + + UE_LOG(LogEleriMapWidget, Display, + TEXT(" region %s, cut into %d tiles a side of %.0f units, which is %.0f units across."), + *Region.ToString(), CaptureActor->TilesPerAxis, CaptureActor->GetTileWorldSize(), Region.Max.X - Region.Min.X); + } + else + { + UE_LOG(LogEleriMapWidget, Display, + TEXT(" with no capture actor there is no mapping to place the player through, so the marker stays hidden.")); + } + + for (const FEleriMapPieceCell& Cell : PieceCells) + { + UE_LOG(LogEleriMapWidget, Display, TEXT(" piece column %d row %d is drawn in cell column %d row %d, %s: %s"), + Cell.SourceColumn, Cell.SourceRow, Cell.CellColumn, Cell.CellRow, + Cell.Texture ? TEXT("loaded") : TEXT("not loaded"), *Cell.ObjectPath); + } +} + +float UMapWidget::GetMapDesignSize() const +{ + return GetMapSizeFor(GridSize, PieceCellSize); +} + +FVector2D UMapWidget::GetViewSize() const +{ + // The slate size the map is looked at through rather than the resolution of the screen: everything about + // zoom is relative to what the map was actually given, which is what makes a fit-zoom a fit whatever shape + // the view is and whatever the DPI scale is. + if (MapViewport) + { + return MapViewport->GetCachedGeometry().GetLocalSize(); + } + + return FVector2D::ZeroVector; +} + +float UMapWidget::GetMapScale() const +{ + const FVector2D ViewSize = GetViewSize(); + const float MapSize = GetMapDesignSize(); + + if (ViewSize.X <= 0.0 || ViewSize.Y <= 0.0 || MapSize <= 0.f) + { + return 0.f; + } + + // Zoom one is the whole map across the shorter side of the view, so the map is as large as it can be and + // still be seeable whole, whichever side of the view is the shorter one. + return static_cast(FMath::Min(ViewSize.X, ViewSize.Y) / MapSize) * FMath::Max(Zoom, KINDA_SMALL_NUMBER); +} + +FVector2D UMapWidget::GetClampedFocusPoint(const FVector2D& InFocusPoint) const +{ + const float MapSize = GetMapDesignSize(); + const FVector2D ViewSize = GetViewSize(); + const float Scale = GetMapScale(); + + if (Scale <= 0.f || ViewSize.X <= 0.0 || ViewSize.Y <= 0.0) + { + return InFocusPoint; + } + + // Half of what the view covers, in map units. The centre of the view cannot come closer to an edge of the + // map than that, or the map would be dragged off the view. An axis the whole map already fits across has no + // room to pan in at all, so the map is held on the middle of it: that is what a fitted map does, and it is + // why a map at zoom one cannot be dragged sideways. + const FVector2D HalfVisible = ViewSize * (0.5 / static_cast(Scale)); + const double HalfMap = MapSize * 0.5; + + FVector2D Clamped; + Clamped.X = (HalfVisible.X >= HalfMap) ? HalfMap : FMath::Clamp(InFocusPoint.X, HalfVisible.X, MapSize - HalfVisible.X); + Clamped.Y = (HalfVisible.Y >= HalfMap) ? HalfMap : FMath::Clamp(InFocusPoint.Y, HalfVisible.Y, MapSize - HalfVisible.Y); + + return Clamped; +} + +void UMapWidget::FocusOnContent(const FVector2D& ContentPosition) +{ + FocusPoint = GetClampedFocusPoint(ContentPosition); + + ApplyViewTransform(); +} + +void UMapWidget::ApplyViewTransform() +{ + if (!MapLayer) + { + return; + } + + const FVector2D ViewSize = GetViewSize(); + const float MapScale = GetMapScale(); + + if (ViewSize.X <= 0.0 || ViewSize.Y <= 0.0 || MapScale <= 0.f) + { + // There is no view to fit the map into until the widget has been laid out, which the next tick will + // have behind it. + return; + } + + FocusPoint = GetClampedFocusPoint(FocusPoint); + + // The layer fills the view and is scaled about its own centre, so what this transform is about is the + // content point that lands at the centre of the view: translation = scale * (half the view - the focus), + // in slate units. Everything about the view follows from that one line. + const FVector2D Translation = (ViewSize * 0.5 - FocusPoint) * static_cast(MapScale); + + const FWidgetTransform Transform(Translation, FVector2D(MapScale, MapScale), FVector2D::ZeroVector, 0.f); + if (bMapTransformApplied && Transform == AppliedMapTransform) + { + // Nothing about the view has moved, and setting the transform again would invalidate the whole map's + // draw for nothing. + return; + } + + AppliedMapTransform = Transform; + bMapTransformApplied = true; + + MapLayer->SetRenderTransform(Transform); +} + +bool UMapWidget::GetPlayerMapUV(FVector2D& OutUV) const +{ + const APawn* PlayerPawn = GetOwningPlayerPawn(); + if (!PlayerPawn || !CaptureActor) + { + return false; + } + + // The same mapping the pieces were placed through, so the marker cannot end up describing a different map + // than the one on screen. + OutUV = CaptureActor->WorldToBakedMapUV(PlayerPawn->GetActorLocation()); + + return true; +} + +FVector2D UMapWidget::GetMarkerSlotSize() const +{ + // The one thing about the marker's size the map decides: the longer side of it, in the map's own units. + const float Size = FMath::Max(2.f, MarkerSize); + + FVector2D MarkerShape = FVector2D::ZeroVector; + + // An icon says what shape it is through the art it draws: the size the art was imported at is the shape it + // was drawn with, and the size of what is resident is the second choice for a texture with nothing imported + // to give. + if (const UImage* MarkerIcon = Cast(PlayerMarker)) + { + if (const UTexture2D* IconTexture = Cast(MarkerIcon->GetBrush().GetResourceObject())) + { + const FIntPoint ImportedSize = IconTexture->GetImportedSize(); + + MarkerShape = FVector2D(ImportedSize.X, ImportedSize.Y); + if (MarkerShape.X <= 0.0 || MarkerShape.Y <= 0.0) + { + MarkerShape = FVector2D(IconTexture->GetSizeX(), IconTexture->GetSizeY()); + } + } + } + + // Anything else -- a box, a panel, or an icon with no art pointed at yet -- says what shape it is by the + // size it asks to be drawn at, which is what the art of an image and the contents of a panel are measured + // into. Nothing has a size until it has been laid out, so this is asked again whenever the size is written + // rather than once. + if (MarkerShape.X <= 0.0 || MarkerShape.Y <= 0.0) + { + MarkerShape = PlayerMarker ? PlayerMarker->GetDesiredSize() : FVector2D::ZeroVector; + } + + // A marker with nothing to say what shape it is is drawn square: a plain box of a marker is a marker. + if (MarkerShape.X <= 0.0 || MarkerShape.Y <= 0.0) + { + return FVector2D(Size, Size); + } + + // The design's own shape, drawn at the size the map asks for: one setting is the size of the marker whether + // the marker is an icon, a box or a panel of several, and none of them is squeezed out of shape by it. + return MarkerShape * (static_cast(Size) / FMath::Max(MarkerShape.X, MarkerShape.Y)); +} + +bool UMapWidget::ResolveMarkerAngle(float& OutAngle) const +{ + if (!bMapAxesResolved) + { + return false; + } + + const APawn* PlayerPawn = GetOwningPlayerPawn(); + const float FacingYaw = static_cast(PlayerPawn->GetActorRotation().Yaw);; + const double Facing = FMath::DegreesToRadians(static_cast(FacingYaw)); + + const FVector2D MapDirection = MapUVPerWorldX * FMath::Cos(Facing) + MapUVPerWorldY * FMath::Sin(Facing); + const double DirectionDegrees = FMath::RadiansToDegrees(FMath::Atan2(MapDirection.Y, MapDirection.X)); + + OutAngle = FMath::UnwindDegrees(static_cast(DirectionDegrees) + 90.f + MarkerFacingOffset); + + return true; +} + +void UMapWidget::UpdateMarkerView() +{ + if (!PlayerMarker) + { + return; + } + + if (bFollowPlayer) + { + FVector2D PlayerMapUV; + if (GetPlayerMapUV(PlayerMapUV)) + { + MarkerMapUV = PlayerMapUV; + bMarkerPlaced = true; + } + + // With nothing to say where the player is, the last place known is the last place known, and the marker + // stays on it rather than blinking out. + } + + if (!bMarkerPlaced) + { + PlayerMarker->SetVisibility(ESlateVisibility::Hidden); + + return; + } + + // Whether the marker is drawn is the map's to say, because the map is what knows whether there is a place + // to put it; what the design says is what it looks like when there is one. Only the marker itself is taken + // out of the way of the pointer, so a marker made of several widgets keeps whatever they do themselves. + PlayerMarker->SetVisibility(ESlateVisibility::SelfHitTestInvisible); + + if (UCanvasPanelSlot* MarkerSlot = Cast(PlayerMarker->Slot)) + { + // Alignment centres the widget on the slot's position, so a place on the map is one pair of numbers. + MarkerSlot->SetPosition(MarkerMapUV * static_cast(GetMapDesignSize())); + } + + // The turn is the one thing about the marker that changes without its place on the map changing, and it is + // written only when it has actually turned: a player standing still, or one whose facing setting answers the + // same thing every frame, costs nothing here. + float WantedAngle = 0.f; + if (ResolveMarkerAngle(WantedAngle) + && (!bMarkerAngleApplied || !FMath::IsNearlyEqual(MarkerAngle, WantedAngle, 0.01f))) + { + MarkerAngle = WantedAngle; + bMarkerAngleApplied = true; + + PlayerMarker->SetRenderTransformAngle(MarkerAngle); + } +} + +void UMapWidget::SetZoom(float NewZoom) +{ + // The two ends are read in the order they are set in, so that a pair of settings edited the wrong way round + // is a zoom range rather than a zoom that cannot be changed. + const float Clamped = FMath::Clamp(NewZoom, FMath::Min(MinZoom, MaxZoom), FMath::Max(MinZoom, MaxZoom)); + + if (FMath::IsNearlyEqual(Zoom, Clamped)) + { + return; + } + + // The focus point is what is held still across a zoom, so the map grows and shrinks about the middle of the + // view rather than about its top left corner. + Zoom = Clamped; + + ApplyViewTransform(); +} + +void UMapWidget::ZoomBy(float Amount) +{ + if (!FMath::IsFinite(Amount) || FMath::IsNearlyZero(Amount)) + { + return; + } + + const float SafeStep = FMath::Max(ZoomStep, 1.0001f); + const float Multiplier = FMath::Pow(SafeStep, Amount); + + SetZoom(Zoom * Multiplier); +} + +void UMapWidget::ResetView() +{ + Zoom = FMath::Clamp(DefaultZoom, FMath::Min(MinZoom, MaxZoom), FMath::Max(MinZoom, MaxZoom)); + + // A reset is a reset: the view is handed back to the player, which is what the map is for, and the marker is + // placed now so that the map can be centred on where the player actually is rather than where they were. + bCenterOnPlayer = true; + + UpdateMarkerView(); + + if (bMarkerPlaced) + { + FocusOnContent(MarkerMapUV * static_cast(GetMapDesignSize())); + } + else + { + FocusOnContent(FVector2D(GetMapDesignSize() * 0.5)); + } +} + +void UMapWidget::SetFollowPlayer(bool bNewFollow) +{ + bFollowPlayer = bNewFollow; + + // Turning following on is a request to be found, so the marker is placed now rather than at the next tick. + // Turning it off leaves the marker where it is, which is the whole point of turning it off. + UpdateMarkerView(); +} + +void UMapWidget::SetCenterOnPlayer(bool bNewCenter) +{ + bCenterOnPlayer = bNewCenter; + + if (bCenterOnPlayer) + { + UpdateMarkerView(); + + if (bMarkerPlaced) + { + FocusOnContent(MarkerMapUV * static_cast(GetMapDesignSize())); + } + } +} + +void UMapWidget::HandleShown() +{ + // The bake is resolved and the pieces asked for the first time the map is looked at rather than when it was + // made, so that the map costs nothing until it is wanted and the world is up by the time it is. + EnsureMapReady(); + + bFollowPlayer = bFollowPlayerByDefault; + + UpdateMarkerView(); + + if (bResetViewWhenShown) + { + // A map that opens where it was left last time has to be reset before it can be read, and a reset is also + // how the view is handed back to the marker. + ResetView(); + } + + // After the reset rather than before it: a view that opens fitted but is not held on the player is what + // bCenterOnPlayerByDefault being off means, and a reset centres whatever that says. + bCenterOnPlayer = bCenterOnPlayerByDefault; + + ApplyViewTransform(); +} + +void UMapWidget::RefreshPieces() +{ + // A grid changed under the widget is a different number of cells, which is a different tree. It is taken down + // and built again here, which is what the next build of this widget's slate widget will use. A grid changed + // while the map is on screen is a settings change, so it is worth re-opening the map for: an already built + // widget cannot change shape under the player. + if (PieceImages.Num() != FMath::Square(FMath::Max(1, GridSize))) + { + if (WidgetTree) + { + if (WidgetTree->RootWidget == MapRoot) + { + WidgetTree->RootWidget = nullptr; + } + else if (MapRoot) + { + MapRoot->RemoveFromParent(); + } + } + + MapRoot = nullptr; + MapViewport = nullptr; + MapLayer = nullptr; + MapBackground = nullptr; + PieceGrid = nullptr; + PieceImages.Reset(); + + // The marker is not the tree's to make any more: it belongs to the design, and the tree being built + // again takes it off the layer just taken down and puts it on the new one. + Interaction = nullptr; + + EnsureWidgetTree(); + bMapTransformApplied = false; + } + + // A second request is what a re-bake needs: what is already in memory arrives at once, and what has changed + // since is loaded fresh. + if (PieceStreamHandle.IsValid()) + { + PieceStreamHandle->ReleaseHandle(); + PieceStreamHandle.Reset(); + } + + bPiecesRequested = false; + bMapReady = false; + + EnsureMapReady(); + + UE_LOG(LogEleriMapWidget, Display, TEXT("The map was asked for again: %d pieces of %s."), PieceCells.Num(), *MapName); +} + +void UMapWidget::ToggleWidget_Implementation(bool bActive) +{ + Super::ToggleWidget_Implementation(bActive); + + // Showing is the main game widget's OpenWidget going through here, and it is where the map does its work: + // resolving the bake, asking for the pieces the first time, and fitting the map to the view. It is all + // idempotent, so a show that also arrives through SetVisibility below is nothing to worry about. + if (bActive) + { + HandleShown(); + + return; + } + + if (bPanning) + { + // The map was closed in the middle of a drag, so the drag ends here rather than being left running under a + // widget that is no longer drawn. + EndPan(PanPointerIndex); + } +} + +void UMapWidget::SetVisibility(ESlateVisibility InVisibility) +{ + const auto IsShown = [](ESlateVisibility VisibilityState) + { + return VisibilityState != ESlateVisibility::Collapsed && VisibilityState != ESlateVisibility::Hidden; + }; + + const bool bWasShown = IsShown(GetVisibility()); + + Super::SetVisibility(InVisibility); + + // Being shown is what the map waits for, whichever way it was shown: through ToggleWidget, from a blueprint, + // or from an animation. Without this, a map shown by anything other than the toggle would open blank. + if (!bWasShown && IsShown(GetVisibility())) + { + HandleShown(); + } +} + +void UMapWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + // The widget exists but is not being looked at yet, and the bake is only resolved when it is, unless the + // settings ask for it earlier. That is what makes the pieces lazy rather than a cost paid at startup. + PanPointerIndex = INDEX_NONE; + bPanning = false; + + if (bPreloadPiecesOnConstruct) + { + EnsureMapReady(); + } +} + +void UMapWidget::NativeDestruct() +{ + // The pieces are wanted by this widget and by nothing else, so letting go of the request lets go of whatever + // of them nothing else is holding on to. Asking again on the next show is cheap: what is still in memory + // arrives at once. + if (PieceStreamHandle.IsValid()) + { + PieceStreamHandle->ReleaseHandle(); + PieceStreamHandle.Reset(); + } + + bPiecesRequested = false; + PanPointerIndex = INDEX_NONE; + bPanning = false; + + Super::NativeDestruct(); +} + +void UMapWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) +{ + Super::NativeTick(MyGeometry, InDeltaTime); + + const ESlateVisibility CurrentVisibility = GetVisibility(); + if (CurrentVisibility == ESlateVisibility::Collapsed || CurrentVisibility == ESlateVisibility::Hidden) + { + // Nothing on screen, so nothing to keep up to date: the marker and the view are worked out again when the + // map is shown. + return; + } + + if (!bMapReady) + { + // The map was shown by something other than the toggle: added to the viewport directly, or left visible by + // whatever holds it. A tick means it is on screen, so this is the first moment the bake is worth resolving, + // and a hidden map still costs nothing at all. + HandleShown(); + } + + UpdateMarkerView(); + + if (bCenterOnPlayer && bMarkerPlaced) + { + // Held on the player, wherever they have got to. The transform is only written when something about the + // view has actually changed, so a player standing still costs nothing per frame. + FocusOnContent(MarkerMapUV * static_cast(GetMapDesignSize())); + + return; + } + + // Not held on the player, so the only thing left that can move the map is the view changing size, which a + // fit-relative zoom depends on. + ApplyViewTransform(); +} + +void UMapWidget::HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value) +{ + Super::HandleUiInputAction_Implementation(InputAction, Value); + + switch (InputAction) + { + case EEleriUIInputAction::Select: + break; + case EEleriUIInputAction::Back: + break; + case EEleriUIInputAction::TabLeft: + break; + case EEleriUIInputAction::TabRight: + break; + case EEleriUIInputAction::Zoom: + { + const float ZoomAmount = Value.Get(); + ZoomBy(ZoomAmount); + break; + } + case EEleriUIInputAction::MovePointer: + { + const FVector2D MoveDelta = Value.Get(); + UpdatePan(MoveDelta * MoveStep); + break; + } + } +} + +void UMapWidget::BeginPan(int32 PointerIndex, const FVector2D& ScreenPosition) +{ + // One pointer at a time: a second finger while a drag is already running is ignored rather than added, + // because the map is dragged and not pinched. + if (PanPointerIndex != INDEX_NONE) + { + return; + } + + PanPointerIndex = PointerIndex; + LastPanScreenPosition = ScreenPosition; + bPanning = true; + + // Looking somewhere else is what a drag is for, so the view stops being held on the player while it is done. + bCenterOnPlayer = false; +} + +void UMapWidget::UpdatePan(const FVector2D& Delta) +{ + bCenterOnPlayer = false; + + const float MapScale = GetMapScale(); + if (MapScale <= 0.f || Delta.IsNearlyZero()) + { + return; + } + + // The map moves with the pointer, so the point of the map under the pointer stays under it: the centre of the + // view moves the other way, by the same distance in map units, which is the screen distance over the scale. + FocusPoint = GetClampedFocusPoint(FocusPoint - Delta * (1.0 / static_cast(MapScale))); + + ApplyViewTransform(); +} + +void UMapWidget::EndPan(int32 PointerIndex) +{ + if (PointerIndex != PanPointerIndex) + { + return; + } + + PanPointerIndex = INDEX_NONE; + bPanning = false; +} diff --git a/Source/ProjectEleri/UI/MapWidget.h b/Source/ProjectEleri/UI/MapWidget.h new file mode 100644 index 00000000..986f0913 --- /dev/null +++ b/Source/ProjectEleri/UI/MapWidget.h @@ -0,0 +1,535 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "UI/EleriBaseWidget.h" +#include "MapWidget.generated.h" + +class AOrthoMapCaptureActor; +class UBorder; +class UCanvasPanel; +class UImage; +class UOverlay; +class UTexture2D; +class UWidget; + +/** What a request for a set of assets to be loaded is held by. Only used through a shared pointer here, so the + * full declaration is left to the file that actually asks for the pieces. */ +struct FStreamableHandle; + +/** How the widget decides which piece asset belongs in which cell of the map. */ +UENUM(BlueprintType) +enum class EMapPieceLayout : uint8 +{ + /** Ask the capture actor's own world to map mapping where each piece's world box lands. This is the + * default because it is the same mapping the player marker is placed through, so the picture and the + * marker cannot describe two different maps. Falls back to the filename order when there is no actor + * to ask, or when the answer is not a lattice. */ + SampledFromMapping UMETA(DisplayName = "Sampled From The Mapping"), + + /** Put the piece whose filename says column C, row R in the cell C across and R down, and ask nothing. + * What is left when the mapping cannot be consulted. */ + FilenameColumnRow UMETA(DisplayName = "Filename Column Row") +}; + +/** Whether each piece's own picture is mirrored inside its cell. The actor's bFlipU and bFlipV mirror the + * whole map, and a piece baked as a map of its own carries that mirroring either inside the asset or not + * at all, depending on how the bake wrote it. Nothing the widget can ask, so it is a setting, and off is + * right for the usual bake: the one that reads the way the world does. */ +UENUM(BlueprintType) +enum class EMapPieceMirroring : uint8 +{ + /** Draw the piece as the asset has it. */ + None UMETA(DisplayName = "As The Asset Has It"), + /** Mirror every piece left to right, the counterpart of the actor's bFlipU. */ + Horizontal UMETA(DisplayName = "Horizontal"), + /** Mirror every piece top to bottom, the counterpart of the actor's bFlipV. */ + Vertical UMETA(DisplayName = "Vertical"), + /** Both, for a bake that was made with both flags on. */ + Both UMETA(DisplayName = "Both") +}; + +/** Which way the player is said to be facing, which is the direction the marker is turned to point along. A map + * is usually asked which way the player is going, and a pawn and a camera can disagree about that: this is the + * setting that says which of them the map should believe. */ +UENUM(BlueprintType) +enum class EMapMarkerFacing : uint8 +{ + /** The way the pawn is turned to, which is what the player's own forward is for everything that moves + * them. The default, because the pawn is what the marker's place on the map comes from. */ + PawnRotation UMETA(DisplayName = "Pawn Rotation"), + + /** The way the controller is looking, which is the camera's own yaw and can be a long way from the pawn's + * while the pawn is moving sideways or backwards. */ + ControlRotation UMETA(DisplayName = "Control Rotation"), + + /** The way the pawn is moving. A pawn standing still points nowhere by its velocity, so the way it is + * turned to stands in. */ + Velocity UMETA(DisplayName = "Velocity"), + + /** Not the player's facing at all: the marker is turned by MarkerFacingOffset and nothing else, which is + * what a marker that should sit still, or art that has to be squared up, is for. */ + None UMETA(DisplayName = "Not Turned To The Player") +}; + +/** One cell of the map: which piece of the bake was asked for it, and where the widget put it. Kept so that + * ReportMapLayout can say what the widget decided without the decision being worked out again. */ +USTRUCT(BlueprintType) +struct FEleriMapPieceCell +{ + GENERATED_BODY() + + /** Column in the piece's own filename. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map") + int32 SourceColumn = 0; + + /** Row in the piece's own filename. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map") + int32 SourceRow = 0; + + /** Column the piece is drawn in, counted across from the left of the map. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map") + int32 CellColumn = 0; + + /** Row the piece is drawn in, counted down from the top of the map. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map") + int32 CellRow = 0; + + /** The asset this cell is showing, as a long object path. Reported even when the load fails, because a + * path that does not exist is the first thing worth knowing about a cell that came back empty. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map") + FString ObjectPath; + + /** The texture the cell is showing, or null while its piece is still arriving, which is the normal state + * for a moment after the map is first opened. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map") + TObjectPtr Texture = nullptr; +}; + +/** + * The world map, as a grid of the baked map's pieces. + * + * The bake writes one texture per piece, each a map of its own region, and this widget lays them out in a + * grid, one cell per piece, with the player marked where their world position falls. Nothing here is drawn + * from the level at runtime: the pieces are the bake, and the level only says where a world position lands + * in them, which is what the capture actor's world to map mapping is for. + * + * The map itself is built in code, in RebuildWidget, because a user widget's slate widget is made from the root + * of its widget tree and there is no tree at all until the widget has been initialized. A blueprint subclass of + * this therefore needs to hold nothing: it may be empty, or its root may be a panel, which is where this + * widget's own tree is added. The one part of the map that is not built here is the player's marker, which is + * the widget named PlayerMarker in that blueprint: what a marker looks like is the design's business, and only + * where it sits on the map is the map's. + * + * The pieces are 16384 pixel virtual textures, so loading all 64 of them eagerly would be a stall and a + * virtual texture pool full of a map nobody is looking at. They are asked for lazily, the first time the + * widget is shown, in one batched asynchronous load, and the renderer decides from there what of them is + * resident. + */ +UCLASS() +class PROJECTELERI_API UMapWidget : public UEleriBaseWidget +{ + GENERATED_BODY() + +public: + UMapWidget(); + + //----- Map source ----- + + /** Map the pieces were baked from. Spelled out rather than taken from the world, because in PIE the + * world's own name is prefixed (UEDPIE_0_Persistent_GameWorld) and the assets are not. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source") + FString MapName; + + /** Folder the baked map was saved into, as a long package path: the same one the capture actor's + * OutputPackageFolder names. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source") + FString PiecePackageFolder; + + /** Name of one piece, with {Map}, {Grid}, {Col} and {Row} standing in for the parts that vary. The + * pieces are named after the map they came from, so a map and its pieces can be re-baked without this + * having to be edited. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source") + FString PieceAssetPattern; + + /** Pieces per axis: the grid the map was cut into, and the side of the grid that is drawn. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source", meta = (ClampMin = "1", ClampMax = "64")) + int32 GridSize; + + /** How the widget works out which piece goes where. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source") + EMapPieceLayout PieceLayout; + + /** Whether each piece is mirrored inside its cell. Only a map baked with the capture actor's bFlipU or + * bFlipV needs anything other than the default, and the widget warns at load when one of those flags is + * on while this still says the pieces are drawn as the assets have them. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source") + EMapPieceMirroring PieceMirroring; + + /** Whether the pieces are asked for as soon as the widget is constructed rather than the first time it + * is shown. Off is the lazy default: the map is not usually the first thing a player opens. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Loading") + bool bPreloadPiecesOnConstruct; + + //----- View settings ----- + + /** Zoom of one fits the whole map across the shorter side of the view, so the map is as large as it can + * be and still be seen whole. Everything else is relative to that. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "0.01")) + float DefaultZoom; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "0.01")) + float MinZoom; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "0.01")) + float MaxZoom; + + /** How much one step of zoom changes the zoom: one notch of the wheel, and one call to ZoomIn or + * ZoomOut. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "1.0001")) + float ZoomStep; + + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "1.0001")) + float MoveStep; + + /** Design units one piece is drawn at, which fixes how large the whole map is before zoom is applied. + * It sets the size the pieces are laid out at, not how large they appear: zoom one still fits the map + * to the view. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "16")) + float PieceCellSize; + + /** Whether opening the map starts with the whole map in view and centred on the player, which is what + * the map is for. Off leaves whatever the player had zoomed and panned to. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View") + bool bResetViewWhenShown; + + //----- Marker settings ----- + + /** Whether the marker follows the player as they move. Off freezes it where it was, which is what a + * player comparing two places wants. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker") + bool bFollowPlayerByDefault; + + /** Whether the view keeps the player at its centre as they move. Dragging the map turns this off, so + * that looking somewhere else is not fought by the marker being pulled back every frame. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker") + bool bCenterOnPlayerByDefault; + + /** The player's place on the map: the widget of this name in the blueprint this map is drawn in, found by + * name rather than made here. Nothing in this file draws it, so what the marker looks like -- its brush, + * its colours, its corner radii -- belongs to the design. The map takes it onto its own layer, which is + * what a marker has to be on to move and zoom with the map it marks, and then sizes it, places it and + * turns it. Any kind of widget will do, from an image to a panel holding several. Optional, because a + * design with no marker in it is a map that says so once rather than a map that will not open. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|Marker", meta = (BindWidgetOptional)) + TObjectPtr PlayerMarker; + + /** How long the marker's longer side is drawn, in the map's own design units, which is the one thing about + * the marker's look the map has to know, because the map is what places it and is measured in these units. + * What shape it is comes from the design: an icon's shape is the shape of its art, and anything else is + * drawn at the shape it asks to be, so a wide or a tall marker is not squeezed into a square. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker", meta = (ClampMin = "2")) + float MarkerSize; + + /** Degrees added to the turn the marker is given, for art that does not point up: an arrow drawn pointing + * right wants -90 here, and one drawn pointing down wants 180. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker") + float MarkerFacingOffset; + + /** Colour the map's own rectangle is filled with, so that a piece still arriving reads as a part of the + * map that has not been drawn yet rather than as nothing at all. */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View") + FLinearColor MapBackgroundColor; + + //----- View state ----- + + /** Zoom relative to fitting the map in the view, held between MinZoom and MaxZoom. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|View") + float Zoom; + + /** Whether the marker is following the player. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|Marker") + bool bFollowPlayer; + + /** Whether the view is keeping the player at its centre. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|Marker") + bool bCenterOnPlayer; + + /** Whether the map is being dragged right now, which is worth knowing outside the widget because a drag + * is not a click on whatever is behind the map. */ + UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|View") + bool bPanning; + + //----- View API ----- + + /** Sets the zoom, held between MinZoom and MaxZoom. One is the whole map in view. */ + UFUNCTION(BlueprintCallable, Category = "Eleri|Map|View") + void SetZoom(float NewZoom); + + /** Zooms by a signed number of steps: positive zooms in, negative zooms out, 0 does nothing. + * A magnitude of 1 is one full ZoomStep, so this can be fed the raw value from a wheel or + * gamepad trigger axis directly. Fractional or >1 magnitudes scale accordingly. */ + UFUNCTION(BlueprintCallable, Category = "Eleri|Map|View") + void ZoomBy(float Amount); + + /** Fits the whole map in the view, centres it on the player, and holds it there again. */ + UFUNCTION(BlueprintCallable, Category = "Eleri|Map|View") + void ResetView(); + + //----- Marker API ----- + + UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Marker") + void SetFollowPlayer(bool bNewFollow); + + UFUNCTION(BlueprintPure, Category = "Eleri|Map|Marker") + bool IsFollowingPlayer() const { return bFollowPlayer; } + + /** Whether the view is centred on the player, which it is until the map is dragged. */ + UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Marker") + void SetCenterOnPlayer(bool bNewCenter); + + UFUNCTION(BlueprintPure, Category = "Eleri|Map|Marker") + bool IsCenteringOnPlayer() const { return bCenterOnPlayer; } + + //----- The bake this widget is showing ----- + + /** What the widget has worked out about the bake so far: which piece the pattern asks for, where it was + * put, and whether it has arrived. */ + UFUNCTION(BlueprintPure, Category = "Eleri|Map|Source") + const TArray& GetPieceCells() const { return PieceCells; } + + /** The capture actor the region and the mapping came from, or null when there is none in the world. Null + * means the player cannot be placed and the lattice could not be checked, which is reported at load + * rather than treated as an error. */ + UFUNCTION(BlueprintPure, Category = "Eleri|Map|Source") + AOrthoMapCaptureActor* GetMapCaptureActor() const { return CaptureActor; } + + /** Does the load again: works the layout out from scratch and asks for the pieces, whether they were + * asked for before or not. The thing to call after re-baking the map without leaving the editor. */ + UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Source") + void RefreshPieces(); + + /** Logs the bake this widget is showing: the pattern and folder it asked, whether the mapping could be + * consulted, how many pieces arrived, and every cell's piece and place. The thing to look at when the + * map is blank, or whole but wrong. */ + UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Source") + void ReportMapLayout(); + + /** Everything the widget does when it is shown: resolves the bake, asks for the pieces the first time, + * and fits the map to the view. Exposed so that a blueprint can do the same work for a show this widget + * did not get to see, and so that the show path is one thing rather than three. */ + UFUNCTION(BlueprintCallable, Category = "Eleri|Map") + void HandleShown(); + +protected: + //----- Lifetime, in the form the engine hands it over ----- + + virtual void ToggleWidget_Implementation(bool bActive) override; + virtual void SetVisibility(ESlateVisibility InVisibility) override; + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; + + /** Where the tree has to be built: a user widget's slate widget is made by taking the root of its widget + * tree, and there is no tree until the widget has been initialized, which this call makes sure of. */ + virtual TSharedRef RebuildWidget() override; + + //----- Input, in the form the engine hands it over ----- + + virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value) override; + + //----- Building and loading ----- + + /** Builds the widget tree if it is not there already, so that calling this twice is nothing. */ + void EnsureWidgetTree(); + + /** Resolves the bake and asks for its pieces, once. Called on the way to being shown rather than at + * construction, so that the world is up and the map is only paid for when it is wanted. Doing it twice + * is nothing. */ + void EnsureMapReady(); + + /** Works out which piece belongs in which cell and fills PieceCells with it. Reports the reason and + * returns false when the mapping could not be consulted, which leaves the caller to say so. */ + bool ResolveMapLayout(); + + /** Works out how the baked map lies in the world: what a step of one world unit along X does to the map's + * own UV, and what a step along Y does, both taken from the capture actor's own mapping. That is what the + * marker is turned by, and taking it from the mapping is what makes a bake that was captured turned, or + * mirrored with the actor's flip flags, come out right without anything being assumed about it. */ + void ResolveMapAxes(); + + /** The mapping route to a lattice: every piece's world box centre put through the capture actor's own + * world to map mapping, which is the mapping the marker is placed through as well. False with a reason + * when there is no actor, when a piece lands off the map, or when two pieces land in one cell. */ + bool TrySamplePieceCells(FText& OutReason); + + /** Where a piece's own cell of the region lands in the baked map, as a cell of the grid. */ + bool GetPieceCellFromMapping(int32 SourceColumn, int32 SourceRow, FIntPoint& OutCell) const; + + /** The other route: the piece whose filename says column C, row R goes in the cell C across and R down, + * without asking anything. */ + void FillPieceCellsFromFilenames(); + + /** One piece's box in the world, from the region the pieces were cut out of: columns run along X and + * rows along Y from its min corner, the way round the bake's own piece boxes run. */ + FBox GetPieceWorldBox(int32 SourceColumn, int32 SourceRow) const; + + /** Asks for every piece in PieceCells in one batched asynchronous load. */ + void RequestPieceLoads(); + + /** Puts every arrived piece on its image, and reports the ones that did not arrive. */ + void ApplyLoadedPieces(); + + /** The brush one piece image is drawn with: the piece's own texture, and the mirroring the bake needs, + * which is the one thing about a piece that cannot be asked for and has to be told. */ + void ApplyPieceBrush(UImage* PieceImage, UTexture2D* PieceTexture) const; + + //----- View ----- + + /** Grid side of the drawn map in design units: GridSize pieces of PieceCellSize. */ + float GetMapDesignSize() const; + + /** Side of the view the map is fit into, in slate units, or zero before the widget has been laid out. */ + FVector2D GetViewSize() const; + + /** Slate units one design unit of the map is drawn at: the fit-relative zoom, or zero before the widget + * has been laid out. Everything that turns a screen distance into a map distance goes through it. */ + float GetMapScale() const; + + /** The content point held at the centre of the view, clamped to the map so that it cannot be dragged + * off into the void around it. */ + FVector2D GetClampedFocusPoint(const FVector2D& InFocusPoint) const; + + /** Puts a content position at the centre of the view, clamped. */ + void FocusOnContent(const FVector2D& ContentPosition); + + /** Hands the zoom and the focus point to the map layer, and does nothing when they have not changed, so + * that following the player does not invalidate a transform that is already right. */ + void ApplyViewTransform(); + + /** Where the player is in the baked map, or false when there is nothing to say: no pawn, or no capture + * actor to put their position through. */ + bool GetPlayerMapUV(FVector2D& OutUV) const; + + /** The size the marker is drawn at: the shape the design gave it, with its longer side at MarkerSize, so + * that a wide or a tall marker is not squeezed into a square. A marker with nothing to say what shape it + * is -- a box with nothing in it, or a panel with nothing in that -- is drawn square. */ + FVector2D GetMarkerSlotSize() const; + + /** The angle the marker is turned to on screen, in degrees clockwise, or false when there is nothing to + * say: a mapping that cannot say how the map lies in the world, or a facing that cannot be had. */ + bool ResolveMarkerAngle(float& OutAngle) const; + + /** Puts the marker where the player is, and hides it when they are off the map. */ + void UpdateMarkerView(); + + //----- Drag and wheel ----- + + /** Starts a drag, from the screen position it started at. One pointer at a time: a second finger while a + * drag is running is ignored rather than added, because the map is dragged and not pinched. */ + void BeginPan(int32 PointerIndex, const FVector2D& ScreenPosition); + + /** Moves the map under the pointer, which is also what turns the view loose from the player. */ + void UpdatePan(const FVector2D& Delta); + + /** Ends the drag this pointer started, and does nothing for any other pointer. */ + void EndPan(int32 PointerIndex); + + //----- Widgets the map builds ----- + + /** Root of the tree this widget builds. */ + UPROPERTY(Transient) + TObjectPtr MapRoot; + + /** What the map is looked at through: everything outside it is clipped, and it is what the map is fit + * into. */ + UPROPERTY(Transient) + TObjectPtr MapViewport; + + /** What the map is drawn on: the map's own rectangle, the pieces, and the marker, moved and scaled by + * one transform rather than 64. */ + UPROPERTY(Transient) + TObjectPtr MapLayer; + + /** The region the map covers, drawn so that a piece still arriving is a pale part of the map rather than + * a hole in it. */ + UPROPERTY(Transient) + TObjectPtr MapBackground; + + /** The 64 pieces, each at the cell the layout put it in. */ + UPROPERTY(Transient) + TObjectPtr PieceGrid; + + /** One image per piece, in cell order: row by row from the top left. */ + UPROPERTY(Transient) + TArray> PieceImages; + + /** Takes every pointer event the map needs, so that a drag is never also a click on the level behind + * the map. */ + UPROPERTY(Transient) + TObjectPtr Interaction; + + //----- State ----- + + /** The bake as the widget worked it out: one entry per cell, whether the piece has arrived or not. */ + UPROPERTY(Transient) + TArray PieceCells; + + /** The capture actor the region and the mapping came from: cached because it is asked a great deal, and + * found again whenever it has gone away. */ + UPROPERTY(Transient) + TObjectPtr CaptureActor; + + /** Whether the layout and the piece requests have been worked out. False means a later call still has + * work to do, which is what happens when the map is shown before the world has its capture actor. */ + bool bMapReady; + + /** Whether the pieces have been asked for, so that asking happens once rather than once per show. */ + bool bPiecesRequested; + + /** What does the loading, held so that the pieces stay wanted until they have arrived. */ + TSharedPtr PieceStreamHandle; + + /** How much the map is zoomed, and the content point held at the centre of the view. */ + FVector2D FocusPoint; + + /** Where the player is on the map, as the map's own UV, and whether the marker has ever been placed. + * Held rather than asked for every frame so that the marker can be frozen where the player was, which + * is what turning following off is for. */ + FVector2D MarkerMapUV; + + bool bMarkerPlaced; + + /** The angle the marker is turned to on screen, in degrees clockwise, and whether it has been written yet. + * Held so that a tick which leaves the facing alone writes nothing. */ + float MarkerAngle; + + bool bMarkerAngleApplied; + + /** Whether the map has already said that the design has no marker in it, so that a map drawn without one + * says so once rather than on every update. */ + bool bMissingMarkerReported; + + /** How the baked map's own UV runs through the world: what a step of one world unit along X moves it, and + * what a step along Y moves it. Both are taken from the capture actor's own mapping, and both being zero + * is what bMapAxesResolved says no to. */ + FVector2D MapUVPerWorldX; + + FVector2D MapUVPerWorldY; + + /** Whether the two above have been worked out: false means the marker is placed but not turned. */ + bool bMapAxesResolved; + + /** The transform last handed to the map layer, so that a tick which changes nothing writes nothing. */ + FWidgetTransform AppliedMapTransform; + + /** Whether the transform above is worth comparing against yet. */ + bool bMapTransformApplied; + + /** Which pointer is dragging, or INDEX_NONE. */ + int32 PanPointerIndex; + + /** Where the pointer was when the drag last moved, in screen space. */ + FVector2D LastPanScreenPosition; +};