From 2c272c11b9e44afda4c53d17c2c2768216e4f1d2 Mon Sep 17 00:00:00 2001 From: Mathew Date: Sun, 16 Aug 2026 19:35:27 +0200 Subject: [PATCH] (Feat) Add Harvetable --- Content/Game/3d/Tools/Wood_Axe.uasset | 4 +- .../3d/Tools/Wood_Axe_Texture/Wood_Axe.uasset | 3 + .../Wood_Axe_BaseColor.uasset | 3 + .../Wood_Axe_Texture/Wood_Axe_Normal.uasset | 3 + ...Wood_Axe_OcclusionRoughnessMetallic.uasset | 3 + Content/Game/3d/Tools/Wood_Pickaxe.uasset | 4 +- .../Wood_Pickaxe_Texture/M_Pickaxe.uasset | 3 + ...d_AxeOcclusionRoughnessMetallic_Raw.uasset | 3 + .../Wood_Axe_BaseColor_sRGB.uasset | 3 + .../Wood_Axe_Normal_Raw.uasset | 3 + .../Wood_Pickaxe_AO_Raw.uasset | 3 - ...ood_Pickaxe_AlbedoTransparency_sRGB.uasset | 3 - ...Wood_Pickaxe_MetallicSmoothness_Raw.uasset | 3 - .../Wood_Pickaxe_Normal_Raw.uasset | 3 - Content/Game/3d/Tools/Wood_Sword.uasset | 4 +- .../WS_none_exploded_low_lambert1_AO.uasset | 3 - ...ded_low_lambert1_AlbedoTransparency.uasset | 3 - ...ded_low_lambert1_MetallicSmoothness.uasset | 3 - ...S_none_exploded_low_lambert1_Normal.uasset | 3 - .../Wood_Sword_BaseColor.uasset | 3 + .../Wood_Sword_Normal.uasset | 3 + ...od_Sword_OcclusionRoughnessMetallic.uasset | 3 + .../Animation/Fps/MTG_Pickaxe_Equip.uasset | 4 +- .../Fps/Pickaxe_Attack_Montage.uasset | 2 +- .../Data/Harvetable/DA_Harvest_OakTree.uasset | 3 + .../Data/Harvetable/DA_Harvest_Rock.uasset | 3 + .../Item/Tools/Wood/DA_Wood_Pickaxe.uasset | 4 +- Content/Game/Maps/Dev/Dev_Scene.umap | 4 +- .../Tools/Woods/Pickaxe/M_Pickaxe.uasset | 4 +- Content/Game/Player/BP_FpsPlayer.uasset | 4 +- EmberWild.uproject | 7 + Scripts/reorganize_content.py | 366 ++++++++++++++++++ Source/EmberWild/Private/FpsPlayer.cpp | 137 ++++++- .../Private/HarvestableDataAsset.cpp | 71 ++++ .../EmberWild/Private/HarvestableResource.cpp | 358 +++++++++++++++++ .../EmberWild/Private/HarvestingComponent.cpp | 147 +++++++ Source/EmberWild/Public/FpsPlayer.h | 70 +++- .../EmberWild/Public/HarvestableDataAsset.h | 180 +++++++++ Source/EmberWild/Public/HarvestableResource.h | 242 ++++++++++++ Source/EmberWild/Public/HarvestingComponent.h | 126 ++++++ Source/EmberWild/Public/ItemDataAsset.h | 29 +- 41 files changed, 1759 insertions(+), 71 deletions(-) create mode 100644 Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe.uasset create mode 100644 Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_BaseColor.uasset create mode 100644 Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_Normal.uasset create mode 100644 Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_OcclusionRoughnessMetallic.uasset create mode 100644 Content/Game/3d/Tools/Wood_Pickaxe_Texture/M_Pickaxe.uasset create mode 100644 Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_AxeOcclusionRoughnessMetallic_Raw.uasset create mode 100644 Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_BaseColor_sRGB.uasset create mode 100644 Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_Normal_Raw.uasset delete mode 100644 Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_AO_Raw.uasset delete mode 100644 Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_AlbedoTransparency_sRGB.uasset delete mode 100644 Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_MetallicSmoothness_Raw.uasset delete mode 100644 Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_Normal_Raw.uasset delete mode 100644 Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_AO.uasset delete mode 100644 Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_AlbedoTransparency.uasset delete mode 100644 Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_MetallicSmoothness.uasset delete mode 100644 Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_Normal.uasset create mode 100644 Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_BaseColor.uasset create mode 100644 Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_Normal.uasset create mode 100644 Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_OcclusionRoughnessMetallic.uasset create mode 100644 Content/Game/Data/Harvetable/DA_Harvest_OakTree.uasset create mode 100644 Content/Game/Data/Harvetable/DA_Harvest_Rock.uasset create mode 100644 Scripts/reorganize_content.py create mode 100644 Source/EmberWild/Private/HarvestableDataAsset.cpp create mode 100644 Source/EmberWild/Private/HarvestableResource.cpp create mode 100644 Source/EmberWild/Private/HarvestingComponent.cpp create mode 100644 Source/EmberWild/Public/HarvestableDataAsset.h create mode 100644 Source/EmberWild/Public/HarvestableResource.h create mode 100644 Source/EmberWild/Public/HarvestingComponent.h diff --git a/Content/Game/3d/Tools/Wood_Axe.uasset b/Content/Game/3d/Tools/Wood_Axe.uasset index 9a6ca42..d25c490 100644 --- a/Content/Game/3d/Tools/Wood_Axe.uasset +++ b/Content/Game/3d/Tools/Wood_Axe.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a92290e4320545b53a37488f01aa05acbe131f6fac03b67c3b89696bad4fc4e -size 331392 +oid sha256:2a86316b6470c9e17841ded41e2fbdac2faf1f3c8055c5d67c65f8626c2b6553 +size 332243 diff --git a/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe.uasset b/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe.uasset new file mode 100644 index 0000000..acc061b --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9cb65d4fa67796d5f1ac9fa55faab79df60e9a3c37c5fad902b823dbfc2348fe +size 17731 diff --git a/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_BaseColor.uasset b/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_BaseColor.uasset new file mode 100644 index 0000000..b03f187 --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_BaseColor.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8950505351220e0bc4bb4a91532d75debf6107d4e491c8871800f246dc2933ca +size 13869771 diff --git a/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_Normal.uasset b/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_Normal.uasset new file mode 100644 index 0000000..1c4e501 --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_Normal.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:858b6ff0235fd392f7ea9ed79874f6e35731228f05cdeb0d8402a70ea184c0ad +size 14722541 diff --git a/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_OcclusionRoughnessMetallic.uasset b/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_OcclusionRoughnessMetallic.uasset new file mode 100644 index 0000000..8dfee5f --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_OcclusionRoughnessMetallic.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:271fb6de98693d092392738427131dba436bb376b1f00a11a14555dcb80845e0 +size 6159035 diff --git a/Content/Game/3d/Tools/Wood_Pickaxe.uasset b/Content/Game/3d/Tools/Wood_Pickaxe.uasset index 148e872..7b981f0 100644 --- a/Content/Game/3d/Tools/Wood_Pickaxe.uasset +++ b/Content/Game/3d/Tools/Wood_Pickaxe.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:45f842ef3e418bc0baa87174618e0ee1eb925168e4132334d15d57e38093f7b9 -size 307061 +oid sha256:540aad90e097479195c69bc59aa985a67c706bf5b739eb6cbdf87627cd274fa9 +size 349650 diff --git a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/M_Pickaxe.uasset b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/M_Pickaxe.uasset new file mode 100644 index 0000000..c2f9aad --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/M_Pickaxe.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:925e01c41513ebb34c8fad2b3faaaaddba848f5dc042f67b252558e472f39abf +size 16619 diff --git a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_AxeOcclusionRoughnessMetallic_Raw.uasset b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_AxeOcclusionRoughnessMetallic_Raw.uasset new file mode 100644 index 0000000..7b32dc7 --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_AxeOcclusionRoughnessMetallic_Raw.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ab70849f5374110131f82423d8bd3da708353674d139f005dea41ae0b425472 +size 5807630 diff --git a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_BaseColor_sRGB.uasset b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_BaseColor_sRGB.uasset new file mode 100644 index 0000000..592bb0c --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_BaseColor_sRGB.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5af02edb535dbfb88c9cd42c7a8f10458590429d80edee0766c11a1df6bcc7c +size 13031183 diff --git a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_Normal_Raw.uasset b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_Normal_Raw.uasset new file mode 100644 index 0000000..99ace5c --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_Normal_Raw.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30a902e300117b30562384183b4e1d313478dc6de09e1baa51352d3f8b4bfb0e +size 15535413 diff --git a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_AO_Raw.uasset b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_AO_Raw.uasset deleted file mode 100644 index 11e4001..0000000 --- a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_AO_Raw.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f85ae668cc3b434497739422db3f066b94185c341956b9a87799e5fd851ad243 -size 1355496 diff --git a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_AlbedoTransparency_sRGB.uasset b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_AlbedoTransparency_sRGB.uasset deleted file mode 100644 index 34f91db..0000000 --- a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_AlbedoTransparency_sRGB.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e49c2de3e9184eb37c6e27ce13843b45ddd17f76c504623e209999d00083f237 -size 13031262 diff --git a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_MetallicSmoothness_Raw.uasset b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_MetallicSmoothness_Raw.uasset deleted file mode 100644 index 61a9280..0000000 --- a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_MetallicSmoothness_Raw.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e4451d145791f5435a6ac6482d4783a8b3c0eb2319d93c447695400485001571 -size 4104444 diff --git a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_Normal_Raw.uasset b/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_Normal_Raw.uasset deleted file mode 100644 index 3e892da..0000000 --- a/Content/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Pickaxe_Normal_Raw.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b7930b3c3175f2d451022445efde45831fc7974c9e4dcebc67244482faba37c4 -size 15536536 diff --git a/Content/Game/3d/Tools/Wood_Sword.uasset b/Content/Game/3d/Tools/Wood_Sword.uasset index 7db373b..b4ec88e 100644 --- a/Content/Game/3d/Tools/Wood_Sword.uasset +++ b/Content/Game/3d/Tools/Wood_Sword.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:89b4c128fcfc4d3d593f179a17a42316b90aba39400146061db94a7933a4beab -size 270376 +oid sha256:09b8659f10f6eb91c26096b50335f542ed665fe6b360758a01d050777618f8fe +size 271893 diff --git a/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_AO.uasset b/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_AO.uasset deleted file mode 100644 index b722709..0000000 --- a/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_AO.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f4b1092e5cac90ff6273ee70fb9fbe1efde4a5abea5af798c0f73d29d46695e3 -size 1154748 diff --git a/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_AlbedoTransparency.uasset b/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_AlbedoTransparency.uasset deleted file mode 100644 index 5087efa..0000000 --- a/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_AlbedoTransparency.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2befcefde7ce39c167764557e392903447395746add12a50b84d1e39d70e92e2 -size 7999489 diff --git a/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_MetallicSmoothness.uasset b/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_MetallicSmoothness.uasset deleted file mode 100644 index d02eeda..0000000 --- a/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_MetallicSmoothness.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a8d3cebd9859cd69f1645342955ded305f21935e3bfcea36d3d7bb871c5bc1b9 -size 2257714 diff --git a/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_Normal.uasset b/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_Normal.uasset deleted file mode 100644 index 80d37df..0000000 --- a/Content/Game/3d/Tools/Wood_Sword_Texture/WS_none_exploded_low_lambert1_Normal.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8346722f3d9419179bf282e4607ecd0166c6721b74dec384bc30e66cf4e2a9b4 -size 14434611 diff --git a/Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_BaseColor.uasset b/Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_BaseColor.uasset new file mode 100644 index 0000000..b77c5b2 --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_BaseColor.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1c2615d9fc383717dc917f62bf8844ef14c2f92345624ff443591d4ef039010 +size 7998908 diff --git a/Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_Normal.uasset b/Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_Normal.uasset new file mode 100644 index 0000000..0d4f66a --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_Normal.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebb4060a0e511fa9c4df2227fb0ef517e1c873744347fb42403ca1ec922b5075 +size 14430666 diff --git a/Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_OcclusionRoughnessMetallic.uasset b/Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_OcclusionRoughnessMetallic.uasset new file mode 100644 index 0000000..f8c9764 --- /dev/null +++ b/Content/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_OcclusionRoughnessMetallic.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c3b7e1e401261a448bd5b334b73b0ac6b22e2811fe426ec15b08c91d21f582e8 +size 3634700 diff --git a/Content/Game/Animation/Fps/MTG_Pickaxe_Equip.uasset b/Content/Game/Animation/Fps/MTG_Pickaxe_Equip.uasset index 364d6d4..838202e 100644 --- a/Content/Game/Animation/Fps/MTG_Pickaxe_Equip.uasset +++ b/Content/Game/Animation/Fps/MTG_Pickaxe_Equip.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4dcdd979023ebabf122c29fe7b7e61e7a96775f813712c194dd904cd80e1085 -size 8038 +oid sha256:fe13156526af1534f8ff02817e408b5e7519cf832812debd51b7bac1ea36818b +size 8182 diff --git a/Content/Game/Animation/Fps/Pickaxe_Attack_Montage.uasset b/Content/Game/Animation/Fps/Pickaxe_Attack_Montage.uasset index 86c76b9..cd759db 100644 --- a/Content/Game/Animation/Fps/Pickaxe_Attack_Montage.uasset +++ b/Content/Game/Animation/Fps/Pickaxe_Attack_Montage.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47a57437d544d78015f1e4a164644e76ab85b39feddbdeda9f5712a0932d5495 +oid sha256:32aca013a6730842de412bd9abb47370283406370de14f1dd83972d6f70ab504 size 10493 diff --git a/Content/Game/Data/Harvetable/DA_Harvest_OakTree.uasset b/Content/Game/Data/Harvetable/DA_Harvest_OakTree.uasset new file mode 100644 index 0000000..3e3ead0 --- /dev/null +++ b/Content/Game/Data/Harvetable/DA_Harvest_OakTree.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2894a33b42c0add99e7d358a6d83ee70a99991d194c8e4fa5200d1a0cebbc42f +size 2661 diff --git a/Content/Game/Data/Harvetable/DA_Harvest_Rock.uasset b/Content/Game/Data/Harvetable/DA_Harvest_Rock.uasset new file mode 100644 index 0000000..3ae2f20 --- /dev/null +++ b/Content/Game/Data/Harvetable/DA_Harvest_Rock.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ceec6f00527979fefc0c335cd512b8b035287d4a9ed5033d1ee7a8a99359416 +size 2801 diff --git a/Content/Game/Data/Item/Tools/Wood/DA_Wood_Pickaxe.uasset b/Content/Game/Data/Item/Tools/Wood/DA_Wood_Pickaxe.uasset index eccdc56..e003243 100644 --- a/Content/Game/Data/Item/Tools/Wood/DA_Wood_Pickaxe.uasset +++ b/Content/Game/Data/Item/Tools/Wood/DA_Wood_Pickaxe.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8053b3317a74723072241173c8c61a98e0d0e3889c86c7d1e8b78e7fd1167766 -size 3841 +oid sha256:01633c4c1ad0e5dbb962aae909a4c2f044f26ed08654c2e05e967c4c37678f04 +size 3734 diff --git a/Content/Game/Maps/Dev/Dev_Scene.umap b/Content/Game/Maps/Dev/Dev_Scene.umap index 54896b2..1ac11e6 100644 --- a/Content/Game/Maps/Dev/Dev_Scene.umap +++ b/Content/Game/Maps/Dev/Dev_Scene.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65dcb7041dbed5451f8b1ee9dc92e7c38e9e4583422c0493aec8c3c1b1706702 -size 159390 +oid sha256:1e070e9b21be3807b063a2c81c68ea1280f6999b375b58d54523af6e587f43fe +size 159221 diff --git a/Content/Game/Material/Tools/Woods/Pickaxe/M_Pickaxe.uasset b/Content/Game/Material/Tools/Woods/Pickaxe/M_Pickaxe.uasset index e86ebb6..a281617 100644 --- a/Content/Game/Material/Tools/Woods/Pickaxe/M_Pickaxe.uasset +++ b/Content/Game/Material/Tools/Woods/Pickaxe/M_Pickaxe.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:629d0dfc4edc53995147f36f8e8023e126bf95332fe5c8ffba88972fd00de8e7 -size 16824 +oid sha256:74170a2323c1fd6823fcaef7d5359152ce1a84edd6e53c2c239eee45928e44b2 +size 14226 diff --git a/Content/Game/Player/BP_FpsPlayer.uasset b/Content/Game/Player/BP_FpsPlayer.uasset index d35a0f2..0d3f512 100644 --- a/Content/Game/Player/BP_FpsPlayer.uasset +++ b/Content/Game/Player/BP_FpsPlayer.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a985cbfa0063f43d44d571c7cd729ae1eb33e483c0d8aa2224f004ebe8e385cc -size 43065 +oid sha256:bb25655fc6372a0a64be27099d857a420cbd1cbed96a82ad9344e00ecb3c7905 +size 43338 diff --git a/EmberWild.uproject b/EmberWild.uproject index b0d9dee..ddf2147 100644 --- a/EmberWild.uproject +++ b/EmberWild.uproject @@ -32,6 +32,13 @@ "Editor" ] }, + { + "Name": "PythonScriptPlugin", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, { "Name": "OnlineSubsystemSteam", "Enabled": true diff --git a/Scripts/reorganize_content.py b/Scripts/reorganize_content.py new file mode 100644 index 0000000..fc1867d --- /dev/null +++ b/Scripts/reorganize_content.py @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- +""" +Rangement unique de Content/Game. + +A lancer depuis l'editeur : Tools > Execute Python Script. +Mettre DRY_RUN a True pour une simulation qui ne touche a rien, puis a False. + +Pourquoi un script et pas du drag & drop : ~70 operations, dont des renommages +qui doivent se faire dans un ordre precis (un materiau prend le nom qu'une +instance vient de liberer). Une erreur de destination se voit ici, pas trois +jours plus tard sur un asset au mauvais endroit. + +Unreal se charge lui-meme de reecrire les references : rename_asset laisse un +redirector derriere lui, et fixup_referencers() les resout a la fin. +""" + +import unreal + +DRY_RUN = True + +EAL = unreal.EditorAssetLibrary + +# --------------------------------------------------------------------------- +# Assets a supprimer -- verifie sans aucun referenceur dans tout le projet. +# --------------------------------------------------------------------------- +DELETIONS = [ + # Doublon reel : le M_Pickaxe utilise par le mesh est celui de 3d/, pas celui-ci. + "/Game/Game/Material/Tools/Woods/Pickaxe/M_Pickaxe", + "/Game/Game/Audio/Chest/Chest_Closes_2", +] + +# --------------------------------------------------------------------------- +# Renommages d'assets. L'ORDRE COMPTE dans le bloc Table : l'instance libere +# le nom M_Table avant que le materiau parent ne le prenne. +# --------------------------------------------------------------------------- +ASSET_MOVES = [ + # --- Art / Building / Table ------------------------------------------- + ("/Game/Game/3d/Building/Table/M_Table", + "/Game/Game/Art/Building/Table/MI_Table"), + ("/Game/Game/3d/Building/Table/textures/M_table_albedo_Mat", + "/Game/Game/Art/Building/Table/M_Table"), + ("/Game/Game/3d/Building/Table/Table_FBX", + "/Game/Game/Art/Building/Table/SM_Table"), + ("/Game/Game/3d/Building/Table/textures/M_table_albedo", + "/Game/Game/Art/Building/Table/T_Table_BaseColor"), + ("/Game/Game/3d/Building/Table/textures/M_table_Metal", + "/Game/Game/Art/Building/Table/T_Table_Metallic"), + ("/Game/Game/3d/Building/Table/textures/M_table_Normal", + "/Game/Game/Art/Building/Table/T_Table_Normal"), + ("/Game/Game/3d/Building/Table/textures/M_table_Roughness", + "/Game/Game/Art/Building/Table/T_Table_Roughness"), + + # --- Art / Cooking ----------------------------------------------------- + # Les suffixes numeriques sont conserves : rien ne dit lequel est lequel, + # et inventer un nom parlant ici serait une supposition. + ("/Game/Game/3d/Cooking/Campfire_Logs", + "/Game/Game/Art/Cooking/SM_Campfire_Logs"), + ("/Game/Game/3d/Cooking/Campfire_Logs_001", + "/Game/Game/Art/Cooking/SM_Campfire_Logs_001"), + ("/Game/Game/3d/Cooking/Campfire_Logs_002", + "/Game/Game/Art/Cooking/SM_Campfire_Logs_002"), + ("/Game/Game/3d/Cooking/Campfire_Logs_003", + "/Game/Game/Art/Cooking/SM_Campfire_Logs_003"), + ("/Game/Game/3d/Cooking/LogS", + "/Game/Game/Art/Cooking/SM_LogS"), + ("/Game/Game/3d/Cooking/Logs1", + "/Game/Game/Art/Cooking/SM_Logs1"), + ("/Game/Game/3d/Cooking/Rocks_003", + "/Game/Game/Art/Cooking/MI_Rocks_003"), + ("/Game/Game/3d/Cooking/Wood_004", + "/Game/Game/Art/Cooking/MI_Wood_004"), + ("/Game/Game/3d/Cooking/Wood_light_001", + "/Game/Game/Art/Cooking/MI_Wood_light_001"), + + # --- Art / Tools / Axe ------------------------------------------------- + # Le materiau s'appelait "Wood_Axe", exactement comme le mesh : deux assets + # homonymes a deux endroits, c'est ce qui rendait le dossier illisible. + ("/Game/Game/3d/Tools/Wood_Axe", + "/Game/Game/Art/Tools/Axe/SM_Wood_Axe"), + ("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe", + "/Game/Game/Art/Tools/Axe/M_Wood_Axe"), + ("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_BaseColor", + "/Game/Game/Art/Tools/Axe/T_Wood_Axe_BaseColor"), + ("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_Normal", + "/Game/Game/Art/Tools/Axe/T_Wood_Axe_Normal"), + ("/Game/Game/3d/Tools/Wood_Axe_Texture/Wood_Axe_OcclusionRoughnessMetallic", + "/Game/Game/Art/Tools/Axe/T_Wood_Axe_ORM"), + # Orphelin, conserve : la hache en bois est un chantier ouvert. + ("/Game/Game/Material/Tools/Woods/Axe/M_Axe", + "/Game/Game/Art/Tools/Axe/M_Axe"), + + # --- Art / Tools / Pickaxe --------------------------------------------- + # Les textures portaient le nom de la hache alors qu'elles alimentent la + # pioche : on remet le nom en accord avec l'usage reel. + ("/Game/Game/3d/Tools/Wood_Pickaxe", + "/Game/Game/Art/Tools/Pickaxe/SM_Wood_Pickaxe"), + ("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/M_Pickaxe", + "/Game/Game/Art/Tools/Pickaxe/M_Wood_Pickaxe"), + ("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_BaseColor_sRGB", + "/Game/Game/Art/Tools/Pickaxe/T_Wood_Pickaxe_BaseColor"), + ("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_Axe_Normal_Raw", + "/Game/Game/Art/Tools/Pickaxe/T_Wood_Pickaxe_Normal"), + ("/Game/Game/3d/Tools/Wood_Pickaxe_Texture/Wood_AxeOcclusionRoughnessMetallic_Raw", + "/Game/Game/Art/Tools/Pickaxe/T_Wood_Pickaxe_ORM"), + + # --- Art / Tools / Sword ----------------------------------------------- + ("/Game/Game/3d/Tools/Wood_Sword", + "/Game/Game/Art/Tools/Sword/SM_Wood_Sword"), + ("/Game/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_BaseColor", + "/Game/Game/Art/Tools/Sword/T_Wood_Sword_BaseColor"), + ("/Game/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_Normal", + "/Game/Game/Art/Tools/Sword/T_Wood_Sword_Normal"), + ("/Game/Game/3d/Tools/Wood_Sword_Texture/Wood_Sword_OcclusionRoughnessMetallic", + "/Game/Game/Art/Tools/Sword/T_Wood_Sword_ORM"), + + # --- Art / Character / Arms -------------------------------------------- + ("/Game/Game/Material/Fps_Arm/M_Fps_Arm", + "/Game/Game/Art/Character/Arms/M_Fps_Arm"), + ("/Game/Game/Material/Fps_Arm/MaleBody_M_Base_color_sRGB", + "/Game/Game/Art/Character/Arms/T_Fps_Arm_BaseColor"), + + # --- Blueprints -------------------------------------------------------- + # Framework = ce qui n'est pas un acteur du monde : GameMode, GameInstance, + # PlayerControllers. Le critere est celui de CLAUDE.md, pas le type d'asset. + ("/Game/Game/Player/BP_FpsPlayer", + "/Game/Game/Blueprints/Player/BP_FpsPlayer"), + ("/Game/Game/Player/BP_FpsPlayerController", + "/Game/Game/Blueprints/Framework/BP_FpsPlayerController"), + ("/Game/Game/Menu/BP_MainMenuPlayerController", + "/Game/Game/Blueprints/Framework/BP_MainMenuPlayerController"), + ("/Game/Game/GameMode/GM_FpsPlayer", + "/Game/Game/Blueprints/Framework/GM_FpsPlayer"), + ("/Game/Game/GameMode/GM_MainMenu", + "/Game/Game/Blueprints/Framework/GM_MainMenu"), + ("/Game/Game/Game-Instance/BP_SurvivalGameInstance", + "/Game/Game/Blueprints/Framework/BP_SurvivalGameInstance"), + ("/Game/Game/PickupItem/BP_Pickup_Test", + "/Game/Game/Blueprints/World/BP_Pickup_Test"), + ("/Game/Game/Building/Cooking/BP_Campfire", + "/Game/Game/Blueprints/World/BP_Campfire"), + ("/Game/Game/Building/Crafting/BP_Workbench", + "/Game/Game/Blueprints/World/BP_Workbench"), + ("/Game/Game/Building/Storage/BP_Chest_Wooden", + "/Game/Game/Blueprints/World/BP_Chest_Wooden"), + ("/Game/Game/BP/Menu/CharaterCustomisation/BP_CharacterPreview", + "/Game/Game/Blueprints/Menu/BP_CharacterPreview"), +] + +# Renommages qui doivent avoir lieu APRES un deplacement de dossier, parce que +# leur chemin source n'existe qu'une fois le dossier deplace. +ASSET_MOVES_LATE = [ + # Le squelette n'est pas une animation : il sort de Sequences/. + ("/Game/Game/Animation/FirstPerson/Sequences/BasePos_Skeleton", + "/Game/Game/Animation/FirstPerson/BasePos_Skeleton"), + # Nom colle deux fois par un import rate. + ("/Game/Game/UI/Widgets/Player/WBP_PlayerNameplateWBP_PlayerNameplate", + "/Game/Game/UI/Widgets/Player/WBP_PlayerNameplate"), +] + +# --------------------------------------------------------------------------- +# Deplacements de dossiers entiers. +# --------------------------------------------------------------------------- +DIR_MOVES = [ + # --- Animation --------------------------------------------------------- + ("/Game/Game/Animation/Fps", + "/Game/Game/Animation/FirstPerson/Sequences"), + ("/Game/Game/Animation/ThirdPerson/RetargetedAnimations", + "/Game/Game/Animation/ThirdPerson/Sequences"), + + # --- Audio : fusion de Sounds/ dans Audio/ ----------------------------- + ("/Game/Game/Sounds/Mix", "/Game/Game/Audio/Mix"), + ("/Game/Game/Sounds/Music", "/Game/Game/Audio/Music"), + ("/Game/Game/Sounds/Playlists", "/Game/Game/Audio/Playlists"), + ("/Game/Game/Sounds/UI", "/Game/Game/Audio/UI"), + # FootSteps -> Footsteps : Windows ne distingue pas la casse, donc on passe + # par un nom intermediaire, sinon le renommage est un no-op silencieux. + ("/Game/Game/Audio/FootSteps", "/Game/Game/Audio/_TmpFootsteps"), + ("/Game/Game/Audio/_TmpFootsteps", "/Game/Game/Audio/Footsteps"), + + # --- Data -------------------------------------------------------------- + ("/Game/Game/Data/Harvetable", "/Game/Game/Data/Harvestable"), + ("/Game/Game/Data/Item", "/Game/Game/Data/Items"), + + # --- UI : les widgets d'un cote, les ressources de l'autre ------------- + ("/Game/Game/UI/Images/512x512", "/Game/Game/UI/Icons/Items"), + ("/Game/Game/UI/Images/Tools", "/Game/Game/UI/Icons/Tools"), + ("/Game/Game/UI/Font", "/Game/Game/UI/Fonts"), + ("/Game/Game/UI/Confirm", "/Game/Game/UI/Widgets/Confirm"), + ("/Game/Game/UI/Craft", "/Game/Game/UI/Widgets/Craft"), + ("/Game/Game/UI/CrossHair", "/Game/Game/UI/Widgets/Crosshair"), + ("/Game/Game/UI/Customization", "/Game/Game/UI/Widgets/Customization"), + ("/Game/Game/UI/Fade", "/Game/Game/UI/Widgets/Fade"), + ("/Game/Game/UI/Interaction", "/Game/Game/UI/Widgets/Interaction"), + ("/Game/Game/UI/Inventory", "/Game/Game/UI/Widgets/Inventory"), + ("/Game/Game/UI/Menu", "/Game/Game/UI/Widgets/MainMenu"), + ("/Game/Game/UI/Pause", "/Game/Game/UI/Widgets/Pause"), + ("/Game/Game/UI/Player", "/Game/Game/UI/Widgets/Player"), + ("/Game/Game/UI/Setting", "/Game/Game/UI/Widgets/Settings"), + ("/Game/Game/UI/Stats", "/Game/Game/UI/Widgets/Stats"), +] + +# Les deux icones egarees hors de Content/Game. +STRAY_MOVES = [ + ("/Game/UI/Icons/T_Icon_Food_06", "/Game/Game/UI/Icons/Items/T_Icon_Food_06"), + ("/Game/UI/Icons/T_Icon_Food_07", "/Game/Game/UI/Icons/Items/T_Icon_Food_07"), +] + +# Dossiers vides une fois tout deplace ET les redirectors resolus. +EMPTY_DIRS = [ + "/Game/Game/3d", + "/Game/Game/Material", + "/Game/Game/Player", + "/Game/Game/Menu", + "/Game/Game/GameMode", + "/Game/Game/Game-Instance", + "/Game/Game/PickupItem", + "/Game/Game/Building", + "/Game/Game/BP", + "/Game/Game/Sounds", + "/Game/Game/Animation/Anim_BP", + "/Game/Game/UI/Images", + "/Game/UI", +] + +# --------------------------------------------------------------------------- + +stats = {"ok": 0, "skip": 0, "fail": 0} + + +def log(kind, message): + stats[kind] = stats.get(kind, 0) + 1 + prefix = {"ok": " OK ", "skip": " SKIP ", "fail": " FAIL "}[kind] + unreal.log("{}{}".format(prefix, message)) + + +def move_asset(src, dst): + if not EAL.does_asset_exist(src): + log("skip", "source absente : {}".format(src)) + return + if EAL.does_asset_exist(dst): + log("fail", "destination DEJA OCCUPEE : {}".format(dst)) + return + if DRY_RUN: + log("ok", "{} -> {}".format(src, dst)) + return + if EAL.rename_asset(src, dst): + log("ok", "{} -> {}".format(src, dst)) + else: + log("fail", "rename_asset a echoue : {}".format(src)) + + +def move_dir(src, dst): + if not EAL.does_directory_exist(src): + log("skip", "dossier absent : {}".format(src)) + return + if DRY_RUN: + log("ok", "[dir] {} -> {}".format(src, dst)) + return + if EAL.rename_directory(src, dst): + log("ok", "[dir] {} -> {}".format(src, dst)) + else: + log("fail", "rename_directory a echoue : {}".format(src)) + + +def delete_asset(path): + if not EAL.does_asset_exist(path): + log("skip", "deja absent : {}".format(path)) + return + if DRY_RUN: + log("ok", "[del] {}".format(path)) + return + if EAL.delete_asset(path): + log("ok", "[del] {}".format(path)) + else: + log("fail", "delete_asset a echoue : {}".format(path)) + + +def fixup_redirectors(): + """Resout tous les ObjectRedirector du projet et les supprime. + + C'est l'etape que personne n'avait lancee : deux redirectors de 2024 + trainaient encore dans Data/Item. + """ + registry = unreal.AssetRegistryHelpers.get_asset_registry() + registry.search_all_assets(True) + + asset_filter = unreal.ARFilter( + class_paths=[unreal.TopLevelAssetPath("/Script/CoreUObject", "ObjectRedirector")], + package_paths=["/Game"], + recursive_paths=True, + ) + found = registry.get_assets(asset_filter) + unreal.log("Redirectors trouves : {}".format(len(found))) + if not found: + return + + for data in found: + unreal.log(" {}".format(data.package_name)) + + if DRY_RUN: + return + + redirectors = [data.get_asset() for data in found] + unreal.AssetToolsHelpers.get_asset_tools().fixup_referencers(redirectors) + + +def run(): + mode = "SIMULATION (rien n'est modifie)" if DRY_RUN else "EXECUTION REELLE" + unreal.log("=" * 70) + unreal.log("Rangement de Content/Game -- {}".format(mode)) + unreal.log("=" * 70) + + unreal.log("\n--- 1. Redirectors existants ---") + fixup_redirectors() + + unreal.log("\n--- 2. Suppressions ---") + for path in DELETIONS: + delete_asset(path) + + unreal.log("\n--- 3. Assets ---") + for src, dst in ASSET_MOVES: + move_asset(src, dst) + + unreal.log("\n--- 4. Dossiers ---") + for src, dst in DIR_MOVES: + move_dir(src, dst) + + unreal.log("\n--- 5. Assets dependant d'un dossier deplace ---") + for src, dst in ASSET_MOVES_LATE: + move_asset(src, dst) + + unreal.log("\n--- 6. Icones egarees hors de Content/Game ---") + for src, dst in STRAY_MOVES: + move_asset(src, dst) + + # Obligatoire avant de supprimer les dossiers d'origine : chaque + # deplacement y a laisse un redirector, donc ils ne sont pas vides. + unreal.log("\n--- 7. Resolution des redirectors crees ---") + fixup_redirectors() + + unreal.log("\n--- 8. Dossiers vides ---") + for path in EMPTY_DIRS: + if not EAL.does_directory_exist(path): + log("skip", "deja absent : {}".format(path)) + continue + if DRY_RUN: + log("ok", "[rmdir] {}".format(path)) + continue + if EAL.delete_directory(path): + log("ok", "[rmdir] {}".format(path)) + else: + log("fail", "dossier non vide : {}".format(path)) + + if not DRY_RUN: + unreal.log("\n--- 9. Sauvegarde ---") + EAL.save_directory("/Game/Game", only_if_is_dirty=False, recursive=True) + + unreal.log("\n" + "=" * 70) + unreal.log("Termine -- OK: {} SKIP: {} FAIL: {}".format( + stats["ok"], stats["skip"], stats["fail"])) + if stats["fail"]: + unreal.log_error("{} operation(s) en echec : lire le log ci-dessus.".format(stats["fail"])) + unreal.log("=" * 70) + + +run() diff --git a/Source/EmberWild/Private/FpsPlayer.cpp b/Source/EmberWild/Private/FpsPlayer.cpp index 2b53380..28a6a09 100644 --- a/Source/EmberWild/Private/FpsPlayer.cpp +++ b/Source/EmberWild/Private/FpsPlayer.cpp @@ -19,6 +19,7 @@ #include "CraftingComponent.h" #include "FootstepComponent.h" #include "FpsPlayerController.h" +#include "HarvestingComponent.h" #include "HeadBobComponent.h" #include "HeldItemComponent.h" #include "InteractionComponent.h" @@ -115,6 +116,7 @@ AFpsPlayer::AFpsPlayer() AppearanceComponent = CreateDefaultSubobject(TEXT("AppearanceComponent")); HeldItemComponent = CreateDefaultSubobject(TEXT("HeldItemComponent")); FootstepComponent = CreateDefaultSubobject(TEXT("FootstepComponent")); + HarvestingComponent = CreateDefaultSubobject(TEXT("HarvestingComponent")); // En vue FPS on ne veut pas voir son propre corps (mesh 3e personne), // mais on garde son ombre. @@ -786,52 +788,113 @@ void AFpsPlayer::TriggerHeldItemAction() return; } + // Le trace part d'ICI, chez celui qui frappe, et sans condition sur ce qu'il + // tient. Deux raisons de ne pas le reserver aux outils : une ressource dont + // RequiredTool vaut None se cueille a mains nues, et lire l'inventaire local + // pour trier ferait dependre le trace d'un etat qui peut avoir une frame de + // retard. Le cout est un unique balayage spherique par CLIC -- pas par frame. + FVector ImpactPoint; + FVector HitDirection; + AActor* SwingTarget = HarvestingComponent + ? HarvestingComponent->TraceSwingTarget(ImpactPoint, HitDirection) + : nullptr; + // L'hote agit tout de suite, le client demande. On envoie meme si le slot // parait vide chez nous : c'est le serveur qui fait foi, et son inventaire // peut avoir une frame d'avance sur le notre. if (!HasAuthority()) { - Server_UseItem(PendingUseSlotIndex); + Server_UseItem(PendingUseSlotIndex, SwingTarget, ImpactPoint, HitDirection); return; } - PerformUseItem(PendingUseSlotIndex); + PerformUseItem(PendingUseSlotIndex, SwingTarget, ImpactPoint, HitDirection); } -void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex) +void AFpsPlayer::Server_UseItem_Implementation(int32 SlotIndex, AActor* SwingTarget, + FVector_NetQuantize ImpactPoint, FVector_NetQuantizeNormal HitDirection) { - PerformUseItem(SlotIndex); + PerformUseItem(SlotIndex, SwingTarget, ImpactPoint, HitDirection); } -void AFpsPlayer::PerformUseItem(int32 SlotIndex) +void AFpsPlayer::PerformUseItem(int32 SlotIndex, AActor* SwingTarget, const FVector& ImpactPoint, + const FVector& HitDirection) { - if (!HasAuthority() || !InventoryComponent->GetSlots().IsValidIndex(SlotIndex)) + if (!HasAuthority()) { return; } - const FInventorySlot Selected = InventoryComponent->GetSlots()[SlotIndex]; - if (Selected.IsEmpty()) + // Un slot invalide ou vide n'est PAS une sortie : c'est le cas des mains nues, + // qui frappent quand meme -- un buisson de baies se cueille au poing. La + // version precedente sortait ici, quand seuls les consommables agissaient. + const FInventorySlot* Selected = InventoryComponent->GetSlots().IsValidIndex(SlotIndex) + ? &InventoryComponent->GetSlots()[SlotIndex] + : nullptr; + + const UItemDataAsset* Item = (Selected && !Selected->IsEmpty()) ? Selected->Item.Get() : nullptr; + + if (!Item) + { + PerformToolSwing(nullptr, INDEX_NONE, SwingTarget, ImpactPoint, HitDirection); + return; + } + + if (Item->bIsConsumable) + { + // Les valeurs de restauration sont appliquees a CHAQUE utilisation. + SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore); + + // ConsumeUse gere les deux cas : usage unique (l'exemplaire disparait) et + // usages multiples (une charge en moins, l'objet reste tant qu'il en a). + // On vise le slot actif precisement, et pas RemoveItem qui piocherait + // n'importe quelle pile du meme objet, y compris dans le sac. + InventoryComponent->ConsumeUse(SlotIndex); + return; + } + + // Tout le reste frappe : les outils avec leur puissance, les autres objets + // comme un poing. Un objet qui n'est ni consommable ni outil ne peut donc rien + // contre un arbre -- AHarvestableResource::AcceptsTool l'y refuse -- mais + // cueille les ressources a mains nues, ce qui est exactement ce qu'on veut : + // tenir une pomme n'empeche pas de ramasser des baies. + PerformToolSwing(Item, SlotIndex, SwingTarget, ImpactPoint, HitDirection); +} + +void AFpsPlayer::PerformToolSwing(const UItemDataAsset* Tool, int32 SlotIndex, AActor* SwingTarget, + const FVector& ImpactPoint, const FVector& HitDirection) +{ + if (!HarvestingComponent || !SwingTarget) { return; } - const UItemDataAsset* Item = Selected.Item; + const EHarvestResult Result = HarvestingComponent->ApplySwing(SwingTarget, Tool, ImpactPoint, HitDirection); - // Objet non consommable : on ne fait rien pour l'instant. C'est ici que - // viendront les outils (frapper), les armes (tirer), les placeables. - if (!Item->bIsConsumable) + // SEULS les deux resultats qui portent usent l'outil. Un mauvais outil, une + // souche ou un coup dans le vide n'en consomment aucune charge : abimer une + // hache en tapant un rocher serait une punition que rien a l'ecran n'explique. + if (Result != EHarvestResult::Harvested && Result != EHarvestResult::Depleted) { return; } - // Les valeurs de restauration sont appliquees a CHAQUE utilisation. - SurvivalStats->ApplyRestore(Item->HealthRestore, Item->HungerRestore, Item->ThirstRestore); + if (!Tool || !Tool->IsTool() || SlotIndex == INDEX_NONE) + { + return; + } + + // Un outil laisse a MaxUses = 1 disparaitrait au PREMIER coup : ConsumeUse + // retire alors l'exemplaire entier. On refuse plutot que de faire s'evaporer + // la hache que le joueur vient de fabriquer, et on le dit -- le symptome, lui, + // se lit comme « ma hache a disparu » et se cherche tres longtemps. + if (Tool->MaxUses <= 1) + { + UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer : l'outil %s a MaxUses = 1, il casserait au premier coup. Usure ignoree."), + *GetNameSafe(Tool)); + return; + } - // ConsumeUse gere les deux cas : usage unique (l'exemplaire disparait) et - // usages multiples (une charge en moins, l'objet reste tant qu'il en a). - // On vise le slot actif precisement, et pas RemoveItem qui piocherait - // n'importe quelle pile du meme objet, y compris dans le sac. InventoryComponent->ConsumeUse(SlotIndex); } @@ -940,6 +1003,11 @@ bool AFpsPlayer::DropFromInventory(UInventoryComponent* Inventory, int32 SlotInd } void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses) +{ + DropAtFeet(Item, Quantity, RemainingUses); +} + +void AFpsPlayer::DropAtFeet(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses) { if (!Item || Quantity <= 0) { @@ -948,7 +1016,7 @@ void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 if (!DroppedPickupClass) { - UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sortis de la fabrication sont perdus."), + UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sont perdus."), Quantity, *GetNameSafe(Item)); return; } @@ -960,16 +1028,43 @@ void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 // Aux pieds et non devant les yeux comme DropSlot : le joueur fabrique le // nez dans un menu, il ne vise rien. Poser a distance risquerait de traverser - // une cloison qu'on ne regarde meme pas. + // une cloison qu'on ne regarde meme pas. Le meme raisonnement vaut pour le + // surplus de recolte -- le joueur regarde l'arbre, pas le sol. const FVector DropLocation = GetActorLocation() + GetActorForwardVector() * 40.f; if (!SpawnPickup(Overflow, PlaceOnGround(DropLocation))) { - UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s issus de la fabrication."), + UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : impossible de poser au sol %d x %s."), Quantity, *GetNameSafe(Item)); } } +int32 AFpsPlayer::GiveItemOrDrop(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses) +{ + if (!Item || Quantity <= 0 || !InventoryComponent) + { + return 0; + } + + // Garde-fou et pas simple precaution : engendrer un pickup chez un client + // donnerait un objet visible de lui seul, efface au premier nettoyage reseau. + // L'appelant normal -- AHarvestableResource -- est deja cote serveur. + if (!HasAuthority()) + { + UE_LOG(LogTemp, Warning, TEXT("AFpsPlayer::GiveItemOrDrop appelee sans autorite pour %s, ignoree."), + *GetNameSafe(Item)); + return 0; + } + + const int32 Rejected = InventoryComponent->AddItem(Item, Quantity, RemainingUses); + if (Rejected > 0) + { + DropAtFeet(Item, Rejected, RemainingUses); + } + + return Quantity - Rejected; +} + void AFpsPlayer::DropAllItems() { // Declenche par la mort, qui sera arbitree par le serveur. La garde est la diff --git a/Source/EmberWild/Private/HarvestableDataAsset.cpp b/Source/EmberWild/Private/HarvestableDataAsset.cpp new file mode 100644 index 0000000..c4a1d19 --- /dev/null +++ b/Source/EmberWild/Private/HarvestableDataAsset.cpp @@ -0,0 +1,71 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "HarvestableDataAsset.h" + +#include "ItemDataAsset.h" + +#if WITH_EDITOR +#include "Misc/DataValidation.h" +#endif + +#define LOCTEXT_NAMESPACE "HarvestableData" + +int32 FHarvestDrop::Roll() const +{ + if (!Item) + { + return 0; + } + + // Le jet de chance d'abord : une ligne a 30 % qui echoue ne doit pas consommer + // de tirage de quantite, sinon deux tables identiques a la chance pres ne + // donneraient pas la meme suite -- sans consequence en jeu, mais deroutant a + // deboguer avec une graine fixee. + if (Chance < 1.f && FMath::FRand() > Chance) + { + return 0; + } + + // Max borne par Min plutot que l'inverse : un designer qui saisit 3 puis 1 + // obtient 3, jamais un intervalle vide qui rendrait la ligne muette sans rien + // dire. RandRange est inclusif des deux cotes. + return FMath::RandRange(MinQuantity, FMath::Max(MinQuantity, MaxQuantity)); +} + +#if WITH_EDITOR +EDataValidationResult UHarvestableDataAsset::IsDataValid(FDataValidationContext& Context) const +{ + EDataValidationResult Result = Super::IsDataValid(Context); + + // Le symptome d'une ligne vide est fourbe : l'arbre se recolte, s'abat, et + // rend simplement moins que prevu. Rien dans le log, rien a l'ecran. + auto ValidateTable = [&Context, &Result](const TArray& Table, const FText& TableName) + { + for (int32 Index = 0; Index < Table.Num(); ++Index) + { + if (!Table[Index].Item) + { + Context.AddError(FText::Format( + LOCTEXT("DropNoItem", "{0}[{1}] has no 'Item' assigned: this line would silently yield nothing."), + TableName, FText::AsNumber(Index))); + Result = EDataValidationResult::Invalid; + } + } + }; + + ValidateTable(PerHitDrops, LOCTEXT("PerHitTable", "Per Hit Drops")); + ValidateTable(DepletionDrops, LOCTEXT("DepletionTable", "Depletion Drops")); + + // Une ressource sans aucun butin est frappable, s'epuise, et ne donne rien. + // C'est legitime pour un decor destructible ; ca ne l'est presque jamais ici. + if (PerHitDrops.IsEmpty() && DepletionDrops.IsEmpty()) + { + Context.AddWarning(LOCTEXT("NoDropsAtAll", "This harvestable has no drops at all: harvesting it will consume tool durability for nothing.")); + } + + return Result; +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/EmberWild/Private/HarvestableResource.cpp b/Source/EmberWild/Private/HarvestableResource.cpp new file mode 100644 index 0000000..4f3a4a9 --- /dev/null +++ b/Source/EmberWild/Private/HarvestableResource.cpp @@ -0,0 +1,358 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "HarvestableResource.h" + +#include "Components/StaticMeshComponent.h" +#include "Engine/World.h" +#include "FpsPlayer.h" +#include "HarvestableDataAsset.h" +#include "ItemDataAsset.h" +#include "Kismet/GameplayStatics.h" +#include "NiagaraFunctionLibrary.h" +#include "Net/UnrealNetwork.h" +#include "TimerManager.h" + +AHarvestableResource::AHarvestableResource() +{ + // Le Tick ne sert QU'AU balancement, et il s'allume tout seul le temps de + // l'amortissement. Une foret de quatre cents arbres qui ticke en permanence + // pour ne rien faire est exactement ce qu'on evite ici. + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = false; + + // Acteur du monde : le compte des coups appartient au serveur. Quatre clients + // qui compteraient chacun leurs points de vie divergeraient, et l'arbre + // tomberait chez l'un alors qu'il tient encore chez l'autre. + bReplicates = true; + SetReplicateMovement(false); + + Mesh = CreateDefaultSubobject(TEXT("Mesh")); + SetRootComponent(Mesh); + + // QueryAndPhysics : l'arbre barre le passage en plus d'etre visable. Le trace + // du coup passe par le canal Visibility, que la collision par defaut bloque. + Mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics); + + // Le balancement bouge le mesh a l'execution : Static le cuirait sur place et + // la rotation n'aurait aucun effet a l'ecran, sans le moindre avertissement. + Mesh->SetMobility(EComponentMobility::Movable); +} + +void AHarvestableResource::GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const +{ + Super::GetLifetimeReplicatedProps(OutLifetimeProps); + + DOREPLIFETIME(AHarvestableResource, bDepleted); +} + +void AHarvestableResource::BeginPlay() +{ + Super::BeginPlay(); + + // Retenue ici et pas au constructeur : la rotation relative du mesh peut avoir + // ete reglee dans le Blueprint ou sur l'instance posee dans le niveau, et c'est + // a CETTE valeur que le balancement doit revenir. + BaseMeshRotation = Mesh->GetRelativeRotation(); + + // AVANT le premier ApplyDepletionVisuals, qui peut deja poser la souche chez un + // client arrivant sur un arbre abattu : on capturerait alors la souche comme + // etat intact, et l'arbre ne repousserait jamais. + IntactMesh = Mesh->GetStaticMesh(); + + if (HasAuthority() && HarvestData) + { + CurrentHealth = HarvestData->Health; + } + + // Sur toutes les machines : chez l'hote parce qu'aucun OnRep ne se declenche + // pour lui, chez un client parce que l'ordre d'arrivee entre la construction de + // l'acteur et ses proprietes repliquees n'est garanti par rien. + ApplyDepletionVisuals(); + + if (!HarvestData) + { + UE_LOG(LogTemp, Warning, TEXT("AHarvestableResource : %s n'a pas de HarvestData, il ne se recoltera jamais."), + *GetName()); + } +} + +bool AHarvestableResource::AcceptsTool(const UItemDataAsset* Tool) const +{ + if (!HarvestData) + { + return false; + } + + // Mains vides = EToolType::None, exactement comme un objet qui n'est pas un + // outil : tenir une pomme ne doit pas abattre plus qu'un poing. + const EToolType HeldType = (Tool && Tool->IsTool()) ? Tool->ToolType : EToolType::None; + + return HeldType == HarvestData->RequiredTool; +} + +EHarvestResult AHarvestableResource::TryHarvest(AFpsPlayer* Harvester, const UItemDataAsset* Tool, float Power, + const FVector& ImpactPoint, const FVector& HitDirection) +{ + // La garde est ici et pas seulement chez l'appelant : c'est le point d'entree + // unique du systeme, et rien ne garantit qu'un futur appelant y pense. + if (!HasAuthority() || !HarvestData || !Harvester || Power <= 0.f) + { + return EHarvestResult::Invalid; + } + + if (bDepleted) + { + // Un « toc » et rien d'autre : le joueur doit entendre que sa souche ne + // donne plus rien, sans y perdre de durabilite. + Multicast_PlayHarvestFeedback(EHarvestFeedback::Ineffective, ImpactPoint, HitDirection); + return EHarvestResult::AlreadyDepleted; + } + + if (!AcceptsTool(Tool)) + { + Multicast_PlayHarvestFeedback(EHarvestFeedback::Ineffective, ImpactPoint, HitDirection); + return EHarvestResult::WrongTool; + } + + CurrentHealth -= Power; + + // Le butin par coup part AVANT le test d'epuisement : le coup qui abat rend + // donc les deux tables. C'est ce que le joueur attend -- son dernier coup a + // bien entaille le tronc en plus de le faire tomber. + GrantDrops(HarvestData->PerHitDrops, Harvester); + + if (CurrentHealth > 0.f) + { + Multicast_PlayHarvestFeedback(EHarvestFeedback::Hit, ImpactPoint, HitDirection); + return EHarvestResult::Harvested; + } + + GrantDrops(HarvestData->DepletionDrops, Harvester); + Multicast_PlayHarvestFeedback(EHarvestFeedback::FinalBlow, ImpactPoint, HitDirection); + Deplete(); + + return EHarvestResult::Depleted; +} + +int32 AHarvestableResource::GrantDrops(const TArray& Table, AFpsPlayer* Harvester) +{ + int32 Granted = 0; + + for (const FHarvestDrop& Drop : Table) + { + const int32 Quantity = Drop.Roll(); + if (Quantity <= 0) + { + continue; + } + + // Passe par le pawn et pas par son inventaire : ce qui ne rentre pas doit + // tomber au sol, et un UInventoryComponent ne sait pas engendrer un acteur. + Harvester->GiveItemOrDrop(Drop.Item, Quantity); + ++Granted; + } + + return Granted; +} + +void AHarvestableResource::Deplete() +{ + bDepleted = true; + + // Chez l'hote aucun OnRep ne se declenche : on applique nous-memes juste apres + // avoir ecrit la propriete. Meme schema que SetCharacterAppearance. + ApplyDepletionVisuals(); + + const float Delay = HarvestData ? HarvestData->RegrowDelay : 0.f; + if (Delay <= 0.f) + { + return; + } + + GetWorldTimerManager().SetTimer(RegrowTimer, this, &AHarvestableResource::Regrow, Delay, /*bLoop=*/false); +} + +void AHarvestableResource::Regrow() +{ + if (!HasAuthority()) + { + return; + } + + CurrentHealth = HarvestData ? HarvestData->Health : 0.f; + bDepleted = false; + ApplyDepletionVisuals(); +} + +void AHarvestableResource::OnRep_Depleted() +{ + ApplyDepletionVisuals(); +} + +void AHarvestableResource::ApplyDepletionVisuals() +{ + if (!bDepleted) + { + SetActorHiddenInGame(false); + SetActorEnableCollision(true); + + // IntactMesh peut etre nul le tout premier coup -- BeginPlay ne l'a pas + // encore capture -- auquel cas il n'y a de toute facon rien a reposer. + if (IntactMesh && Mesh->GetStaticMesh() != IntactMesh) + { + Mesh->SetStaticMesh(IntactMesh); + } + + return; + } + + if (DepletedMesh) + { + Mesh->SetStaticMesh(DepletedMesh); + SetActorHiddenInGame(false); + + // Une souche reste solide : on la garde visable, ce qui permet au coup + // suivant de rencontrer quelque chose et de sonner « inefficace » plutot + // que de traverser dans le vide. + SetActorEnableCollision(true); + return; + } + + // Sans mesh de remplacement, la ressource disparait entierement. La collision + // PART avec elle : la laisser donnerait un mur invisible en pleine foret. + SetActorHiddenInGame(true); + SetActorEnableCollision(false); +} + +void AHarvestableResource::Multicast_PlayHarvestFeedback_Implementation(EHarvestFeedback Feedback, + FVector_NetQuantize ImpactPoint, FVector_NetQuantizeNormal HitDirection) +{ + PlayHarvestFeedback(Feedback, ImpactPoint, HitDirection); +} + +void AHarvestableResource::PlayHarvestFeedback(EHarvestFeedback Feedback, const FVector& ImpactPoint, + const FVector& HitDirection) +{ + if (!HarvestData) + { + return; + } + + // Un serveur dedie n'a ni haut-parleur ni ecran : les FX y seraient du travail + // pur. En serveur d'ecoute la fenetre de l'hote est un client comme un autre, + // donc elle passe cette garde. + if (IsNetMode(NM_DedicatedServer)) + { + return; + } + + USoundBase* Sound = nullptr; + UNiagaraSystem* Effect = nullptr; + + switch (Feedback) + { + case EHarvestFeedback::Hit: + Sound = HarvestData->HitSound; + Effect = HarvestData->HitEffect; + break; + + case EHarvestFeedback::FinalBlow: + // Le son de chute REMPLACE celui du coup, il ne s'y ajoute pas : deux + // impacts sur la meme frame s'entendent comme un doublon, pas comme un + // evenement plus fort. + Sound = HarvestData->DepletionSound ? HarvestData->DepletionSound.Get() : HarvestData->HitSound.Get(); + Effect = HarvestData->DepletionEffect ? HarvestData->DepletionEffect.Get() : HarvestData->HitEffect.Get(); + break; + + case EHarvestFeedback::Ineffective: + Sound = HarvestData->IneffectiveHitSound; + break; + } + + if (Sound) + { + UGameplayStatics::PlaySoundAtLocation(this, Sound, ImpactPoint); + } + + if (Effect) + { + // Les copeaux repartent VERS le joueur : -HitDirection. Oriente dans le sens + // du coup, ils gicleraient dans le tronc et on ne verrait rien. + UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, Effect, ImpactPoint, + (-HitDirection).Rotation()); + } + + // Pas de balancement sur un coup qui n'a rien fait : une souche qui oscille + // sous un coup inefficace annonce exactement le contraire de ce qui se passe. + if (Feedback != EHarvestFeedback::Ineffective) + { + StartShake(HitDirection); + } +} + +void AHarvestableResource::StartShake(const FVector& HitDirection) +{ + if (!HarvestData || HarvestData->HitShakeAngle <= 0.f || HarvestData->HitShakeDuration <= 0.f) + { + return; + } + + // L'axe de rotation est perpendiculaire au coup ET a la verticale : l'arbre + // penche DANS le sens ou on l'a frappe. Le produit vectoriel degenere quand on + // frappe a la verticale -- de haut en bas sur un rocher -- d'ou le repli. + FVector WorldAxis = FVector::CrossProduct(FVector::UpVector, HitDirection); + if (!WorldAxis.Normalize()) + { + WorldAxis = GetActorRightVector(); + } + + // En espace local : le mesh tourne dans le repere de son parent, et un arbre + // pose avec un yaw quelconque pencherait de travers si l'axe restait en monde. + ShakeAxis = GetActorTransform().InverseTransformVectorNoScale(WorldAxis).GetSafeNormal(); + + // Reparti de zero meme si un balancement tournait deja : marteler l'arbre doit + // relancer l'oscillation a pleine amplitude, pas l'accumuler. + ShakeElapsed = 0.f; + SetActorTickEnabled(true); +} + +void AHarvestableResource::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if (ShakeElapsed < 0.f || !HarvestData) + { + SetActorTickEnabled(false); + return; + } + + ShakeElapsed += DeltaTime; + + const float Duration = HarvestData->HitShakeDuration; + if (ShakeElapsed >= Duration) + { + // On repose la rotation d'origine EXACTEMENT plutot que de laisser la + // derniere valeur calculee : a un cheveu pres, l'arbre resterait penche, et + // vingt coups plus tard il pousserait franchement de travers. + Mesh->SetRelativeRotation(BaseMeshRotation); + ShakeElapsed = -1.f; + SetActorTickEnabled(false); + return; + } + + const float Alpha = ShakeElapsed / Duration; + + // Amortissement au carre plutot que lineaire : l'oscillation meurt vite, ce qui + // donne le poids d'un tronc. Lineaire, elle traine et l'arbre parait mou. + const float Damping = FMath::Square(1.f - Alpha); + + const float Angle = HarvestData->HitShakeAngle * Damping + * FMath::Sin(2.f * PI * HarvestData->HitShakeFrequency * ShakeElapsed); + + // Composition par quaternion et pas addition de FRotator : additionner deux + // rotations d'Euler n'est une composition correcte que par chance, et l'arbre + // pose avec un pitch non nul partirait de travers. + const FQuat Offset(ShakeAxis, FMath::DegreesToRadians(Angle)); + Mesh->SetRelativeRotation(Offset * BaseMeshRotation.Quaternion()); +} diff --git a/Source/EmberWild/Private/HarvestingComponent.cpp b/Source/EmberWild/Private/HarvestingComponent.cpp new file mode 100644 index 0000000..03f06b2 --- /dev/null +++ b/Source/EmberWild/Private/HarvestingComponent.cpp @@ -0,0 +1,147 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "HarvestingComponent.h" + +#include "DrawDebugHelpers.h" +#include "Engine/World.h" +#include "FpsPlayer.h" +#include "GameFramework/Controller.h" +#include "GameFramework/Pawn.h" +#include "HarvestableResource.h" +#include "ItemDataAsset.h" + +UHarvestingComponent::UHarvestingComponent() +{ + // Aucun Tick : ce composant ne travaille qu'au moment du coup, une poignee de + // fois par minute. C'est toute la difference avec UInteractionComponent, qui + // doit entretenir un prompt a l'ecran. + PrimaryComponentTick.bCanEverTick = false; + + // Pas de SetIsReplicatedByDefault : aucune RPC ne vit ici, elles sont sur le + // pawn. Voir le commentaire de classe. +} + +bool UHarvestingComponent::GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const +{ + const APawn* OwnerPawn = Cast(GetOwner()); + if (!OwnerPawn) + { + return false; + } + + // Par le controller et pas par la camera : GetPlayerViewPoint renvoie + // exactement ce que le joueur a a l'ecran, secousses et balancement de tete + // compris. Le coup part donc pile du centre du viseur. + if (const AController* OwnerController = OwnerPawn->GetController()) + { + OwnerController->GetPlayerViewPoint(OutLocation, OutRotation); + return true; + } + + return false; +} + +AActor* UHarvestingComponent::TraceSwingTarget(FVector& OutImpactPoint, FVector& OutDirection) const +{ + OutImpactPoint = FVector::ZeroVector; + OutDirection = FVector::ForwardVector; + + FVector ViewLocation; + FRotator ViewRotation; + if (!GetViewPoint(ViewLocation, ViewRotation)) + { + return nullptr; + } + + OutDirection = ViewRotation.Vector(); + const FVector TraceEnd = ViewLocation + OutDirection * SwingRange; + + FCollisionQueryParams Params(SCENE_QUERY_STAT(HarvestSwingTrace), /*bTraceComplex=*/false, GetOwner()); + + FHitResult Hit; + const bool bHit = GetWorld()->SweepSingleByChannel( + Hit, + ViewLocation, + TraceEnd, + FQuat::Identity, + SwingChannel, + FCollisionShape::MakeSphere(SwingRadius), + Params); + +#if ENABLE_DRAW_DEBUG + if (bDrawDebug) + { + const FColor Color = bHit ? FColor::Green : FColor::Red; + DrawDebugLine(GetWorld(), ViewLocation, TraceEnd, Color, false, 1.5f, 0, 1.f); + if (bHit) + { + DrawDebugSphere(GetWorld(), Hit.ImpactPoint, SwingRadius, 12, Color, false, 1.5f); + } + } +#endif + + if (!bHit) + { + return nullptr; + } + + // ImpactPoint et pas Location : Location est le centre de la sphere balayee a + // l'arret, donc a SwingRadius de la surface. Les copeaux en jailliraient a + // vingt-cinq centimetres du tronc, en plein air. + OutImpactPoint = Hit.ImpactPoint; + + return Hit.GetActor(); +} + +float UHarvestingComponent::GetSwingPower(const UItemDataAsset* Tool) const +{ + // Un objet qui n'est pas un outil frappe comme un poing : tenir une pomme ne + // doit ni empecher de cueillir une baie, ni aider a abattre un arbre. + return (Tool && Tool->IsTool()) ? Tool->HarvestPower : UnarmedHarvestPower; +} + +EHarvestResult UHarvestingComponent::ApplySwing(AActor* Target, const UItemDataAsset* Tool, + const FVector& ImpactPoint, const FVector& HitDirection) +{ + AActor* Owner = GetOwner(); + if (!Owner || !Owner->HasAuthority()) + { + return EHarvestResult::Invalid; + } + + AHarvestableResource* Resource = Cast(Target); + if (!IsValid(Resource)) + { + // Le cas normal, et de loin : la plupart des coups tombent sur le sol, un + // mur, ou rien du tout. Ce n'est pas une erreur, juste un coup dans le vide. + return EHarvestResult::Invalid; + } + + // Le client a vise il y a un aller-retour : entre-temps il a bouge, et la + // position que le serveur connait de lui est en retard. On revalide donc, avec + // la meme portee elargie par ServerRangeTolerance. + // + // La distance se mesure a la BOITE ENGLOBANTE de la cible et pas a son origine : + // le pivot d'un arbre de pack est au SOL, donc frapper le tronc a deux metres de + // haut mesurerait une distance bien plus grande que la portee du coup. Le + // symptome serait un grand arbre impossible a abattre pour un client, et + // parfaitement normal chez l'hote. + const FBox TargetBounds = Resource->GetComponentsBoundingBox(/*bNonColliding=*/true); + const float MaxDistanceSq = FMath::Square(SwingRange * ServerRangeTolerance); + + if (TargetBounds.ComputeSquaredDistanceToPoint(Owner->GetActorLocation()) > MaxDistanceSq) + { + UE_LOG(LogTemp, Verbose, TEXT("UHarvestingComponent : %s a frappe %s, hors de portee cote serveur."), + *GetNameSafe(Owner), *GetNameSafe(Resource)); + return EHarvestResult::Invalid; + } + + AFpsPlayer* Harvester = Cast(Owner); + if (!Harvester) + { + return EHarvestResult::Invalid; + } + + return Resource->TryHarvest(Harvester, Tool, GetSwingPower(Tool), ImpactPoint, HitDirection); +} diff --git a/Source/EmberWild/Public/FpsPlayer.h b/Source/EmberWild/Public/FpsPlayer.h index 3b81b1f..59cdc1d 100644 --- a/Source/EmberWild/Public/FpsPlayer.h +++ b/Source/EmberWild/Public/FpsPlayer.h @@ -3,6 +3,12 @@ #pragma once #include "CoreMinimal.h" +// FVector_NetQuantize / FVector_NetQuantizeNormal, dans la signature de +// Server_UseItem : un point d'impact au demi-centimetre pres et une direction +// deja normalisee tiennent en quelques octets la ou deux FVector pleins en +// coutent vingt-quatre, pour une precision dont personne ne verrait la difference +// sur un jet de copeaux. +#include "Engine/NetSerialization.h" #include "GameFramework/Character.h" #include "FpsPlayer.generated.h" @@ -12,6 +18,7 @@ class UCameraComponent; class UCharacterAppearanceComponent; class UCraftingComponent; class UFootstepComponent; +class UHarvestingComponent; class UHeadBobComponent; class UHeldItemComponent; class UInputAction; @@ -167,6 +174,9 @@ public: UFUNCTION(BlueprintPure, Category = "Audio") UFootstepComponent* GetFootstepComponent() const { return FootstepComponent; } + UFUNCTION(BlueprintPure, Category = "Harvesting") + UHarvestingComponent* GetHarvestingComponent() const { return HarvestingComponent; } + // ------------------------------------------------------------------ // Commandes console d'apparence // @@ -245,6 +255,23 @@ public: UFUNCTION(BlueprintCallable, Category = "Inventaire") void DropAllItems(); + /** + * Donne un objet au joueur, et pose au sol ce qui ne rentre pas. + * + * Le chemin normal de tout ce que le monde produit : récolte, surplus de + * fabrication, futur butin. Refuser parce que l'inventaire est plein ferait + * disparaître ce que le joueur vient d'obtenir sous ses yeux — c'est le + * genre de perte silencieuse qui coûte une session de jeu. + * + * Passe par `AddItem`, donc exactement comme un ramassage : piles entamées + * d'abord, puis barre rapide, puis grille. + * + * @return le nombre d'exemplaires réellement entrés dans l'inventaire ; le + * reste est au sol. + */ + UFUNCTION(BlueprintCallable, Category = "Inventaire") + int32 GiveItemOrDrop(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses = -1); + UFUNCTION(BlueprintPure, Category = "Survie") bool IsDead() const { return bIsDead; } @@ -472,6 +499,16 @@ protected: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Audio") TObjectPtr FootstepComponent; + /** + * Le coup d'outil : trace du geste et recolte. + * + * Sur le pawn parce qu'il frappe avec CE corps, depuis CETTE camera, et qu'un + * cadavre ne recolte plus. Pendant exact d'UInteractionComponent, dont il + * partage le schema reseau -- voir son commentaire de classe. + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Harvesting") + TObjectPtr HarvestingComponent; + // ------------------------------------------------------------------ // Enhanced Input : assets à assigner dans le Blueprint // ------------------------------------------------------------------ @@ -786,12 +823,34 @@ private: * Le slot est passé explicitement : SelectedHotbarIndex n'est PAS répliqué, * c'est une intention purement locale. Le serveur ne peut donc pas deviner * ce que le joueur avait en main. + * + * La CIBLE aussi voyage, pour la même raison de fond : elle a été tracée + * depuis le point de vue réel du joueur à l'instant du geste. La retracer + * côté serveur la retracerait depuis une position qui a un demi-aller-retour + * de retard — un joueur en mouvement raterait des arbres qu'il vise pourtant + * en plein. Le serveur revalide la distance, il ne refait pas la visée. */ UFUNCTION(Server, Reliable) - void Server_UseItem(int32 SlotIndex); + void Server_UseItem(int32 SlotIndex, AActor* SwingTarget, FVector_NetQuantize ImpactPoint, + FVector_NetQuantizeNormal HitDirection); /** L'utilisation réelle, côté autorité. */ - void PerformUseItem(int32 SlotIndex); + void PerformUseItem(int32 SlotIndex, AActor* SwingTarget, const FVector& ImpactPoint, + const FVector& HitDirection); + + /** + * Le coup d'outil côté autorité, une fois la cible validée. + * + * Séparée de `PerformUseItem` parce qu'elle porte une décision qui lui est + * propre : l'usure. Un coup qui ne porte pas — mauvais outil, souche, vide — + * ne consomme aucune charge, sinon taper un rocher à la hache l'abîmerait + * sans rien rendre. + * + * @param SlotIndex la case de l'outil, pour y prélever la charge. INDEX_NONE + * à mains nues. + */ + void PerformToolSwing(const UItemDataAsset* Tool, int32 SlotIndex, AActor* SwingTarget, + const FVector& ImpactPoint, const FVector& HitDirection); /** * Point d'entrée unique du sprint : applique en local ET prévient le serveur. @@ -839,6 +898,13 @@ private: UFUNCTION() void HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses); + /** + * Pose une pile aux pieds du joueur. Le fond commun de `HandleCraftOverflow` + * et de `GiveItemOrDrop` : deux copies finiraient par diverger, et l'une des + * deux oublierait le trace vers le sol. + */ + void DropAtFeet(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses); + /** Fait apparaitre un pickup deja configure a un endroit donne. */ APickupItem* SpawnPickup(const struct FInventorySlot& Slot, const FVector& Location); diff --git a/Source/EmberWild/Public/HarvestableDataAsset.h b/Source/EmberWild/Public/HarvestableDataAsset.h new file mode 100644 index 0000000..aa294c7 --- /dev/null +++ b/Source/EmberWild/Public/HarvestableDataAsset.h @@ -0,0 +1,180 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "ItemDataAsset.h" +#include "HarvestableDataAsset.generated.h" + +class UItemDataAsset; +class UNiagaraSystem; +class USoundBase; + +/** + * Une ligne de butin : « 1 a 3 Bois, 70 % du temps ». + * + * Le tirage n'a lieu QUE sur le serveur (AHarvestableResource::TryHarvest), donc + * l'aleatoire ne peut pas diverger d'une machine a l'autre -- les clients ne + * recoivent jamais le jet, seulement l'objet qui atterrit dans l'inventaire. + */ +USTRUCT(BlueprintType) +struct FHarvestDrop +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop") + TObjectPtr Item; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop", meta = (ClampMin = "1")) + int32 MinQuantity = 1; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop", meta = (ClampMin = "1")) + int32 MaxQuantity = 1; + + /** Probabilite que cette ligne tombe. 1 = toujours. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Drop", meta = (ClampMin = "0", ClampMax = "1")) + float Chance = 1.f; + + /** Tire une quantite pour ce coup. Renvoie 0 quand le jet echoue. */ + int32 Roll() const; +}; + +/** + * Les REGLES de recolte d'une ressource : ce qu'il faut pour l'abattre, ce + * qu'elle rend, en combien de temps elle repousse. + * + * Un asset par ESPECE, pas par instance ni par mesh : les quinze variantes de + * sapin du pack partagent toutes DA_Harvest_Tree, chacune posant son propre mesh + * sur son acteur. C'est la meme separation que UItemDataAsset et APickupItem -- + * la donnee decrit une categorie, l'acteur porte le visuel et l'etat. + * + * Ce qui ne doit JAMAIS venir ici : les points de vie restants, le fait qu'un + * arbre soit deja abattu, l'heure de sa repousse. C'est de l'etat, il vit sur + * AHarvestableResource -- sinon abattre un arbre abattrait toute la foret. + */ +UCLASS(BlueprintType) +class EMBERWILD_API UHarvestableDataAsset : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + /** Identifiant stable, pour la sauvegarde a venir. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable") + FName HarvestableId; + + /** Nom lisible, pour les logs et un futur message « Il vous faut une hache ». */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable") + FText DisplayName; + + /** + * Outil capable d'entamer cette ressource. + * + * `None` ne veut pas dire « n'importe quoi » mais « a mains nues » : c'est ce + * qui rend un buisson de baies cueillable au poing, sans code particulier. + * Un arbre demande Axe, un filon Pickaxe. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable") + EToolType RequiredTool = EToolType::Axe; + + /** + * Points a retirer avant epuisement. + * + * En points et pas en nombre de coups, pour que la hache en fer abatte en + * quatre coups l'arbre qui en demande dix a la hache de pierre : c'est + * `HarvestPower` de l'outil qui fait la difference. Un compte de coups en dur + * ici obligerait a dupliquer chaque ressource par palier d'outil. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable", meta = (ClampMin = "1")) + float Health = 100.f; + + /** + * Ce que rend CHAQUE coup, meme celui qui n'abat pas. + * + * C'est la moitie qui rend le geste satisfaisant : un arbre qui ne donne rien + * pendant neuf coups laisse croire qu'on tape dans le vide. Garde les + * quantites basses -- c'est le lot final qui doit peser. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable") + TArray PerHitDrops; + + /** Bonus du coup qui epuise la ressource. C'est la recompense de l'abattage. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable") + TArray DepletionDrops; + + /** + * Secondes avant que la ressource redevienne recoltable. 0 = jamais. + * + * Une souche qui repousse dispense d'un systeme de spawn tant que la carte + * est peuplee a la main : rien ne disparait, donc rien n'a a etre recree. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable", meta = (ClampMin = "0", Units = "s")) + float RegrowDelay = 180.f; + + // ------------------------------------------------------------------ + // Retour au joueur + // + // Sur la DONNEE et non sur l'acteur : c'est l'espece qui decide qu'un coup + // de hache dans du bois claque et qu'un coup de pioche dans la pierre + // resonne. Regler ca instance par instance sur quatre cents arbres poses + // dans le niveau serait ingerable. + // ------------------------------------------------------------------ + + /** Joue a chaque coup qui entame reellement la ressource. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback") + TObjectPtr HitSound; + + /** Copeaux, eclats de pierre. Joue au point d'impact, oriente par la normale. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback") + TObjectPtr HitEffect; + + /** + * Le coup n'a rien fait : mauvais outil, ou ressource deja epuisee. + * + * Un seul son pour les deux cas, et c'est volontaire : dans les deux le + * joueur doit entendre que son geste ne sert a rien. Le laisser vide donne un + * coup parfaitement muet, ce que l'oreille lit comme un bug. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback") + TObjectPtr IneffectiveHitSound; + + /** Craquement de chute, eboulement. Joue sur le coup qui epuise. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback") + TObjectPtr DepletionSound; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback") + TObjectPtr DepletionEffect; + + /** + * Angle maximal du balancement encaisse a chaque coup, en degres. + * + * 0 pour un rocher : la pierre ne plie pas, et la voir osciller casse + * immediatement l'illusion de masse. Quelques degres suffisent sur un arbre -- + * au-dela le tronc a l'air en caoutchouc. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback", + meta = (ClampMin = "0", ClampMax = "45", Units = "deg")) + float HitShakeAngle = 3.f; + + /** Duree du balancement amorti, en secondes. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback", + meta = (ClampMin = "0", Units = "s")) + float HitShakeDuration = 0.45f; + + /** Oscillations par seconde. Plus haut = plus nerveux, plus leger. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable|Feedback", meta = (ClampMin = "0.1")) + float HitShakeFrequency = 7.f; + + /** Vrai si cette ressource se recolte a mains nues. */ + UFUNCTION(BlueprintPure, Category = "Harvestable") + bool IsHarvestableByHand() const { return RequiredTool == EToolType::None; } + +#if WITH_EDITOR + /** + * Une table de butin dont une ligne pointe dans le vide ne planterait pas : + * l'arbre donnerait simplement moins, sans rien dire. C'est le genre de bug + * qu'on cherche une heure -- autant que l'editeur le refuse a la sauvegarde. + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Source/EmberWild/Public/HarvestableResource.h b/Source/EmberWild/Public/HarvestableResource.h new file mode 100644 index 0000000..360a16b --- /dev/null +++ b/Source/EmberWild/Public/HarvestableResource.h @@ -0,0 +1,242 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/NetSerialization.h" +#include "GameFramework/Actor.h" +#include "HarvestableResource.generated.h" + +class AFpsPlayer; +class UHarvestableDataAsset; +class UItemDataAsset; +class UStaticMesh; +class UStaticMeshComponent; + +/** + * Ce qu'a produit un coup, du point de vue de celui qui frappe. + * + * Un enum et pas un booleen, parce que l'appelant en tire trois conduites + * distinctes : un coup qui porte use l'outil et rend du butin, un mauvais outil + * ne doit RIEN user -- sinon taper un rocher a la hache la deteriore sans rien + * donner, ce que personne ne peut comprendre -- et une ressource deja epuisee + * n'est pas une erreur du joueur, juste un geste sans effet. + */ +UENUM(BlueprintType) +enum class EHarvestResult : uint8 +{ + /** Le coup a porte : du butin est parti, l'outil s'use. */ + Harvested, + + /** Le coup a epuise la ressource : butin final compris. */ + Depleted, + + /** L'outil en main ne peut rien contre cette ressource. */ + WrongTool, + + /** Deja epuisee ; elle repoussera peut-etre. */ + AlreadyDepleted, + + /** Cible invalide, donnee manquante : rien ne s'est produit. */ + Invalid +}; + +/** Ce que le multicast de retour doit jouer. Un seul RPC pour les trois cas. */ +UENUM() +enum class EHarvestFeedback : uint8 +{ + Hit, + FinalBlow, + Ineffective +}; + +/** + * Un arbre, un rocher, un buisson : quelque chose qu'on frappe pour en tirer des + * ressources. + * + * Ce n'est PAS un IInteractable, et c'est la decision structurante : on ne + * « valide » pas un arbre, on le frappe. Le coup part du marqueur + * UAnimNotify_ItemAction pose dans le montage de l'outil, donc au moment ou la + * hache touche -- jamais a l'appui. L'acteur n'a donc ni prompt, ni volume de + * visee dedie : le trace du coup vient toucher son MESH, comme un vrai coup. + * + * ------------------------------------------------------------------ + * Ou vit quoi + * ------------------------------------------------------------------ + * + * Les REGLES (outil requis, points de vie, butin, repousse) sont dans le + * UHarvestableDataAsset, partage par toute l'espece. Les MESHES sont ici, sur + * l'acteur : les quinze sapins du pack ont quinze visuels et une seule regle. + * Mettre le mesh sur la donnee obligerait a un DataAsset par variante, mettre la + * regle sur l'acteur obligerait a re-regler le butin sur quatre cents instances. + * + * ------------------------------------------------------------------ + * Reseau : le serveur compte, les clients regardent + * ------------------------------------------------------------------ + * + * CurrentHealth n'est PAS repliquee et ne doit jamais l'etre : c'est un compteur + * qui change a chaque coup pour un resultat que le joueur lit deja dans le + * balancement du tronc et dans les objets qui arrivent dans son sac. Seul + * bDepleted part sur le reseau -- une transition, pas un flux. + */ +UCLASS() +class EMBERWILD_API AHarvestableResource : public AActor +{ + GENERATED_BODY() + +public: + AHarvestableResource(); + + virtual void Tick(float DeltaTime) override; + virtual void GetLifetimeReplicatedProps(TArray& OutLifetimeProps) const override; + + /** + * Encaisse un coup. A n'appeler que sur le SERVEUR. + * + * @param Harvester celui qui frappe ; c'est son inventaire qui recoit. + * @param Tool l'objet en main, nul si les mains sont vides. + * @param Power points retires par ce coup. + * @param ImpactPoint point touche par le trace, pour les copeaux. + * @param HitDirection direction du coup, normalisee, pour le sens du balancement. + */ + EHarvestResult TryHarvest(AFpsPlayer* Harvester, const UItemDataAsset* Tool, float Power, + const FVector& ImpactPoint, const FVector& HitDirection); + + /** Vrai si l'outil fourni (nul = mains nues) peut entamer cette ressource. */ + UFUNCTION(BlueprintPure, Category = "Harvestable") + bool AcceptsTool(const UItemDataAsset* Tool) const; + + UFUNCTION(BlueprintPure, Category = "Harvestable") + bool IsDepleted() const { return bDepleted; } + + UFUNCTION(BlueprintPure, Category = "Harvestable") + UHarvestableDataAsset* GetHarvestData() const { return HarvestData; } + +protected: + virtual void BeginPlay() override; + + /** + * Le visuel ET la surface qu'on frappe. + * + * Le mesh porte lui-meme la collision, comme ACraftingStation et contrairement + * a APickupItem : un arbre est un obstacle, il a forcement une collision, et un + * volume ajoute par-dessus laisserait frapper le vide autour du tronc. + * Corollaire a verifier sur chaque mesh de pack : sans collision simple, le + * trace du coup ne rencontre rien et l'arbre est intouchable. + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Harvestable") + TObjectPtr Mesh; + + /** Les regles de recolte. Sans elle l'acteur est un simple decor. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable") + TObjectPtr HarvestData; + + /** + * Ce qu'on voit une fois la ressource epuisee : la souche, le filon vide. + * + * Vide = l'acteur se cache entierement, collision comprise. C'est un repli + * acceptable pour un buisson de baies -- personne ne remarque un buisson + * absent -- mais un arbre qui s'evapore se voit immediatement : donne-lui une + * souche. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Harvestable") + TObjectPtr DepletedMesh; + + /** + * Epuisee, en attente de repousse. + * + * ReplicatedUsing et pas simplement Replicated : c'est le meme piege que + * APickupItem::ItemData. Un client qui rejoint une partie en cours construit + * l'acteur AVANT que la propriete arrive -- il verrait l'arbre intact et + * pourrait le frapper, alors que le serveur le sait deja abattu. + */ + UPROPERTY(ReplicatedUsing = OnRep_Depleted, BlueprintReadOnly, Category = "Harvestable") + bool bDepleted = false; + + UFUNCTION() + void OnRep_Depleted(); + +private: + /** + * Points restants. SERVEUR UNIQUEMENT -- voir le commentaire de classe. + * Initialisee au BeginPlay depuis la donnee, et remise a plein a la repousse. + */ + float CurrentHealth = 0.f; + + /** + * Le mesh d'origine, retenu au BeginPlay pour le reposer a la repousse. + * + * Retenu et surtout pas relu depuis le CDO de la classe : une instance posee + * dans le niveau peut avoir son propre mesh -- c'est meme le cas normal, un + * seul BP_Harvestable servant les quinze variantes d'arbre du pack. Le CDO + * rendrait alors le mesh du Blueprint, et l'arbre repousserait en sapin + * generique. Le symptome n'apparait qu'apres RegrowDelay secondes. + */ + UPROPERTY(Transient) + TObjectPtr IntactMesh; + + /** + * Verse le contenu d'une table dans l'inventaire du recolteur. + * @return le nombre de lignes qui ont reellement donne quelque chose. + */ + int32 GrantDrops(const TArray& Table, AFpsPlayer* Harvester); + + /** Passe la ressource a l'etat epuise et arme la repousse. Serveur. */ + void Deplete(); + + /** Remet la ressource d'aplomb. Appelee par le timer, donc serveur. */ + void Regrow(); + + /** + * Pose le mesh correspondant a bDepleted. Tourne sur TOUTES les machines. + * + * Partagee par BeginPlay et OnRep : deux copies finiraient par diverger, et le + * symptome serait une souche correcte chez l'hote et un arbre intact chez le + * client qui vient de le voir tomber. + */ + void ApplyDepletionVisuals(); + + /** + * Retour audiovisuel du coup, joue chez tout le monde. + * + * Unreliable, et c'est un choix : un paquet perdu coute un bruit de hache, pas + * un etat de jeu. Le fiabiliser ferait payer un accuse de reception a chacun + * des trois autres joueurs pour chaque coup porte dans la partie. + */ + UFUNCTION(NetMulticast, Unreliable) + void Multicast_PlayHarvestFeedback(EHarvestFeedback Feedback, FVector_NetQuantize ImpactPoint, + FVector_NetQuantizeNormal HitDirection); + + /** Joue son, particules et balancement. Locale a chaque machine. */ + void PlayHarvestFeedback(EHarvestFeedback Feedback, const FVector& ImpactPoint, const FVector& HitDirection); + + /** Lance le balancement. Axe deduit de la direction du coup. */ + void StartShake(const FVector& HitDirection); + + /** + * Temps ecoule dans le balancement en cours, en secondes. + * Negatif = aucun balancement, donc le Tick est coupe. + */ + float ShakeElapsed = -1.f; + + /** + * Axe autour duquel le mesh oscille, en espace LOCAL. + * + * En local et pas en monde : le mesh tourne dans le repere de son parent, et + * un arbre pose avec un yaw quelconque ferait pencher le tronc de travers si + * l'axe restait exprime en monde. + */ + FVector ShakeAxis = FVector::RightVector; + + /** + * Rotation d'origine du mesh, pour y revenir exactement. + * + * Retenue et jamais recalculee : accumuler des AddRelativeRotation laisse + * derriver l'arbre a chaque coup, et au bout de vingt coups il pousse + * franchement de travers. Le symptome ne sort qu'apres une longue session. + */ + FRotator BaseMeshRotation = FRotator::ZeroRotator; + + /** Repousse, cote serveur uniquement. */ + FTimerHandle RegrowTimer; +}; diff --git a/Source/EmberWild/Public/HarvestingComponent.h b/Source/EmberWild/Public/HarvestingComponent.h new file mode 100644 index 0000000..532a6c5 --- /dev/null +++ b/Source/EmberWild/Public/HarvestingComponent.h @@ -0,0 +1,126 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "Engine/EngineTypes.h" +#include "HarvestableResource.h" +#include "HarvestingComponent.generated.h" + +class UItemDataAsset; + +/** + * Le COUP : ce qui part quand la hache touche. + * + * A poser sur le pawn du joueur, a cote d'UInteractionComponent -- dont il est le + * pendant exact, et la comparaison est la meilleure facon de comprendre les deux : + * + * UInteractionComponent trace en continu, affiche un prompt, agit sur E. + * UHarvestingComponent ne trace QU'AU moment du coup, n'affiche rien. + * + * On ne « vise » pas un arbre, on le frappe : il n'y a donc ni cible memorisee, + * ni surbrillance, ni texte a l'ecran. Le trace part une fois par coup, depuis + * l'AnimNotify pose dans le montage de l'outil -- jamais a l'appui du clic, sans + * quoi la hache abattrait l'arbre avant de l'avoir touche. + * + * ------------------------------------------------------------------ + * Reseau : le client vise, le serveur tranche + * ------------------------------------------------------------------ + * + * C'est le schema d'UInteractionComponent, et pour la meme raison : le trace doit + * partir du point de vue REEL du joueur au moment du geste. Le refaire cote + * serveur le referait depuis une position qui a un demi-aller-retour de retard, + * et un joueur en mouvement raterait des arbres qu'il vise pourtant en plein. + * + * La cible part donc dans la RPC (AFpsPlayer::Server_UseItem) et le serveur + * revalide la distance avec une tolerance. Ce n'est PAS de l'anti-triche -- coop + * entre amis -- mais un garde-fou contre un client desynchronise qui abattrait + * un arbre a l'autre bout de la carte. + * + * Aucune RPC ne vit sur ce composant : elles sont toutes sur le pawn, qui portait + * deja Server_UseItem. Un composant qui en porterait aurait besoin de + * SetIsReplicatedByDefault(true), sans quoi le moteur les jetterait en silence. + */ +UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent)) +class EMBERWILD_API UHarvestingComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UHarvestingComponent(); + + /** + * Trace le coup depuis le point de vue du joueur. A appeler EN LOCAL. + * + * Renvoie n'importe quel acteur touche, pas seulement une ressource : c'est le + * serveur qui decide de ce qu'on peut recolter, et lui renvoyer un nullptr + * parce qu'on a mal identifie la cible chez nous serait lui retirer ce mot. + * + * @param OutImpactPoint point touche, pour les copeaux et la validation. + * @param OutDirection direction du coup, normalisee. + * @return l'acteur touche, ou nullptr si le coup part dans le vide. + */ + UFUNCTION(BlueprintCallable, Category = "Harvesting") + AActor* TraceSwingTarget(FVector& OutImpactPoint, FVector& OutDirection) const; + + /** + * Applique le coup a la cible. A n'appeler que sur le SERVEUR. + * + * @param Tool l'objet en main, nul si les mains sont vides. + * @return ce qui s'est produit ; l'appelant s'en sert pour decider de l'usure. + */ + EHarvestResult ApplySwing(AActor* Target, const UItemDataAsset* Tool, + const FVector& ImpactPoint, const FVector& HitDirection); + + /** Points retires par un coup avec cet outil, mains vides comprises. */ + UFUNCTION(BlueprintPure, Category = "Harvesting") + float GetSwingPower(const UItemDataAsset* Tool) const; + +protected: + /** + * Portee du coup en cm. + * + * Plus courte que celle de l'interaction, et c'est voulu : ramasser une branche + * a bout de bras est un confort, abattre un arbre a la meme distance donnerait + * une hache qui frappe le vide. On doit etre AU pied de l'arbre. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting", meta = (ClampMin = "1", Units = "cm")) + float SwingRange = 200.f; + + /** + * Rayon de la sphere balayee. + * + * Nettement plus large que celui de l'interaction : on ne vise pas un tronc au + * pixel pres, on donne un coup de hache. Un rayon serre rend la recolte penible + * sans rendre le jeu plus exigeant. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting", meta = (ClampMin = "0", Units = "cm")) + float SwingRadius = 25.f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting") + TEnumAsByte SwingChannel = ECC_Visibility; + + /** + * Puissance d'un coup a mains nues. + * + * Sur le composant et pas sur un DataAsset, pour la raison evidente qu'il n'y a + * pas d'objet a qui la demander -- exactement comme UnarmedUseMontage sur + * UHeldItemComponent. Elle n'a d'effet que sur les ressources dont + * RequiredTool vaut None : cueillir des baies, arracher des herbes. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting", meta = (ClampMin = "0")) + float UnarmedHarvestPower = 10.f; + + /** Marge de distance accordee par le serveur. Voir le commentaire de classe. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting|Network", meta = (ClampMin = "1.0")) + float ServerRangeTolerance = 2.f; + + /** Affiche le trace du coup. Le premier reflexe quand la hache ne touche rien. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Harvesting") + bool bDrawDebug = false; + +private: + /** Point de vue reel du joueur, camera comprise. */ + bool GetViewPoint(FVector& OutLocation, FRotator& OutRotation) const; +}; diff --git a/Source/EmberWild/Public/ItemDataAsset.h b/Source/EmberWild/Public/ItemDataAsset.h index 6c6e2b5..6839caa 100644 --- a/Source/EmberWild/Public/ItemDataAsset.h +++ b/Source/EmberWild/Public/ItemDataAsset.h @@ -240,20 +240,35 @@ public: // ------------------------------------------------------------------ // Outil // - // Volontairement reduit au TYPE pour l'instant : la puissance de recolte et - // le cout en durabilite d'un coup n'ont pas encore de lecteur, et inventer - // leur forme avant d'ecrire les ressources recoltables serait deviner. + // L'usure passe par MaxUses / RemainingUses, comme une canette : un outil neuf + // part a MaxUses charges, l'inventaire les suit deja au sol comme dans un + // coffre, et WBP_ItemDetails affiche deja la barre correspondante. Un coup qui + // PORTE consomme une charge -- un coup dans le vide ou contre la mauvaise + // matiere n'use rien, voir UHarvestingComponent. // - // L'usure, elle, est deja tranchee : ce sera MaxUses / RemainingUses, comme - // une canette. Un outil neuf part donc a MaxUses charges, l'inventaire les - // suit deja au sol comme dans un coffre, et WBP_ItemDetails affiche deja la - // barre correspondante -- rien a ajouter le jour venu. + // Corollaire a connaitre : un outil laisse a MaxUses = 1 casserait au premier + // coup. AFpsPlayer::PerformUseItem s'y refuse et le dit dans le log, plutot que + // de faire disparaitre la hache que le joueur vient de fabriquer. // ------------------------------------------------------------------ /** Ce que cet objet sait recolter. None = ce n'est pas un outil. */ UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Tool") EToolType ToolType = EToolType::None; + /** + * Points retires a une ressource par coup. + * + * A comparer au Health de UHarvestableDataAsset : 25 contre un arbre a 100 + * donne quatre coups. C'est ce champ, et lui seul, qui fait la difference entre + * la hache de pierre et celle en fer -- la ressource, elle, ne change pas. + * + * Sans effet sur une ressource que ToolType n'autorise pas : une pioche a 100 + * de puissance ne coupe toujours pas un arbre. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Item|Tool", + meta = (EditCondition = "ToolType != EToolType::None", ClampMin = "0")) + float HarvestPower = 25.f; + UFUNCTION(BlueprintPure, Category = "Item|Tool") bool IsTool() const { return ToolType != EToolType::None; }