diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 6c2cbf4..6599292 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -118,6 +118,8 @@ qu'une compilation propre l'est vraiment. | Inventaire | `InventoryComponent`, `InventoryWidget`, `InventorySlotWidget`, `HotbarWidget`, `InventoryDragDropOperation` | | Survie | `SurvivalStatsComponent`, `SurvivalStatsWidget` | | Menu principal | `MainMenuGameMode`, `MainMenuPlayerController`, `MainMenuWidget`, `MenuCameraSpot` | +| Fabrication | `CraftingRecipeDataAsset`, `CraftingRecipeBook`, `CraftingComponent`, `CraftingWidget`, `CraftingRecipeSlotWidget`, `CraftingIngredientRowWidget` | +| Écran à onglets (inventaire / craft) | `InventoryScreenWidget` | | Menu de pause | `PauseMenuWidget` + la partie pause de `FpsPlayerController` | | Réglages | `SurvivalUserSettings`, `SettingsMenuWidget`, `SettingsTabWidget`, `SettingsGameplayWidget`, `SettingRowWidget` | | Fondus, boîtes de dialogue | `ScreenFadeComponent`, `ConfirmDialogWidget` | @@ -154,9 +156,55 @@ qu'une compilation propre l'est vraiment. `SetDialogZOrder()` : **le contrôleur reste seul maître de son empilement**, un widget réutilisable n'a pas à connaître la pile de son hôte. Une ligne = un index dans une liste de paliers, y compris les nombres : d'où un seul `WBP_SettingRow` pour tout le menu. +- **Craft instantané**, jamais de barre de progression : c'est le choix de Valheim, Raft et + Aloft. La fabrication qui prend du temps sera celle des postes qui travaillent **sans le + joueur** (fourneau, séchoir) — un autre système, avec sa file d'attente, ses acteurs posés + dans le monde et leur mesh qui change d'état. Ce ne sont donc **pas** des `ECraftingStation`. +- **Une seule fenêtre de craft**, dont le contenu dépend d'où l'on se trouve. La recette déclare + son `RequiredStation` ; `UCraftingComponent` tient la liste des postes **accessibles à + l'instant t** (`{Hands}` seul, `{Hands, Workbench}` devant un établi). Par-dessus vient un + filtre d'affichage **purement visuel** : les deux notions restent séparées, sinon un bouton + d'interface finirait par rendre un craft impossible. +- **`Craft()` consomme avant d'ajouter.** Refuser parce que l'inventaire est plein serait faux : + consommer trois branches libère justement la case du résultat. Ce qui ne rentre malgré tout + pas part par `OnCraftOverflow` et le **pawn** le pose au sol — le composant ne sait pas + engendrer un acteur, et un établi déposerait son surplus sur sa propre table. +- Le résultat passe par `AddItem()`, donc **exactement comme un ramassage** : piles entamées + d'abord, puis barre rapide, puis grille. +- **La barre rapide se masque sur l'onglet Fabrication.** `UInventoryScreenWidget` se contente + de diffuser `OnTabChanged` ; c'est le contrôleur qui décide, via une unique + `UpdateHotbarVisibility()`. Un seul point de décision, sinon fermer l'écran depuis l'onglet + craft laisse la barre cachée pour le reste de la partie. + +### Pièges UMG rencontrés sur l'écran de craft + +- Un **Canvas Panel a une taille désirée de zéro** : imbriqué dans un conteneur en `Auto`, il + disparaît. C'est ce qui arrive à `WBP_Inventory` devenu page d'un WidgetSwitcher. +- Dans un Vertical/Horizontal Box, un enfant en **`Size = Fill` ne compte pas** dans la taille + désirée du parent. Tous les enfants en Fill → le parent mesure zéro, et la Border qui + l'entoure se réduit à son padding. +- Un **Size Box impose une taille désirée, pas une taille allouée** : un parent qui l'aligne en + `Fill` l'étire quand même. Il faut `Auto` + `Center`. +- Une **Border ne peint rien avec `Draw As = Image` sans texture**. Pour un aplat de couleur, + c'est **`Rounded Box`** (contour creux : Tint en alpha 0 + Outline Width). +- **« Wrap With… » sur l'unique enfant d'une Border le fait sortir de la Border.** Vérifier + l'indentation juste après, et re-glisser dans la hiérarchie. +- Pour un cadre de taille fixe, le Size Box va **autour** de la Border. Dedans, c'est le contenu + qui commande. Le plus externe gagne toujours. +- `Scale Box` + `Stretch = Scale To Fit` est le seul « keep aspect ratio » d'UMG ; un Size Box + avec `Min/Max Aspect Ratio` à 1.0 suffit pour des icônes carrées. +- Le texte placeholder d'un Text Block (« Text Block ») fausse la mise en page dans la Designer. + Y mettre une valeur représentative — le code l'écrase de toute façon à l'exécution. ## Conventions +- **Le code est en anglais, les commentaires en français.** Tout ce qui est du code s'écrit + en anglais sans exception : noms de classes, de membres et de fonctions, valeurs d'enum, + chaînes `Category = "..."`, `UMETA(DisplayName = ...)`, textes source des `FText` et + `LOCTEXT`. Un `ECraftingStation::Hands`, pas `AlaMain`. Le français est réservé aux + commentaires — un lecteur extérieur doit pouvoir lire le code sans traduire. + *(Les fichiers antérieurs à cette règle ont encore des `Category` et des `FText` en + français ; les mettre à jour au fur et à mesure qu'on les touche.)* - Commentaires **en français**. Ils expliquent le *pourquoi* et les pièges, jamais le *quoi*. - Nommage éditeur : `BP_` acteurs, `WBP_` widgets, `DA_` data assets, `IA_` input actions, `IMC_` mapping contexts, `GM_` game mode. @@ -167,9 +215,16 @@ qu'une compilation propre l'est vraiment. ## État actuel et suite -Boucle complète : récolter → ranger → consommer → survivre → mourir → réapparaître. -Autour : menu principal, menu de pause, et un écran de réglages dont **seul l'onglet -« Jeu & Contrôles » est rempli**. +Boucle complète : récolter → **fabriquer** → ranger → consommer → survivre → mourir → +réapparaître. Autour : menu principal, menu de pause, et un écran de réglages dont **seul +l'onglet « Jeu & Contrôles » est rempli**. + +Le craft est en place de bout en bout : `DA_RecipeBook`, onglets Inventaire / Fabrication, +grille de recettes filtrée par poste accessible, panneau de détail avec compteurs colorés. +Restent à faire quand le besoin viendra : l'**acteur établi** qui appellera +`AddAvailableStation(Workbench)`, les **boutons de filtre** par poste (`SetStationFilter` existe +déjà et `ECraftingStation::Count` sert de sentinelle « tout »), et les vraies recettes — celles +en place sont deux `DA_Craft_Test_*` bâties sur `DA_Branch` et `DA_Rocher`. Suite immédiate sur les réglages, dans l'ordre : onglet **Affichage** (le moteur fait tout, prévoir la confirmation à rebours de 10 s sur la résolution), onglet **Graphismes** (les dix @@ -181,5 +236,5 @@ Prochaine étape gameplay recommandée : **spawner de ressources avec repousse** (`AResourceSpawner`, liste pondérée de DataAssets, rayon, densité, délai). Sans lui la carte se vide en dix minutes. -Ensuite, par ordre de valeur : craft, objet visible en main, coffres, sauvegarde, +Ensuite, par ordre de valeur : objet visible en main, établi, coffres, sauvegarde, cycle jour/nuit. diff --git a/Content/Game/Data/Crafting/DA_Craft_Test_Branche.uasset b/Content/Game/Data/Crafting/DA_Craft_Test_Branche.uasset new file mode 100644 index 0000000..1238d45 --- /dev/null +++ b/Content/Game/Data/Crafting/DA_Craft_Test_Branche.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c0618f5a33ad05efac6eb02816046922b073eb8000fa7f608e9a2ef5ce54078 +size 2117 diff --git a/Content/Game/Data/Crafting/DA_RecipeBook.uasset b/Content/Game/Data/Crafting/DA_RecipeBook.uasset new file mode 100644 index 0000000..f9de246 --- /dev/null +++ b/Content/Game/Data/Crafting/DA_RecipeBook.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c8eadcfdd73f6507cb2c697e2e716bc216fbf9798c015457602b409da60225ba +size 1612 diff --git a/Content/Game/Data/Item/DA_Branch.uasset b/Content/Game/Data/Item/DA_Branch.uasset index c270211..2228ed4 100644 --- a/Content/Game/Data/Item/DA_Branch.uasset +++ b/Content/Game/Data/Item/DA_Branch.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e673c27c786c46251dded21242f15875cc0233a0b0801bd57a794e49b10f82f6 -size 2343 +oid sha256:cf1180e8f2ef03f520963334638c26b86f851b42a235408a8f4210be2f2ca144 +size 2416 diff --git a/Content/Game/Data/Item/DA_Rocher.uasset b/Content/Game/Data/Item/DA_Rocher.uasset index 13318e0..bdc8ba4 100644 --- a/Content/Game/Data/Item/DA_Rocher.uasset +++ b/Content/Game/Data/Item/DA_Rocher.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34683b112026f8695a6f9b0da2504b11d2fe280130fe92ca55a33d5880f62701 +oid sha256:ba0ffbc3bfc181e98c0fbaadccfcef0ceef4e4ccd3a1cadba4e89dd1be8319eb size 2345 diff --git a/Content/Game/Input/IA_ToggleCrafting.uasset b/Content/Game/Input/IA_ToggleCrafting.uasset new file mode 100644 index 0000000..1ff61f9 --- /dev/null +++ b/Content/Game/Input/IA_ToggleCrafting.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8a7d191e777877d659411ba9f0f219efc30553ff30c7be57b184696be97e7d9 +size 1198 diff --git a/Content/Game/Input/IMC_Default.uasset b/Content/Game/Input/IMC_Default.uasset index 2afc0f7..ec45988 100644 --- a/Content/Game/Input/IMC_Default.uasset +++ b/Content/Game/Input/IMC_Default.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d45a37d28ba1d448435a17382940fff77ff0cdff269313ab1648653bad93e7ab -size 16135 +oid sha256:4d158ac2946f150894b103097fb2fd1339ab523db90977508297111fb9af7f17 +size 16782 diff --git a/Content/Game/Maps/GameScene.umap b/Content/Game/Maps/GameScene.umap index 26c4f18..8068f0b 100644 --- a/Content/Game/Maps/GameScene.umap +++ b/Content/Game/Maps/GameScene.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c113eb28109346b12bbb55bad9d5a9974ccd84fcc6a86e1798c23ff1221c735 -size 15123778 +oid sha256:4e2392240e9c076c2f516161a3c34144324864eac4a4e921d6b7de9ac9cea6ab +size 15123894 diff --git a/Content/Game/Maps/MenuScene.umap b/Content/Game/Maps/MenuScene.umap index e30e3b7..2ba5ab1 100644 --- a/Content/Game/Maps/MenuScene.umap +++ b/Content/Game/Maps/MenuScene.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e04ac2e0903c8e8f937c57e1f86ba9606ed0e1732824b98b8e63c73e5e6fe2ef -size 15120728 +oid sha256:eadc04a1254e35997b200bd2301e599406ba1b085374249979289e88f3456924 +size 15120886 diff --git a/Content/Game/Menu/BP_MainMenuPlayerController.uasset b/Content/Game/Menu/BP_MainMenuPlayerController.uasset index 795756f..0ca890e 100644 --- a/Content/Game/Menu/BP_MainMenuPlayerController.uasset +++ b/Content/Game/Menu/BP_MainMenuPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95a1714281e371e38897d5efcac84c13b4d7d34aad54ac8ae427cf4c269798ab -size 21906 +oid sha256:e0a99359c3674f2561633330e75af75b076fdf4b3235ab245b69e12ab32b681a +size 21924 diff --git a/Content/Game/Player/BP_FpsPlayer.uasset b/Content/Game/Player/BP_FpsPlayer.uasset index c623993..6bc6a9f 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:b9aa15c231ba13c9201656ea9ddde189abbaba2e53cdba6ac16e7ebba73fd2b0 -size 27829 +oid sha256:55afc9d979b9d6c147744e4d721e808f0811b120bfb4a95040da9c7a5cb203c5 +size 28321 diff --git a/Content/Game/Player/BP_FpsPlayerController.uasset b/Content/Game/Player/BP_FpsPlayerController.uasset index 5b8b4c1..04db7bf 100644 --- a/Content/Game/Player/BP_FpsPlayerController.uasset +++ b/Content/Game/Player/BP_FpsPlayerController.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86ceba18e93b5b0aa266a1715ef146f5c7ad1102c2c7e2289bb2f6ab55c06ab4 -size 23996 +oid sha256:57b5612e1316512eaaa423c5c39935f66af69359008cb2f82047dd83fa1a1c03 +size 24284 diff --git a/Content/Game/UI/Confirm/WBP_ConfirmDialog.uasset b/Content/Game/UI/Confirm/WBP_ConfirmDialog.uasset new file mode 100644 index 0000000..f19df71 --- /dev/null +++ b/Content/Game/UI/Confirm/WBP_ConfirmDialog.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8366f73aeffd3eff3d2814899ee7811fee8ff9417c183c5b513ddb6f5a6b7413 +size 129570 diff --git a/Content/Game/UI/Craft/WBP_Crafting.uasset b/Content/Game/UI/Craft/WBP_Crafting.uasset new file mode 100644 index 0000000..b201653 --- /dev/null +++ b/Content/Game/UI/Craft/WBP_Crafting.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e3ab3174817e9a537cb3433b0ec30c7c8c1c2b9e667a35cc519bcbf14bb35154 +size 42217 diff --git a/Content/Game/UI/Craft/WBP_IngredientRow.uasset b/Content/Game/UI/Craft/WBP_IngredientRow.uasset new file mode 100644 index 0000000..0caa985 --- /dev/null +++ b/Content/Game/UI/Craft/WBP_IngredientRow.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:932521e2ae8dc1db22dcea714aa43501b60c105455bd4fef21fa76512870314c +size 36428 diff --git a/Content/Game/UI/Craft/WBP_InventoryScreen.uasset b/Content/Game/UI/Craft/WBP_InventoryScreen.uasset new file mode 100644 index 0000000..c9cb675 --- /dev/null +++ b/Content/Game/UI/Craft/WBP_InventoryScreen.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db88f76eea0d0f13387b4b5eb6de667c14e77447b592e7ca81ef401caaaced16 +size 38068 diff --git a/Content/Game/UI/Craft/WBP_RecipeSlot.uasset b/Content/Game/UI/Craft/WBP_RecipeSlot.uasset new file mode 100644 index 0000000..b0b2f50 --- /dev/null +++ b/Content/Game/UI/Craft/WBP_RecipeSlot.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cde5790ef25321a31450722eed31a3d5009c7348c888edaf7016f513f2f373b1 +size 34714 diff --git a/Content/Game/UI/CrossHair/WBP_Crosshair.uasset b/Content/Game/UI/CrossHair/WBP_Crosshair.uasset new file mode 100644 index 0000000..47c3cfd --- /dev/null +++ b/Content/Game/UI/CrossHair/WBP_Crosshair.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa1fe7857438e19d462947c78b933389fa0499e6044248e75872456b7a2bb1a6 +size 26593 diff --git a/Content/Game/UI/Fade/WBP_Fade.uasset b/Content/Game/UI/Fade/WBP_Fade.uasset new file mode 100644 index 0000000..b52db67 --- /dev/null +++ b/Content/Game/UI/Fade/WBP_Fade.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1ee139ddf9a7072f8eb0fc99f96f61e13ec026d507bdd3e170a94adb93d120d +size 26064 diff --git a/Content/Game/UI/Interaction/WBP_InteractionPrompt.uasset b/Content/Game/UI/Interaction/WBP_InteractionPrompt.uasset new file mode 100644 index 0000000..3f92ffd --- /dev/null +++ b/Content/Game/UI/Interaction/WBP_InteractionPrompt.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1af5d456fb4f87ceeb5ba1b51423060c1860ff1e403ae10ce6f78b917d7bf17 +size 26689 diff --git a/Content/Game/UI/Inventory/WBP_Hotbar.uasset b/Content/Game/UI/Inventory/WBP_Hotbar.uasset new file mode 100644 index 0000000..45da12f --- /dev/null +++ b/Content/Game/UI/Inventory/WBP_Hotbar.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d70c567ef94429d9245898f4d469b50119f7334d3bc63aade77b5a603c86f1d +size 26419 diff --git a/Content/Game/UI/Inventory/WBP_Inventory.uasset b/Content/Game/UI/Inventory/WBP_Inventory.uasset new file mode 100644 index 0000000..3d21823 --- /dev/null +++ b/Content/Game/UI/Inventory/WBP_Inventory.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f0cafb0625b277c949c0e0b86f64d03cae55d14aec7ecea17f9edca82ca1b5a +size 28006 diff --git a/Content/Game/UI/Inventory/WBP_InventorySlot.uasset b/Content/Game/UI/Inventory/WBP_InventorySlot.uasset new file mode 100644 index 0000000..1df80a7 --- /dev/null +++ b/Content/Game/UI/Inventory/WBP_InventorySlot.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e044569b655de1cdd4fcb8dd1e8ecfde35be40604f111e5e8a0ce980084d7af0 +size 38335 diff --git a/Content/Game/UI/Menu/WBP_MainMenu.uasset b/Content/Game/UI/Menu/WBP_MainMenu.uasset new file mode 100644 index 0000000..2b67e21 --- /dev/null +++ b/Content/Game/UI/Menu/WBP_MainMenu.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:92cc18212d7697008d6d7b8f52e164c6275c0c4abf0e360e32ba138c863f51e1 +size 211194 diff --git a/Content/Game/UI/Pause/WBP_PauseMenu.uasset b/Content/Game/UI/Pause/WBP_PauseMenu.uasset new file mode 100644 index 0000000..e4cf96e --- /dev/null +++ b/Content/Game/UI/Pause/WBP_PauseMenu.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19566d39e61af42bf5198fc20f0e38c08cbd336da9839447a943f22dbf7d7cdc +size 48971 diff --git a/Content/Game/UI/Setting/Input/WBP_SettingsControls.uasset b/Content/Game/UI/Setting/Input/WBP_SettingsControls.uasset index a696964..e9ee451 100644 --- a/Content/Game/UI/Setting/Input/WBP_SettingsControls.uasset +++ b/Content/Game/UI/Setting/Input/WBP_SettingsControls.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4a205e42bc83abf84756f63b7fa90314fb40f10554c16169ea76c7ad40e5e87 -size 38586 +oid sha256:99d3c615f49298471c27e18f09f322edb503aca44b5d833340b215384c3cee02 +size 40703 diff --git a/Content/Game/UI/Stats/WBP_SurvivalStats.uasset b/Content/Game/UI/Stats/WBP_SurvivalStats.uasset new file mode 100644 index 0000000..8aec7e9 --- /dev/null +++ b/Content/Game/UI/Stats/WBP_SurvivalStats.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27056c247274b1a47cf260aebf13d0f75c4101e4abb84b489f141c86c7b8b5ff +size 31930 diff --git a/Content/Game/UI/WBP_ConfirmDialog.uasset b/Content/Game/UI/WBP_ConfirmDialog.uasset deleted file mode 100644 index e9364a4..0000000 --- a/Content/Game/UI/WBP_ConfirmDialog.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f563fe0bf1097cedd8d4de338b21b7dc4bd47cdaf36b13d9804dd95aa6444c0d -size 132382 diff --git a/Content/Game/UI/WBP_Crosshair.uasset b/Content/Game/UI/WBP_Crosshair.uasset deleted file mode 100644 index 72b53e9..0000000 --- a/Content/Game/UI/WBP_Crosshair.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:503a1d56ce86ceaae64bcf7194e389b9a9ecc59f337283dfcea7e3d8a7dfd8ff -size 26563 diff --git a/Content/Game/UI/WBP_Fade.uasset b/Content/Game/UI/WBP_Fade.uasset deleted file mode 100644 index 88e843b..0000000 --- a/Content/Game/UI/WBP_Fade.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2456541f9589db64156e6d67084a0e232ef1c4708c7051a2f280a0aa60e3b8d1 -size 26049 diff --git a/Content/Game/UI/WBP_Hotbar.uasset b/Content/Game/UI/WBP_Hotbar.uasset deleted file mode 100644 index 6472619..0000000 --- a/Content/Game/UI/WBP_Hotbar.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:14c7fe0cbe9001f862f394e92106ca7d5f41ede55e1a9eda49da9969c2f555c7 -size 26379 diff --git a/Content/Game/UI/WBP_InteractionPrompt.uasset b/Content/Game/UI/WBP_InteractionPrompt.uasset deleted file mode 100644 index dbb3b35..0000000 --- a/Content/Game/UI/WBP_InteractionPrompt.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:74278c4bd760608ac5c11786d87cbb6344590a517121d60c4bf52d549ea17d27 -size 26653 diff --git a/Content/Game/UI/WBP_Inventory.uasset b/Content/Game/UI/WBP_Inventory.uasset deleted file mode 100644 index 82852bf..0000000 --- a/Content/Game/UI/WBP_Inventory.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c82072a4032b2c778287c6b6a6955ab5c8291ecd0eacd39a3dc4b46dbbf65822 -size 30057 diff --git a/Content/Game/UI/WBP_InventorySlot.uasset b/Content/Game/UI/WBP_InventorySlot.uasset deleted file mode 100644 index 72c7050..0000000 --- a/Content/Game/UI/WBP_InventorySlot.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:73dede0bf10fb59834a64def03069e367e054e6568fd5c8a9b09a1d6d2611c31 -size 38305 diff --git a/Content/Game/UI/WBP_MainMenu.uasset b/Content/Game/UI/WBP_MainMenu.uasset deleted file mode 100644 index f72693b..0000000 --- a/Content/Game/UI/WBP_MainMenu.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7d365a133589ccbba1a26c6e3658e11dfc93cddede410773a470dda7be6f6e71 -size 211169 diff --git a/Content/Game/UI/WBP_PauseMenu.uasset b/Content/Game/UI/WBP_PauseMenu.uasset deleted file mode 100644 index 3db5540..0000000 --- a/Content/Game/UI/WBP_PauseMenu.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4325390c6e21709f9b329a06c983c8a5110eadbe8695da448f9bdb88b267b7b -size 48953 diff --git a/Content/Game/UI/WBP_SurvivalStats.uasset b/Content/Game/UI/WBP_SurvivalStats.uasset deleted file mode 100644 index c0085cd..0000000 --- a/Content/Game/UI/WBP_SurvivalStats.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:68eeec280c96d3b83afd5bd50db9cd37e193555467fdc5b40a5857a451280b10 -size 31912 diff --git a/Source/Survival_projet/Private/CraftingComponent.cpp b/Source/Survival_projet/Private/CraftingComponent.cpp new file mode 100644 index 0000000..118074e --- /dev/null +++ b/Source/Survival_projet/Private/CraftingComponent.cpp @@ -0,0 +1,226 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "CraftingComponent.h" + +#include "CraftingRecipeBook.h" +#include "InventoryComponent.h" +#include "ItemDataAsset.h" + +UCraftingComponent::UCraftingComponent() +{ + PrimaryComponentTick.bCanEverTick = false; + + // Necessaire pour que InitializeComponent() soit appele. + bWantsInitializeComponent = true; +} + +void UCraftingComponent::InitializeComponent() +{ + Super::InitializeComponent(); + + // Les mains sont toujours disponibles. Les postes s'ajoutent par-dessus. + AvailableStations.Add(ECraftingStation::Hands); + + if (const AActor* Owner = GetOwner()) + { + Inventory = Owner->FindComponentByClass(); + } + + if (!Inventory) + { + UE_LOG(LogTemp, Warning, TEXT("UCraftingComponent : aucun UInventoryComponent sur %s, aucune fabrication ne sera possible."), + *GetNameSafe(GetOwner())); + } + + if (!RecipeBook) + { + UE_LOG(LogTemp, Warning, TEXT("UCraftingComponent : RecipeBook n'est pas assigne sur %s, la grille de craft restera vide."), + *GetNameSafe(GetOwner())); + } +} + +void UCraftingComponent::AddAvailableStation(ECraftingStation Station) +{ + if (Station == ECraftingStation::Count) + { + return; + } + + bool bAlreadyThere = false; + AvailableStations.Add(Station, &bAlreadyThere); + + // On ne diffuse que sur un vrai changement : deux volumes d'etabli qui se + // chevauchent rafraichiraient l'interface deux fois pour rien. + if (!bAlreadyThere) + { + OnAvailableStationsChanged.Broadcast(); + } +} + +void UCraftingComponent::RemoveAvailableStation(ECraftingStation Station) +{ + // Retirer les mains laisserait le joueur incapable de fabriquer quoi que ce + // soit, sans qu'aucun message ne l'explique. + if (Station == ECraftingStation::Hands) + { + return; + } + + if (AvailableStations.Remove(Station) > 0) + { + OnAvailableStationsChanged.Broadcast(); + } +} + +void UCraftingComponent::GatherRecipes(TArray& OutRecipes, ECraftingStation StationFilter) const +{ + OutRecipes.Reset(); + + if (!RecipeBook) + { + return; + } + + const TArray>& AllRecipes = RecipeBook->GetRecipes(); + OutRecipes.Reserve(AllRecipes.Num()); + + for (UCraftingRecipeDataAsset* Recipe : AllRecipes) + { + // Une entree vide dans le livre est signalee par sa validation d'asset. + // Ici on se contente de la sauter. + if (!Recipe) + { + continue; + } + + if (!AvailableStations.Contains(Recipe->RequiredStation)) + { + continue; + } + + if (StationFilter != ECraftingStation::Count && Recipe->RequiredStation != StationFilter) + { + continue; + } + + OutRecipes.Add(Recipe); + } +} + +bool UCraftingComponent::CanCraft(const UCraftingRecipeDataAsset* Recipe, ECraftFailureReason& OutReason) const +{ + OutReason = ECraftFailureReason::None; + + if (!RecipeBook) + { + OutReason = ECraftFailureReason::NoRecipeBook; + return false; + } + + if (!Recipe || !Recipe->ResultItem) + { + OutReason = ECraftFailureReason::InvalidRecipe; + return false; + } + + if (!Inventory) + { + OutReason = ECraftFailureReason::InvalidRecipe; + return false; + } + + if (!AvailableStations.Contains(Recipe->RequiredStation)) + { + OutReason = ECraftFailureReason::StationUnavailable; + return false; + } + + for (const FCraftIngredient& Ingredient : Recipe->Ingredients) + { + // Un ingredient nul est refuse par la validation de l'asset. S'il passe + // quand meme, on ne fabrique pas : mieux vaut un bouton grise qu'une + // recette qui coute moins cher que ce qu'elle affiche. + if (!Ingredient.Item) + { + OutReason = ECraftFailureReason::InvalidRecipe; + return false; + } + + if (Inventory->GetItemCount(Ingredient.Item) < Ingredient.Quantity) + { + OutReason = ECraftFailureReason::MissingIngredients; + return false; + } + } + + return true; +} + +void UCraftingComponent::GatherIngredientStatus(const UCraftingRecipeDataAsset* Recipe, TArray& OutStatus) const +{ + OutStatus.Reset(); + + if (!Recipe) + { + return; + } + + OutStatus.Reserve(Recipe->Ingredients.Num()); + + for (const FCraftIngredient& Ingredient : Recipe->Ingredients) + { + if (!Ingredient.Item) + { + continue; + } + + FCraftIngredientStatus& Status = OutStatus.AddDefaulted_GetRef(); + Status.Item = Ingredient.Item; + Status.RequiredQuantity = Ingredient.Quantity; + Status.OwnedQuantity = Inventory ? Inventory->GetItemCount(Ingredient.Item) : 0; + } +} + +bool UCraftingComponent::Craft(UCraftingRecipeDataAsset* Recipe) +{ + ECraftFailureReason Reason = ECraftFailureReason::None; + if (!CanCraft(Recipe, Reason)) + { + return false; + } + + // CanCraft a deja verifie que tout est present : aucun RemoveItem ne peut + // echouer a partir d'ici, donc pas de consommation a moitie faite. + // + // Chaque retrait diffuse OnInventoryChanged, donc l'interface se rafraichit + // une fois par ingredient. C'est du gaspillage assume : ca reste quelques + // dizaines de SetSlot sur un clic, et grouper les diffusions demanderait + // une API de lot dans l'inventaire pour un gain invisible. + for (const FCraftIngredient& Ingredient : Recipe->Ingredients) + { + Inventory->RemoveItem(Ingredient.Item, Ingredient.Quantity); + } + + // -1 : l'objet sort neuf de la fabrication, avec toutes ses charges. + const int32 Leftover = Inventory->AddItem(Recipe->ResultItem, Recipe->ResultQuantity, -1); + + if (Leftover > 0) + { + if (OnCraftOverflow.IsBound()) + { + OnCraftOverflow.Broadcast(Recipe->ResultItem, Leftover, -1); + } + else + { + // Personne pour poser l'objet au sol : il est perdu. C'est une erreur + // de branchement, pas une situation de jeu -- d'ou le niveau Error. + UE_LOG(LogTemp, Error, + TEXT("UCraftingComponent : %d x %s n'ont pas pu entrer dans l'inventaire et personne n'ecoute OnCraftOverflow. Objets perdus."), + Leftover, *GetNameSafe(Recipe->ResultItem)); + } + } + + OnCraftSucceeded.Broadcast(Recipe); + return true; +} diff --git a/Source/Survival_projet/Private/CraftingIngredientRowWidget.cpp b/Source/Survival_projet/Private/CraftingIngredientRowWidget.cpp new file mode 100644 index 0000000..f357274 --- /dev/null +++ b/Source/Survival_projet/Private/CraftingIngredientRowWidget.cpp @@ -0,0 +1,41 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "CraftingIngredientRowWidget.h" + +#include "Components/Image.h" +#include "Components/TextBlock.h" +#include "ItemDataAsset.h" + +void UCraftingIngredientRowWidget::SetIngredient(const FCraftIngredientStatus& Status) +{ + if (IngredientIcon) + { + UTexture2D* Icon = Status.Item ? Status.Item->Icon : nullptr; + if (Icon) + { + IngredientIcon->SetBrushFromTexture(Icon); + IngredientIcon->SetVisibility(ESlateVisibility::HitTestInvisible); + } + else + { + // Sans ca, un objet sans icone afficherait le carre blanc du brush + // par defaut au milieu de la liste. + IngredientIcon->SetVisibility(ESlateVisibility::Hidden); + } + } + + if (IngredientNameText) + { + IngredientNameText->SetText(Status.Item ? Status.Item->DisplayName : FText::GetEmpty()); + } + + if (IngredientCountText) + { + IngredientCountText->SetText(FText::Format(CountFormat, + FText::AsNumber(Status.OwnedQuantity), + FText::AsNumber(Status.RequiredQuantity))); + + IngredientCountText->SetColorAndOpacity(FSlateColor(Status.IsSatisfied() ? EnoughColor : MissingColor)); + } +} diff --git a/Source/Survival_projet/Private/CraftingRecipeBook.cpp b/Source/Survival_projet/Private/CraftingRecipeBook.cpp new file mode 100644 index 0000000..f8340a5 --- /dev/null +++ b/Source/Survival_projet/Private/CraftingRecipeBook.cpp @@ -0,0 +1,50 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "CraftingRecipeBook.h" + +#include "CraftingRecipeDataAsset.h" + +#if WITH_EDITOR +#include "Misc/DataValidation.h" +#endif + +#define LOCTEXT_NAMESPACE "CraftingBook" + +#if WITH_EDITOR +EDataValidationResult UCraftingRecipeBook::IsDataValid(FDataValidationContext& Context) const +{ + EDataValidationResult Result = Super::IsDataValid(Context); + + // Une case vide dans le tableau ne casse rien au runtime, on la saute. Mais + // c'est le symptome d'une recette supprimee ou d'un ajout laisse en plan, et + // la grille de craft aurait un trou sans qu'on sache pourquoi. + TSet SeenRecipes; + SeenRecipes.Reserve(Recipes.Num()); + + for (int32 Index = 0; Index < Recipes.Num(); ++Index) + { + const UCraftingRecipeDataAsset* Recipe = Recipes[Index]; + + if (!Recipe) + { + Context.AddWarning(FText::Format( + LOCTEXT("BookNullRecipe", "Book entry {0} is empty."), Index)); + continue; + } + + bool bAlreadySeen = false; + SeenRecipes.Add(Recipe, &bAlreadySeen); + if (bAlreadySeen) + { + Context.AddWarning(FText::Format( + LOCTEXT("BookDuplicateRecipe", "Recipe {0} is listed twice: it will show up twice in the grid."), + FText::FromString(Recipe->GetName()))); + } + } + + return Result; +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Survival_projet/Private/CraftingRecipeDataAsset.cpp b/Source/Survival_projet/Private/CraftingRecipeDataAsset.cpp new file mode 100644 index 0000000..0b26c8a --- /dev/null +++ b/Source/Survival_projet/Private/CraftingRecipeDataAsset.cpp @@ -0,0 +1,83 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "CraftingRecipeDataAsset.h" + +#include "ItemDataAsset.h" + +#if WITH_EDITOR +#include "Misc/DataValidation.h" +#endif + +#define LOCTEXT_NAMESPACE "Crafting" + +FText UCraftingRecipeDataAsset::GetDisplayName() const +{ + if (!DisplayNameOverride.IsEmpty()) + { + return DisplayNameOverride; + } + + return ResultItem ? ResultItem->DisplayName : FText::GetEmpty(); +} + +UTexture2D* UCraftingRecipeDataAsset::GetIcon() const +{ + if (IconOverride) + { + return IconOverride; + } + + return ResultItem ? ResultItem->Icon : nullptr; +} + +#if WITH_EDITOR +EDataValidationResult UCraftingRecipeDataAsset::IsDataValid(FDataValidationContext& Context) const +{ + EDataValidationResult Result = Super::IsDataValid(Context); + + if (!ResultItem) + { + Context.AddError(LOCTEXT("RecipeNoResult", "This recipe has no result item: it would consume the ingredients and give nothing back.")); + Result = EDataValidationResult::Invalid; + } + + if (RecipeId.IsNone()) + { + Context.AddWarning(LOCTEXT("RecipeNoId", "RecipeId is empty. Saved recipe unlocks will not be able to identify this recipe.")); + } + + // Deux lignes portant le meme objet donneraient un cout affiche en double + // dans le panneau de detail, alors que la verification, elle, les cumule. + // L'ecart entre les deux est indetectable a la lecture de l'asset. + TSet SeenItems; + SeenItems.Reserve(Ingredients.Num()); + + for (int32 Index = 0; Index < Ingredients.Num(); ++Index) + { + const FCraftIngredient& Ingredient = Ingredients[Index]; + + if (!Ingredient.Item) + { + Context.AddError(FText::Format( + LOCTEXT("RecipeNullIngredient", "Ingredient {0} has no item assigned."), Index)); + Result = EDataValidationResult::Invalid; + continue; + } + + bool bAlreadySeen = false; + SeenItems.Add(Ingredient.Item, &bAlreadySeen); + if (bAlreadySeen) + { + Context.AddError(FText::Format( + LOCTEXT("RecipeDuplicateIngredient", "Item {0} appears twice in the ingredients. Merge them into a single line."), + FText::FromString(Ingredient.Item->GetName()))); + Result = EDataValidationResult::Invalid; + } + } + + return Result; +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/Source/Survival_projet/Private/CraftingRecipeSlotWidget.cpp b/Source/Survival_projet/Private/CraftingRecipeSlotWidget.cpp new file mode 100644 index 0000000..9270e2d --- /dev/null +++ b/Source/Survival_projet/Private/CraftingRecipeSlotWidget.cpp @@ -0,0 +1,66 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "CraftingRecipeSlotWidget.h" + +#include "Components/Image.h" +#include "Components/TextBlock.h" +#include "CraftingRecipeDataAsset.h" +#include "CraftingWidget.h" + +void UCraftingRecipeSlotWidget::InitSlot(UCraftingRecipeDataAsset* InRecipe, UCraftingWidget* InOwningGrid) +{ + Recipe = InRecipe; + OwningGrid = InOwningGrid; + + if (RecipeIcon) + { + UTexture2D* Icon = Recipe ? Recipe->GetIcon() : nullptr; + if (Icon) + { + RecipeIcon->SetBrushFromTexture(Icon); + RecipeIcon->SetVisibility(ESlateVisibility::HitTestInvisible); + } + else + { + // Pas d'icone : on masque plutot que de laisser le brush par defaut, + // qui afficherait un carre blanc franchement laid dans la grille. + RecipeIcon->SetVisibility(ESlateVisibility::Hidden); + } + } + + if (RecipeNameText) + { + RecipeNameText->SetText(Recipe ? Recipe->GetDisplayName() : FText::GetEmpty()); + } + + SetSelected(false); + SetCraftable(true); +} + +void UCraftingRecipeSlotWidget::SetSelected(bool bSelected) +{ + if (SelectionBorder) + { + SelectionBorder->SetVisibility(bSelected ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden); + } +} + +void UCraftingRecipeSlotWidget::SetCraftable(bool bCraftable) +{ + if (RecipeIcon) + { + RecipeIcon->SetColorAndOpacity(bCraftable ? CraftableTint : UncraftableTint); + } +} + +FReply UCraftingRecipeSlotWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && OwningGrid.IsValid()) + { + OwningGrid->NotifyRecipeClicked(Recipe); + return FReply::Handled(); + } + + return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent); +} diff --git a/Source/Survival_projet/Private/CraftingWidget.cpp b/Source/Survival_projet/Private/CraftingWidget.cpp new file mode 100644 index 0000000..92b244f --- /dev/null +++ b/Source/Survival_projet/Private/CraftingWidget.cpp @@ -0,0 +1,397 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "CraftingWidget.h" + +#include "Components/Button.h" +#include "Components/Image.h" +#include "Components/TextBlock.h" +#include "Components/UniformGridPanel.h" +#include "CraftingComponent.h" +#include "CraftingIngredientRowWidget.h" +#include "CraftingRecipeSlotWidget.h" +#include "FpsPlayer.h" +#include "GameFramework/PlayerController.h" +#include "InventoryComponent.h" +#include "ItemDataAsset.h" + +void UCraftingWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (CraftButton) + { + CraftButton->OnClicked.AddDynamic(this, &UCraftingWidget::HandleCraftClicked); + } + + BindToOwningPawn(); +} + +void UCraftingWidget::NativeDestruct() +{ + UnbindDelegates(); + + Super::NativeDestruct(); +} + +UCraftingComponent* UCraftingWidget::ResolveCrafting() const +{ + const APlayerController* OwningController = GetOwningPlayer(); + AFpsPlayer* PlayerPawn = OwningController ? Cast(OwningController->GetPawn()) : nullptr; + return PlayerPawn ? PlayerPawn->GetCraftingComponent() : nullptr; +} + +void UCraftingWidget::UnbindDelegates() +{ + if (BoundInventory.IsValid()) + { + BoundInventory->OnInventoryChanged.RemoveDynamic(this, &UCraftingWidget::HandleInventoryChanged); + } + BoundInventory.Reset(); + + if (BoundCrafting.IsValid()) + { + BoundCrafting->OnAvailableStationsChanged.RemoveDynamic(this, &UCraftingWidget::HandleStationsChanged); + } + BoundCrafting.Reset(); +} + +void UCraftingWidget::BindToOwningPawn() +{ + UCraftingComponent* NewCrafting = ResolveCrafting(); + if (BoundCrafting.Get() == NewCrafting) + { + return; + } + + UnbindDelegates(); + + BoundCrafting = NewCrafting; + + if (BoundCrafting.IsValid()) + { + BoundCrafting->OnAvailableStationsChanged.AddDynamic(this, &UCraftingWidget::HandleStationsChanged); + + // L'inventaire est la vraie source des couleurs et de l'etat du bouton : + // c'est lui qui change quand le joueur ramasse ou consomme. + if (const AActor* Owner = BoundCrafting->GetOwner()) + { + BoundInventory = Owner->FindComponentByClass(); + if (BoundInventory.IsValid()) + { + BoundInventory->OnInventoryChanged.AddDynamic(this, &UCraftingWidget::HandleInventoryChanged); + } + } + } + else + { + UE_LOG(LogTemp, Warning, TEXT("UCraftingWidget : aucun UCraftingComponent trouve sur le pawn possede.")); + } + + // Les cases gardent l'ancienne recette et l'ancien parent en memoire : on + // force leur recreation en vidant la grille. + for (UCraftingRecipeSlotWidget* SlotWidget : RecipeSlots) + { + if (SlotWidget) + { + SlotWidget->RemoveFromParent(); + } + } + RecipeSlots.Reset(); + + // Les lignes de cout aussi : elles affichent l'inventaire du pawn precedent. + for (UCraftingIngredientRowWidget* Row : IngredientRows) + { + if (Row) + { + Row->RemoveFromParent(); + } + } + IngredientRows.Reset(); + + SelectedRecipe = nullptr; + + Refresh(); +} + +void UCraftingWidget::HandleInventoryChanged() +{ + // Seules les couleurs et l'etat du bouton bougent : la liste des recettes, + // elle, ne depend que des postes accessibles. + RefreshStates(); +} + +void UCraftingWidget::HandleStationsChanged() +{ + Refresh(); +} + +void UCraftingWidget::SetStationFilter(ECraftingStation InFilter) +{ + if (StationFilter == InFilter) + { + return; + } + + StationFilter = InFilter; + Refresh(); +} + +void UCraftingWidget::Refresh() +{ + if (!RecipeGrid) + { + UE_LOG(LogTemp, Warning, TEXT("UCraftingWidget::Refresh : le widget nomme 'RecipeGrid' manque dans le Blueprint.")); + return; + } + + TArray Recipes; + if (BoundCrafting.IsValid()) + { + BoundCrafting->GatherRecipes(Recipes, StationFilter); + } + + RebuildGrid(Recipes); + + if (EmptyGridMessage) + { + EmptyGridMessage->SetVisibility(Recipes.Num() > 0 ? ESlateVisibility::Collapsed : ESlateVisibility::Visible); + } + + // La recette choisie a pu disparaitre de la liste : on s'est eloigne de + // l'etabli, ou le filtre a change. + if (SelectedRecipe && !Recipes.Contains(SelectedRecipe)) + { + SelectedRecipe = nullptr; + } + + RefreshStates(); +} + +void UCraftingWidget::RebuildGrid(const TArray& NewRecipes) +{ + // La liste ne change qu'en s'approchant d'un poste ou en changeant de + // filtre. Recreer les cases a chaque ramassage serait du gaspillage pur, + // d'ou cette comparaison prealable. + bool bSameList = RecipeSlots.Num() == NewRecipes.Num(); + if (bSameList) + { + for (int32 Index = 0; Index < RecipeSlots.Num(); ++Index) + { + if (!RecipeSlots[Index] || RecipeSlots[Index]->GetRecipe() != NewRecipes[Index]) + { + bSameList = false; + break; + } + } + } + + if (bSameList) + { + return; + } + + for (UCraftingRecipeSlotWidget* SlotWidget : RecipeSlots) + { + if (SlotWidget) + { + SlotWidget->RemoveFromParent(); + } + } + RecipeSlots.Reset(); + + if (NewRecipes.Num() == 0) + { + return; + } + + if (!RecipeSlotClass) + { + UE_LOG(LogTemp, Warning, TEXT("UCraftingWidget : RecipeSlotClass n'est pas assignee, la grille restera vide.")); + return; + } + + const int32 Columns = FMath::Max(1, ColumnCount); + RecipeSlots.Reserve(NewRecipes.Num()); + + for (int32 Index = 0; Index < NewRecipes.Num(); ++Index) + { + UCraftingRecipeSlotWidget* NewSlot = CreateWidget(this, RecipeSlotClass); + if (!NewSlot) + { + // Arrive typiquement quand le Widget Blueprint de case ne compile + // plus, apres un changement de BindWidget cote C++ par exemple. + UE_LOG(LogTemp, Error, TEXT("UCraftingWidget : impossible de creer une case a partir de %s. Recompile ce Widget Blueprint."), + *GetNameSafe(RecipeSlotClass)); + break; + } + + NewSlot->InitSlot(NewRecipes[Index], this); + RecipeGrid->AddChildToUniformGrid(NewSlot, Index / Columns, Index % Columns); + RecipeSlots.Add(NewSlot); + } +} + +void UCraftingWidget::RefreshStates() +{ + UCraftingComponent* Crafting = BoundCrafting.Get(); + + for (UCraftingRecipeSlotWidget* SlotWidget : RecipeSlots) + { + if (!SlotWidget) + { + continue; + } + + ECraftFailureReason Reason = ECraftFailureReason::None; + const bool bCraftable = Crafting && Crafting->CanCraft(SlotWidget->GetRecipe(), Reason); + + SlotWidget->SetCraftable(bCraftable); + SlotWidget->SetSelected(SlotWidget->GetRecipe() == SelectedRecipe); + } + + RefreshDetails(); +} + +void UCraftingWidget::RefreshDetails() +{ + UCraftingComponent* Crafting = BoundCrafting.Get(); + + if (DetailsPanel) + { + DetailsPanel->SetVisibility(SelectedRecipe ? ESlateVisibility::Visible : ESlateVisibility::Hidden); + } + + if (!SelectedRecipe) + { + if (CraftButton) + { + CraftButton->SetIsEnabled(false); + } + + // Le panneau est masque, mais on vide quand meme : le contenu de + // l'ancienne recette reapparaitrait un instant a la selection suivante. + RefreshIngredients(); + return; + } + + if (ResultIcon) + { + if (UTexture2D* Icon = SelectedRecipe->GetIcon()) + { + ResultIcon->SetBrushFromTexture(Icon); + ResultIcon->SetVisibility(ESlateVisibility::HitTestInvisible); + } + else + { + ResultIcon->SetVisibility(ESlateVisibility::Hidden); + } + } + + if (ResultNameText) + { + ResultNameText->SetText(SelectedRecipe->GetDisplayName()); + } + + if (ResultQuantityText) + { + const bool bMultiple = SelectedRecipe->ResultQuantity > 1; + ResultQuantityText->SetVisibility(bMultiple ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Collapsed); + if (bMultiple) + { + ResultQuantityText->SetText(FText::AsNumber(SelectedRecipe->ResultQuantity)); + } + } + + if (ResultDescriptionText) + { + ResultDescriptionText->SetText(SelectedRecipe->ResultItem ? SelectedRecipe->ResultItem->Description : FText::GetEmpty()); + } + + RefreshIngredients(); + + if (CraftButton) + { + ECraftFailureReason Reason = ECraftFailureReason::None; + CraftButton->SetIsEnabled(Crafting && Crafting->CanCraft(SelectedRecipe, Reason)); + } +} + +void UCraftingWidget::RefreshIngredients() +{ + if (!IngredientList) + { + return; + } + + TArray Statuses; + if (SelectedRecipe && BoundCrafting.IsValid()) + { + BoundCrafting->GatherIngredientStatus(SelectedRecipe, Statuses); + } + + // Trop de lignes : on retire le surplus par la fin. + while (IngredientRows.Num() > Statuses.Num()) + { + const int32 Last = IngredientRows.Num() - 1; + if (IngredientRows[Last]) + { + IngredientRows[Last]->RemoveFromParent(); + } + IngredientRows.RemoveAt(Last); + } + + if (IngredientRows.Num() < Statuses.Num() && !IngredientRowClass) + { + UE_LOG(LogTemp, Warning, TEXT("UCraftingWidget : IngredientRowClass n'est pas assignee, la liste de cout restera vide.")); + } + + // Pas assez : on en cree. + while (IngredientRows.Num() < Statuses.Num() && IngredientRowClass) + { + UCraftingIngredientRowWidget* NewRow = CreateWidget(this, IngredientRowClass); + if (!NewRow) + { + UE_LOG(LogTemp, Error, TEXT("UCraftingWidget : impossible de creer une ligne a partir de %s. Recompile ce Widget Blueprint."), + *GetNameSafe(IngredientRowClass)); + break; + } + + IngredientList->AddChild(NewRow); + IngredientRows.Add(NewRow); + } + + for (int32 Index = 0; Index < IngredientRows.Num(); ++Index) + { + if (IngredientRows[Index] && Statuses.IsValidIndex(Index)) + { + IngredientRows[Index]->SetIngredient(Statuses[Index]); + } + } +} + +void UCraftingWidget::NotifyRecipeClicked(UCraftingRecipeDataAsset* InRecipe) +{ + if (SelectedRecipe == InRecipe) + { + return; + } + + SelectedRecipe = InRecipe; + RefreshStates(); +} + +void UCraftingWidget::HandleCraftClicked() +{ + if (!SelectedRecipe || !BoundCrafting.IsValid()) + { + return; + } + + // Pas de re-verification ici : Craft() refait CanCraft de toute facon, et + // dupliquer la condition finirait par la faire diverger. + BoundCrafting->Craft(SelectedRecipe); + + // L'inventaire diffuse son changement, donc RefreshStates est deja passe. + // Rien a rappeler. +} diff --git a/Source/Survival_projet/Private/FpsPlayer.cpp b/Source/Survival_projet/Private/FpsPlayer.cpp index c500efb..ad4294e 100644 --- a/Source/Survival_projet/Private/FpsPlayer.cpp +++ b/Source/Survival_projet/Private/FpsPlayer.cpp @@ -4,6 +4,7 @@ #include "FpsPlayer.h" #include "Camera/CameraComponent.h" +#include "Camera/CameraTypes.h" #include "Components/CapsuleComponent.h" #include "Components/SkeletalMeshComponent.h" #include "EnhancedInputComponent.h" @@ -12,6 +13,7 @@ #include "GameFramework/CharacterMovementComponent.h" #include "GameFramework/PlayerController.h" #include "InputActionValue.h" +#include "CraftingComponent.h" #include "InteractionComponent.h" #include "InventoryComponent.h" #include "ItemDataAsset.h" @@ -19,6 +21,32 @@ #include "SurvivalStatsComponent.h" #include "SurvivalUserSettings.h" +float FCameraSpring::Update(float Step, float Frequency, float Damping, float RampTime, float MaxOffset) +{ + // L'impulsion n'est pas versee d'un coup : la vitesse passerait de 0 a + // plusieurs centaines d'unites par seconde en une frame, et cette + // discontinuite se lit comme un a-coup. On l'etale sur RampTime. + if (!FMath::IsNearlyZero(PendingImpulse)) + { + const float ReleaseRatio = (RampTime > KINDA_SMALL_NUMBER) + ? FMath::Clamp(Step / RampTime, 0.f, 1.f) + : 1.f; + const float Released = PendingImpulse * ReleaseRatio; + Velocity += Released; + PendingImpulse -= Released; + } + + // Ressort amorti classique : acceleration = -k*x - c*v. + // On exprime k et c a partir d'une frequence et d'un taux d'amortissement, + // beaucoup plus parlants a regler qu'une raideur brute. + const float Omega = 2.f * PI * Frequency; + const float Acceleration = -(Omega * Omega * Offset) - (2.f * Damping * Omega * Velocity); + Velocity += Acceleration * Step; + Offset = FMath::Clamp(Offset + Velocity * Step, -MaxOffset, MaxOffset); + + return Offset; +} + // Sets default values AFpsPlayer::AFpsPlayer() { @@ -47,6 +75,7 @@ AFpsPlayer::AFpsPlayer() InteractionComponent = CreateDefaultSubobject(TEXT("InteractionComponent")); InventoryComponent = CreateDefaultSubobject(TEXT("InventoryComponent")); + CraftingComponent = CreateDefaultSubobject(TEXT("CraftingComponent")); SurvivalStats = CreateDefaultSubobject(TEXT("SurvivalStats")); // En vue FPS on ne veut pas voir son propre corps (mesh 3e personne), @@ -73,6 +102,10 @@ void AFpsPlayer::BeginPlay() SurvivalStats->OnDied.AddDynamic(this, &AFpsPlayer::HandleDeath); + // Sans cet abonnement, un objet fabrique alors que l'inventaire est plein + // serait purement et simplement perdu. + CraftingComponent->OnCraftOverflow.AddDynamic(this, &AFpsPlayer::HandleCraftOverflow); + // Les reglages d'abord : le pawn est recree a chaque respawn et repart des // valeurs de son CDO, il doit donc les relire lui-meme. L'abonnement couvre // ensuite les changements faits en cours de partie depuis le menu de pause. @@ -184,28 +217,14 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime) } // ================================================================== - // 2. Ressort d'impact (saut, atterrissage, et tout AddCameraPunch) + // 2. Ressorts : impact (saut, atterrissage) et bascule de tete // ================================================================== - // L'impulsion n'est pas versee d'un coup : la vitesse de la camera passerait - // de 0 a plusieurs centaines de cm/s en une frame, et cette discontinuite se - // lit comme un a-coup. On l'etale sur PunchImpulseRampTime. - if (!FMath::IsNearlyZero(PendingPunch)) - { - const float ReleaseRatio = (PunchImpulseRampTime > KINDA_SMALL_NUMBER) - ? FMath::Clamp(Step / PunchImpulseRampTime, 0.f, 1.f) - : 1.f; - const float Released = PendingPunch * ReleaseRatio; - PunchVelocity += Released; - PendingPunch -= Released; - } + PunchSpring.Update(Step, PunchFrequency, PunchDamping, PunchImpulseRampTime, PunchMaxOffset); - // Ressort amorti classique : acceleration = -k*x - c*v. - // On exprime k et c a partir d'une frequence et d'un taux d'amortissement, - // beaucoup plus parlants a regler qu'une raideur brute. - const float Omega = 2.f * PI * PunchFrequency; - const float Acceleration = -(Omega * Omega * PunchOffset) - (2.f * PunchDamping * Omega * PunchVelocity); - PunchVelocity += Acceleration * Step; - PunchOffset = FMath::Clamp(PunchOffset + PunchVelocity * Step, -PunchMaxOffset, PunchMaxOffset); + // Le garde-fou est en unites normalisees, ou 1 = le pic d'une bascule + // complete. A 1.5 on laisse passer un leger cumul quand deux impulsions se + // suivent de tres pres, sans jamais laisser la tete partir. + CrouchTiltSpring.Update(Step, CrouchTiltFrequency, CrouchTiltDamping, CrouchTiltRampTime, 1.5f); // ================================================================== // 3. FOV de sprint @@ -216,8 +235,15 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime) const float TargetFOV = BaseFOV + ((bIsSprinting && bMovingFastEnough) ? SprintFOVOffset : 0.f); FirstPersonCamera->SetFieldOfView(FMath::FInterpTo(FirstPersonCamera->FieldOfView, TargetFOV, Step, FOVInterpSpeed)); + // L'application est laissee a ApplyCameraTransform : CalcCamera la rappelle + // juste avant que la vue soit lue, avec l'etat le plus frais de la capsule. + ApplyCameraTransform(); +} + +void AFpsPlayer::ApplyCameraTransform() +{ // ================================================================== - // 4. Application : position + // Position // ================================================================== // SmoothStep donne une courbe en S : demarrage doux, acceleration, arrivee douce. const float Smoothed = FMath::SmoothStep(0.f, 1.f, CrouchAlpha); @@ -227,22 +253,25 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime) // d'un coup pour garder les pieds au sol. La camera, attachee a la capsule, // suivait ce saut instantane -- c'est ce qui rendait le crouch "snap". // En raisonnant en hauteur d'oeil AU-DESSUS DES PIEDS et en soustrayant la - // demi-hauteur courante de la capsule, ce saut est annule exactement. + // demi-hauteur courante de la capsule, ce saut est annule exactement -- + // a condition de lire les deux au meme instant, d'ou l'appel depuis CalcCamera. const float CapsuleHalfHeight = GetCapsuleComponent()->GetScaledCapsuleHalfHeight(); - FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightAboveFeet - CapsuleHalfHeight + PunchOffset)); + FirstPersonCamera->SetRelativeLocation(FVector(0.f, 0.f, EyeHeightAboveFeet - CapsuleHalfHeight + PunchSpring.Offset)); // ================================================================== - // 5. Application : rotation + // Rotation // ================================================================== - // sin(alpha * PI) vaut 0 aux deux extremites et 1 au milieu : le tilt monte - // puis redescend tout seul pendant la transition, et est nul au repos. - // CrouchTiltDirection inverse le sens selon qu'on descend ou qu'on remonte. - const float CrouchTiltShape = FMath::Sin(CrouchAlpha * PI) * CrouchTiltDirection; + // La bascule ne depend plus de l'avancement de la transition : c'est l'etat + // d'un ressort, donc elle reste continue meme si le joueur change d'avis en + // plein mouvement. L'ancienne forme sin(alpha*PI) multipliee par un signe + // sautait de +CrouchTiltPitch a -CrouchTiltPitch en une frame des qu'on + // s'accroupissait et se relevait coup sur coup. + const float CrouchTilt = CrouchTiltSpring.Offset; - FRotator ViewOffset(-CrouchTiltPitch * CrouchTiltShape, 0.f, CrouchTiltRoll * CrouchTiltShape); + FRotator ViewOffset(-CrouchTiltPitch * CrouchTilt, 0.f, CrouchTiltRoll * CrouchTilt); // Le regard suit le deplacement vertical de la camera : elle descend, on // regarde vers le bas. C'est ce qui vend l'impact a l'atterrissage. - ViewOffset.Pitch += PunchOffset * PunchPitchPerCm; + ViewOffset.Pitch += PunchSpring.Offset * PunchPitchPerCm; if (const AController* OwningController = GetController()) { @@ -250,6 +279,23 @@ void AFpsPlayer::UpdateCameraEffects(float DeltaTime) } } +void AFpsPlayer::CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult) +{ + ApplyCameraTransform(); + + Super::CalcCamera(DeltaTime, OutResult); +} + +void AFpsPlayer::AddCrouchTilt(float Direction) +{ + // Impulsion normalisee pour qu'un ressort critique culmine exactement a 1 : + // x(t) = v0 * t * e^(-w*t) atteint son maximum v0/(e*w) en t = 1/w, donc + // v0 = e*w. CrouchTiltPitch et CrouchTiltRoll restent ainsi des degres au + // pic, et non un reglage a l'aveugle. + const float Omega = 2.f * PI * CrouchTiltFrequency; + CrouchTiltSpring.AddImpulse(Direction * UE_EULERS_NUMBER * Omega); +} + void AFpsPlayer::AddCameraPunch(float Strength) { // Le reglage s'applique ICI et pas sur chaque source d'impact : tout ce qui @@ -257,7 +303,7 @@ void AFpsPlayer::AddCameraPunch(float Strength) // reculs d'arme a venir. A 0, la camera ne bouge plus du tout. // Mis en attente plutot qu'applique directement : c'est UpdateCameraEffects // qui l'injecte progressivement dans le ressort. - PendingPunch += Strength * CameraShakeMultiplier; + PunchSpring.AddImpulse(Strength * CameraShakeMultiplier); } void AFpsPlayer::Landed(const FHitResult& Hit) @@ -529,6 +575,37 @@ bool AFpsPlayer::DropSlot(int32 SlotIndex, int32 Quantity) return true; } +void AFpsPlayer::HandleCraftOverflow(UItemDataAsset* Item, int32 Quantity, int32 RemainingUses) +{ + if (!Item || Quantity <= 0) + { + return; + } + + if (!DroppedPickupClass) + { + UE_LOG(LogTemp, Error, TEXT("AFpsPlayer : DroppedPickupClass n'est pas assignee, %d x %s sortis de la fabrication sont perdus."), + Quantity, *GetNameSafe(Item)); + return; + } + + FInventorySlot Overflow; + Overflow.Item = Item; + Overflow.Quantity = Quantity; + Overflow.RemainingUses = RemainingUses; + + // 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. + 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."), + Quantity, *GetNameSafe(Item)); + } +} + void AFpsPlayer::DropAllItems() { if (!DroppedPickupClass) @@ -597,8 +674,12 @@ void AFpsPlayer::ClearTransientInputStates() void AFpsPlayer::StartCrouch() { - // En mode bascule, la meme touche releve le personnage. - if (bCrouchToggleMode && bIsCrouched) + // En mode bascule, la meme touche releve le personnage. On teste l'INTENTION + // (bWantsToCrouch) et non l'etat applique (bIsCrouched) : le CharacterMovement + // ne redimensionne la capsule qu'a son propre tick, donc deux appuis dans la + // meme frame verraient tous les deux bIsCrouched a false et le personnage + // resterait accroupi. Symptome typique quand on martele la touche. + if (bCrouchToggleMode && GetCharacterMovement()->bWantsToCrouch) { StopCrouch(); return; @@ -635,7 +716,7 @@ void AFpsPlayer::OnStartCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdj Super::OnStartCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust); CrouchTargetAlpha = 1.f; - CrouchTiltDirection = 1.f; + AddCrouchTilt(1.f); } void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjust) @@ -643,5 +724,5 @@ void AFpsPlayer::OnEndCrouch(float HalfHeightAdjust, float ScaledHalfHeightAdjus Super::OnEndCrouch(HalfHeightAdjust, ScaledHalfHeightAdjust); CrouchTargetAlpha = 0.f; - CrouchTiltDirection = -1.f; + AddCrouchTilt(-1.f); } diff --git a/Source/Survival_projet/Private/FpsPlayerController.cpp b/Source/Survival_projet/Private/FpsPlayerController.cpp index d9cfac7..25f5099 100644 --- a/Source/Survival_projet/Private/FpsPlayerController.cpp +++ b/Source/Survival_projet/Private/FpsPlayerController.cpp @@ -14,7 +14,7 @@ #include "InputCoreTypes.h" #include "InventoryComponent.h" #include "InteractionPromptWidget.h" -#include "InventoryWidget.h" +#include "InventoryScreenWidget.h" #include "Kismet/GameplayStatics.h" #include "PauseMenuWidget.h" #include "ScreenFadeComponent.h" @@ -104,7 +104,7 @@ void AFpsPlayerController::OnPossess(APawn* InPawn) if (Crosshair) { /* pas d'abonnement */ } if (InteractionPrompt) { InteractionPrompt->BindToOwningPawn(); } if (Hotbar) { Hotbar->BindToOwningPawn(); } - if (InventoryWidget) { InventoryWidget->BindToOwningPawn(); } + if (InventoryScreen) { InventoryScreen->BindToOwningPawn(); } if (SurvivalStatsWidget) { SurvivalStatsWidget->BindToOwningPawn(); } if (AFpsPlayer* PlayerPawn = Cast(InPawn)) @@ -173,6 +173,11 @@ void AFpsPlayerController::SetupInputComponent() Input->BindAction(ToggleInventoryAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleInventory); } + if (ToggleCraftingAction) + { + Input->BindAction(ToggleCraftingAction, ETriggerEvent::Started, this, &AFpsPlayerController::ToggleCrafting); + } + if (DropItemAction) { Input->BindAction(DropItemAction, ETriggerEvent::Started, this, &AFpsPlayerController::DropHoveredItem); @@ -236,14 +241,39 @@ void AFpsPlayerController::HandleHotbarScroll(const FInputActionValue& Value) } } -void AFpsPlayerController::DropHoveredItem() +void AFpsPlayerController::HandleInventoryTabChanged(bool bInventoryTabActive) { - if (!bInventoryOpen || !InventoryWidget) + bCraftingTabActive = !bInventoryTabActive; + UpdateHotbarVisibility(); +} + +void AFpsPlayerController::UpdateHotbarVisibility() +{ + if (!Hotbar) { return; } - const int32 SlotIndex = InventoryWidget->GetHoveredSlotIndex(); + // La barre rapide reste affichee sur l'onglet Inventaire : elle en fait + // partie, on y glisse des objets. Elle n'a en revanche aucun sens sous la + // page de craft, ou elle ne ferait qu'encombrer. + // Surtout pas "bHidden" : AActor en a deja un, le masquer donne une erreur + // de compilation (C4458) plutot qu'un simple avertissement. + const bool bHideHotbar = bInventoryOpen && bCraftingTabActive; + + Hotbar->SetVisibility(bHideHotbar ? ESlateVisibility::Collapsed : HotbarDefaultVisibility); +} + +void AFpsPlayerController::DropHoveredItem() +{ + if (!bInventoryOpen || !InventoryScreen) + { + return; + } + + // Renvoie INDEX_NONE quand l'onglet Fabrication est affiche : la touche ne + // doit pas agir sur une case cachee derriere la grille de craft. + const int32 SlotIndex = InventoryScreen->GetHoveredSlotIndex(); if (SlotIndex == INDEX_NONE) { return; @@ -316,27 +346,35 @@ void AFpsPlayerController::CreateHudWidgets() Hotbar = CreateWidget(this, HotbarClass); if (Hotbar) { + // Relevee avant tout masquage : c'est la valeur a laquelle on + // reviendra en quittant l'onglet Fabrication. + HotbarDefaultVisibility = Hotbar->GetVisibility(); Hotbar->AddToViewport(ZOrderHotbar); } } - if (!InventoryWidget) + if (!InventoryScreen) { - if (InventoryClass) + if (InventoryScreenClass) { - InventoryWidget = CreateWidget(this, InventoryClass); - if (InventoryWidget) + InventoryScreen = CreateWidget(this, InventoryScreenClass); + if (InventoryScreen) { - InventoryWidget->AddToViewport(ZOrderInventory); - // Cree des le depart pour que son abonnement au delegue soit - // actif, mais cache : ainsi il reste a jour meme ferme, et - // l'ouverture est instantanee. - InventoryWidget->SetVisibility(ESlateVisibility::Collapsed); + // Abonnement AVANT l'ajout au viewport : c'est lui qui declenche + // NativeConstruct, donc la premiere diffusion d'onglet. S'abonner + // apres, on la manquerait. + InventoryScreen->OnTabChanged.AddDynamic(this, &AFpsPlayerController::HandleInventoryTabChanged); + + InventoryScreen->AddToViewport(ZOrderInventory); + // Cree des le depart pour que les abonnements de ses deux pages + // soient actifs, mais cache : ainsi l'ecran reste a jour meme + // ferme, et l'ouverture est instantanee. + InventoryScreen->SetVisibility(ESlateVisibility::Collapsed); } } else { - UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InventoryClass n'est pas assignee, l'inventaire ne s'ouvrira pas.")); + UE_LOG(LogTemp, Warning, TEXT("AFpsPlayerController : InventoryScreenClass n'est pas assignee, l'inventaire ne s'ouvrira pas.")); } } @@ -362,14 +400,73 @@ void AFpsPlayerController::CreateHudWidgets() void AFpsPlayerController::ToggleInventory() { - if (!InventoryWidget) + // Tab veut dire "ouvre mon sac". Rouvrir sur l'onglet Fabrication parce + // qu'on l'avait laisse la serait une surprise a chaque fois. + SetInventoryScreenOpen(!bInventoryOpen, /*bShowCraftingTab=*/false); +} + +void AFpsPlayerController::ToggleCrafting() +{ + // Deja sur l'onglet Fabrication : la meme touche referme, comme Tab le fait + // pour l'inventaire. Depuis l'onglet Inventaire, elle bascule sans fermer -- + // c'est le geste attendu quand on cherche quoi construire avec ce qu'on vient + // de ramasser. + if (bInventoryOpen && bCraftingTabActive) + { + SetInventoryScreenOpen(false, /*bShowCraftingTab=*/false); + return; + } + + SetInventoryScreenOpen(true, /*bShowCraftingTab=*/true); +} + +void AFpsPlayerController::SetInventoryScreenOpen(bool bOpen, bool bShowCraftingTab) +{ + if (!InventoryScreen) { return; } - bInventoryOpen = !bInventoryOpen; + // Ecran deja ouvert : on ne fait que changer d'onglet. Repasser par le mode + // d'input serait au mieux inutile, au pire nuisible -- SetIgnoreLookInput + // gere un compteur qu'un appel en trop desequilibrerait durablement. + if (bOpen && bInventoryOpen) + { + if (bShowCraftingTab) + { + InventoryScreen->ShowCraftingTab(); + } + else + { + InventoryScreen->ShowInventoryTab(); + } + return; + } - InventoryWidget->SetVisibility(bInventoryOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); + if (bOpen == bInventoryOpen) + { + return; + } + + bInventoryOpen = bOpen; + + if (bInventoryOpen) + { + if (bShowCraftingTab) + { + InventoryScreen->ShowCraftingTab(); + } + else + { + InventoryScreen->ShowInventoryTab(); + } + } + + InventoryScreen->SetVisibility(bInventoryOpen ? ESlateVisibility::Visible : ESlateVisibility::Collapsed); + + // Fermer l'ecran doit toujours rendre la barre, meme si on l'avait laissee + // sur l'onglet Fabrication. + UpdateHotbarVisibility(); AFpsPlayer* PlayerPawn = Cast(GetPawn()); @@ -693,10 +790,10 @@ void AFpsPlayerController::EndPlay(const EEndPlayReason::Type EndPlayReason) PauseMenu = nullptr; } - if (InventoryWidget) + if (InventoryScreen) { - InventoryWidget->RemoveFromParent(); - InventoryWidget = nullptr; + InventoryScreen->RemoveFromParent(); + InventoryScreen = nullptr; } if (InteractionPrompt) diff --git a/Source/Survival_projet/Private/InventoryScreenWidget.cpp b/Source/Survival_projet/Private/InventoryScreenWidget.cpp new file mode 100644 index 0000000..253c879 --- /dev/null +++ b/Source/Survival_projet/Private/InventoryScreenWidget.cpp @@ -0,0 +1,126 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "InventoryScreenWidget.h" + +#include "Components/Button.h" +#include "Components/WidgetSwitcher.h" +#include "CraftingWidget.h" +#include "FpsPlayer.h" +#include "GameFramework/PlayerController.h" +#include "InventoryDragDropOperation.h" +#include "InventoryWidget.h" + +void UInventoryScreenWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (InventoryTabButton) + { + InventoryTabButton->OnClicked.AddDynamic(this, &UInventoryScreenWidget::HandleInventoryTabClicked); + } + + if (CraftingTabButton) + { + CraftingTabButton->OnClicked.AddDynamic(this, &UInventoryScreenWidget::HandleCraftingTabClicked); + } + + ShowInventoryTab(); +} + +void UInventoryScreenWidget::BindToOwningPawn() +{ + if (InventoryPage) + { + InventoryPage->BindToOwningPawn(); + } + + if (CraftingPage) + { + CraftingPage->BindToOwningPawn(); + } +} + +bool UInventoryScreenWidget::IsInventoryTabActive() const +{ + return TabSwitcher && InventoryPage && TabSwitcher->GetActiveWidget() == InventoryPage; +} + +int32 UInventoryScreenWidget::GetHoveredSlotIndex() const +{ + if (!InventoryPage || !IsInventoryTabActive()) + { + return INDEX_NONE; + } + + return InventoryPage->GetHoveredSlotIndex(); +} + +void UInventoryScreenWidget::ShowInventoryTab() +{ + ShowPage(InventoryPage); +} + +void UInventoryScreenWidget::ShowCraftingTab() +{ + ShowPage(CraftingPage); +} + +void UInventoryScreenWidget::HandleInventoryTabClicked() +{ + ShowPage(InventoryPage); +} + +void UInventoryScreenWidget::HandleCraftingTabClicked() +{ + ShowPage(CraftingPage); +} + +void UInventoryScreenWidget::ShowPage(UWidget* Page) +{ + if (!TabSwitcher || !Page) + { + return; + } + + TabSwitcher->SetActiveWidget(Page); + + // Le bouton de l'onglet courant est desactive : il sert d'indicateur de + // position autant que de garde-fou contre un clic sans effet. + const bool bInventoryActive = (Page == InventoryPage); + + if (InventoryTabButton) + { + InventoryTabButton->SetIsEnabled(!bInventoryActive); + } + + if (CraftingTabButton) + { + CraftingTabButton->SetIsEnabled(bInventoryActive); + } + + OnTabChanged.Broadcast(bInventoryActive); +} + +bool UInventoryScreenWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) +{ + Super::NativeOnDrop(InGeometry, InDragDropEvent, InOperation); + + // On n'arrive ici que si ni une case ni la page d'inventaire n'ont traite le + // depot : les enfants sont consultes en premier et arretent la propagation. + const UInventoryDragDropOperation* Payload = Cast(InOperation); + if (!Payload || Payload->SourceSlotIndex == INDEX_NONE) + { + return true; + } + + if (const APlayerController* OwningController = GetOwningPlayer()) + { + if (AFpsPlayer* PlayerPawn = Cast(OwningController->GetPawn())) + { + PlayerPawn->DropSlot(Payload->SourceSlotIndex, Payload->Quantity); + } + } + + return true; +} diff --git a/Source/Survival_projet/Private/SettingsControlsWidget.cpp b/Source/Survival_projet/Private/SettingsControlsWidget.cpp index 4c3a898..2daecdf 100644 --- a/Source/Survival_projet/Private/SettingsControlsWidget.cpp +++ b/Source/Survival_projet/Private/SettingsControlsWidget.cpp @@ -45,6 +45,7 @@ void USettingsControlsWidget::NativeConstruct() SetupRow(InteractRow, TEXT("Interact"), LOCTEXT("Interagir", "Interagir")); SetupRow(UseItemRow, TEXT("UseItem"), LOCTEXT("UtiliserObjet", "Utiliser l'objet")); SetupRow(ToggleInventoryRow, TEXT("ToggleInventory"), LOCTEXT("Inventaire", "Inventaire")); + SetupRow(ToggleCraftingRow, TEXT("ToggleCrafting"), LOCTEXT("Fabrication", "Fabrication")); SetupRow(DropItemRow, TEXT("DropItem"), LOCTEXT("Jeter", "Jeter")); RefreshFromSettings(); @@ -84,11 +85,11 @@ TArray USettingsControlsWidget::GetAllRows() const { // L'ordre est celui de l'affichage : il sert aussi a la reinitialisation. TArray Rows; - Rows.Reserve(11); + Rows.Reserve(12); for (USettingKeyRowWidget* Row : { MoveForwardRow.Get(), MoveBackwardRow.Get(), MoveLeftRow.Get(), MoveRightRow.Get(), JumpRow.Get(), SprintRow.Get(), CrouchRow.Get(), InteractRow.Get(), - UseItemRow.Get(), ToggleInventoryRow.Get(), DropItemRow.Get() }) + UseItemRow.Get(), ToggleInventoryRow.Get(), ToggleCraftingRow.Get(), DropItemRow.Get() }) { if (Row) { diff --git a/Source/Survival_projet/Public/CraftingComponent.h b/Source/Survival_projet/Public/CraftingComponent.h new file mode 100644 index 0000000..e0abb84 --- /dev/null +++ b/Source/Survival_projet/Public/CraftingComponent.h @@ -0,0 +1,168 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Components/ActorComponent.h" +#include "CraftingRecipeDataAsset.h" +#include "CraftingComponent.generated.h" + +class UCraftingRecipeBook; +class UInventoryComponent; +class UItemDataAsset; + +/** + * Pourquoi une fabrication a echoue. Sert a l'interface, pour dire au joueur + * ce qui manque au lieu de laisser un bouton grise sans explication. + */ +UENUM(BlueprintType) +enum class ECraftFailureReason : uint8 +{ + None, + InvalidRecipe, + NoRecipeBook, + StationUnavailable, + MissingIngredients +}; + +/** + * Etat d'un ingredient pour une recette donnee : ce qu'il faut, ce qu'on a. + * + * C'est le composant qui fait le rapprochement, pas le widget. Une ligne + * d'ingredient n'a plus qu'a afficher deux nombres et choisir une couleur -- + * elle n'interroge jamais l'inventaire elle-meme. + */ +USTRUCT(BlueprintType) +struct FCraftIngredientStatus +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "Crafting") + TObjectPtr Item = nullptr; + + UPROPERTY(BlueprintReadOnly, Category = "Crafting") + int32 RequiredQuantity = 0; + + UPROPERTY(BlueprintReadOnly, Category = "Crafting") + int32 OwnedQuantity = 0; + + bool IsSatisfied() const { return OwnedQuantity >= RequiredQuantity; } +}; + +/** Une fabrication vient d'aboutir. Pour le son, l'animation, une notification. */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCraftSucceeded, UCraftingRecipeDataAsset*, Recipe); + +/** Les postes accessibles ont change : l'interface doit revoir sa liste. */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAvailableStationsChanged); + +/** + * Le resultat n'est pas entre dans l'inventaire. A quelqu'un de le poser + * dans le monde -- voir le commentaire de Craft(). + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnCraftOverflow, UItemDataAsset*, Item, int32, Quantity, int32, RemainingUses); + +/** + * Fabrication d'objets a partir d'un livre de recettes et de l'inventaire du + * meme acteur. + * + * Sur le pawn : fabriquer est une capacite du corps, au meme titre que porter + * un sac. Le composant ne connait ni l'interface ni la classe du pawn -- un + * futur etabli pourra porter le meme composant avec son propre livre. + */ +UCLASS(ClassGroup = (Survival), meta = (BlueprintSpawnableComponent)) +class SURVIVAL_PROJET_API UCraftingComponent : public UActorComponent +{ + GENERATED_BODY() + +public: + UCraftingComponent(); + + virtual void InitializeComponent() override; + + /** + * Recettes a afficher, dans l'ordre du livre. + * + * Ne renvoie que celles dont le poste est ACCESSIBLE : les mains vides, on + * ne voit pas les recettes d'etabli. C'est le modele choisi -- une seule + * fenetre de craft dont le contenu depend d'ou l'on se trouve. + * + * @param StationFilter filtre d'affichage supplementaire, purement visuel. + * Passe Count pour tout voir. Un poste demande ici mais non + * accessible ne fait rien apparaitre : le filtre restreint, il + * n'autorise jamais. + * + * Pas de valeur par defaut sur StationFilter : UHT refuse une entree + * UMETA(Hidden) comme defaut, et dé-cacher Count le rendrait selectionnable + * dans le champ Required Station d'une recette. L'appelant l'ecrit. + */ + UFUNCTION(BlueprintCallable, Category = "Crafting") + void GatherRecipes(TArray& OutRecipes, ECraftingStation StationFilter) const; + + /** Peut-on fabriquer cette recette maintenant, et sinon pourquoi. */ + UFUNCTION(BlueprintCallable, Category = "Crafting") + bool CanCraft(const UCraftingRecipeDataAsset* Recipe, ECraftFailureReason& OutReason) const; + + /** Le detail requis / possede de chaque ingredient, dans l'ordre de la recette. */ + UFUNCTION(BlueprintCallable, Category = "Crafting") + void GatherIngredientStatus(const UCraftingRecipeDataAsset* Recipe, TArray& OutStatus) const; + + /** + * Consomme les ingredients et ajoute le resultat a l'inventaire. + * + * L'ordre compte : on consomme AVANT d'ajouter. Refuser une fabrication + * parce que l'inventaire est plein serait faux -- consommer trois branches + * libere justement la case ou poser le resultat. Et si malgre tout il ne + * rentre pas, l'objet part dans OnCraftOverflow pour etre pose au sol : + * aucune ressource ne disparait jamais, ni a l'entree ni a la sortie. + * + * @return true si la fabrication a eu lieu. + */ + UFUNCTION(BlueprintCallable, Category = "Crafting") + bool Craft(UCraftingRecipeDataAsset* Recipe); + + // ------------------------------------------------------------------ + // Postes accessibles + // + // Etat courant, pas de la configuration : {Hands} en permanence, plus + // Workbench tant qu'on est devant un etabli. C'est une CONDITION de + // fabrication, a ne pas confondre avec le filtre d'affichage de + // GatherRecipes -- melanger les deux rendrait un jour un craft impossible + // a cause d'un bouton d'interface. + // ------------------------------------------------------------------ + + UFUNCTION(BlueprintPure, Category = "Crafting|Stations") + bool HasStation(ECraftingStation Station) const { return AvailableStations.Contains(Station); } + + /** A appeler quand le joueur entre dans le rayon d'un poste. */ + UFUNCTION(BlueprintCallable, Category = "Crafting|Stations") + void AddAvailableStation(ECraftingStation Station); + + /** A appeler quand il s'en eloigne. Hands ne peut pas etre retire. */ + UFUNCTION(BlueprintCallable, Category = "Crafting|Stations") + void RemoveAvailableStation(ECraftingStation Station); + + UPROPERTY(BlueprintAssignable, Category = "Crafting") + FOnCraftSucceeded OnCraftSucceeded; + + UPROPERTY(BlueprintAssignable, Category = "Crafting") + FOnAvailableStationsChanged OnAvailableStationsChanged; + + UPROPERTY(BlueprintAssignable, Category = "Crafting") + FOnCraftOverflow OnCraftOverflow; + +protected: + /** Ton DA_RecipeBook. Sans lui le composant ne connait aucune recette. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting") + TObjectPtr RecipeBook; + + /** Resolu une fois a l'initialisation : chercher le composant a chaque clic serait absurde. */ + UPROPERTY(Transient, BlueprintReadOnly, Category = "Crafting") + TObjectPtr Inventory; + +private: + /** + * Pas une UPROPERTY : un TSet de valeurs d'enum ne contient aucune + * reference a collecter, et cet etat n'a rien a regler dans l'editeur. + */ + TSet AvailableStations; +}; diff --git a/Source/Survival_projet/Public/CraftingIngredientRowWidget.h b/Source/Survival_projet/Public/CraftingIngredientRowWidget.h new file mode 100644 index 0000000..286eee7 --- /dev/null +++ b/Source/Survival_projet/Public/CraftingIngredientRowWidget.h @@ -0,0 +1,59 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "CraftingComponent.h" +#include "CraftingIngredientRowWidget.generated.h" + +class UImage; +class UTextBlock; + +/** + * Une ligne de cout dans le panneau de detail : l'icone d'un ingredient, son + * nom, et "2/3" colore selon qu'on l'a ou non. + * + * Elle ne consulte jamais l'inventaire : UCraftingComponent lui remet un + * FCraftIngredientStatus deja calcule. Une ligne qui irait compter elle-meme + * referait le travail autant de fois qu'il y a d'ingredients, et pourrait + * afficher autre chose que ce sur quoi le bouton Fabriquer s'appuie. + */ +UCLASS(Abstract) +class SURVIVAL_PROJET_API UCraftingIngredientRowWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** Affiche un ingredient et applique la couleur qui va avec. */ + UFUNCTION(BlueprintCallable, Category = "Crafting") + void SetIngredient(const FCraftIngredientStatus& Status); + +protected: + /** Facultatif : l'icone de la ressource demandee. */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr IngredientIcon; + + /** Facultatif : le nom de la ressource. */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr IngredientNameText; + + /** Obligatoire : c'est lui qui porte le "2/3". A nommer "IngredientCountText". */ + UPROPERTY(meta = (BindWidget)) + TObjectPtr IngredientCountText; + + /** Couleur du compteur quand la ressource est en quantite suffisante. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting") + FLinearColor EnoughColor = FLinearColor(0.35f, 0.85f, 0.35f, 1.f); + + /** Couleur du compteur quand il en manque. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting") + FLinearColor MissingColor = FLinearColor(0.9f, 0.3f, 0.3f, 1.f); + + /** + * Mise en forme du compteur. {0} = possede, {1} = requis. + * Exposee pour pouvoir passer a "2 / 3" ou "2 sur 3" sans recompiler. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting") + FText CountFormat = NSLOCTEXT("Crafting", "IngredientCountFormat", "{0}/{1}"); +}; diff --git a/Source/Survival_projet/Public/CraftingRecipeBook.h b/Source/Survival_projet/Public/CraftingRecipeBook.h new file mode 100644 index 0000000..9dc0db3 --- /dev/null +++ b/Source/Survival_projet/Public/CraftingRecipeBook.h @@ -0,0 +1,40 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "CraftingRecipeBook.generated.h" + +class UCraftingRecipeDataAsset; + +/** + * La liste ordonnee des recettes du jeu. + * + * Un catalogue plutot qu'un scan de l'AssetManager : le contenu est + * deterministe, rien ne se charge par surprise au runtime, et surtout l'ORDRE + * d'affichage de la grille devient une donnee de design qu'on regle a la + * souris. Un scan d'assets rendrait cet ordre dependant du nom des fichiers. + * + * Un catalogue plutot qu'une liste posee sur le composant, aussi : un etabli + * devra pointer son propre livre, et le pawn n'a pas a etre l'endroit ou vit + * la table de craft du jeu. + */ +UCLASS(BlueprintType) +class SURVIVAL_PROJET_API UCraftingRecipeBook : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + /** L'ordre de ce tableau est l'ordre d'affichage dans la grille de craft. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting") + TArray> Recipes; + + // Pas de UFUNCTION ici : UHT refuse un TObjectPtr en valeur de retour. Le + // Blueprint lit de toute facon Recipes directement, c'est une UPROPERTY. + const TArray>& GetRecipes() const { return Recipes; } + +#if WITH_EDITOR + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Source/Survival_projet/Public/CraftingRecipeDataAsset.h b/Source/Survival_projet/Public/CraftingRecipeDataAsset.h new file mode 100644 index 0000000..f7b4cc4 --- /dev/null +++ b/Source/Survival_projet/Public/CraftingRecipeDataAsset.h @@ -0,0 +1,124 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataAsset.h" +#include "CraftingRecipeDataAsset.generated.h" + +class UItemDataAsset; +class UTexture2D; + +/** + * Poste de travail necessaire pour realiser une recette. + * + * Ce n'est PAS un filtre d'affichage : c'est une condition de realisation. + * Le UCraftingComponent tient la liste des stations dont le joueur dispose a + * l'instant t -- {Hands} tout seul, {Hands, Workbench} devant un etabli -- et + * une recette n'est realisable que si la sienne s'y trouve. Le filtre de + * l'interface est une notion separee, purement visuelle : melanger les deux + * finirait par rendre un craft impossible parce qu'un bouton d'affichage est + * mal positionne, et ce genre de bug ne se comprend jamais sur le moment. + * + * Fourneau et feu de cuisson n'ont volontairement PAS leur entree ici : ils ne + * passent par aucun menu. Ce sont des acteurs poses dans le monde, facon Raft, + * ou l'on depose une ressource et ou l'on voit le mesh changer d'etat. Leur + * donnee et leur file d'attente leur appartiendront, une recette de menu n'a + * rien a y faire. + */ +UENUM(BlueprintType) +enum class ECraftingStation : uint8 +{ + Hands, + Workbench, + + // Sentinelle, jamais assignee a une recette. Elle sert a parcourir l'enum + // pour construire les boutons de filtre sans avoir a les enumerer a la main. + Count UMETA(Hidden) +}; + +/** Une ligne de cout : tant d'exemplaires de tel objet. */ +USTRUCT(BlueprintType) +struct FCraftIngredient +{ + GENERATED_BODY() + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe") + TObjectPtr Item = nullptr; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe", meta = (ClampMin = "1")) + int32 Quantity = 1; +}; + +/** + * Une recette : ce qu'il faut, ce que ca donne, ou ca se fabrique. + * + * Un asset par recette, comme un DataAsset par objet. Trois comportements de + * craft n'existeront jamais : la recette est de la donnee pure, c'est le + * composant qui l'execute. + * + * Elle ne contient aucun etat -- ni "deja decouverte", ni "en cours de + * fabrication". Le jour ou les recettes se debloqueront en jeu, la liste des + * RecipeId connus vivra dans la sauvegarde, pas ici : sinon decouvrir une + * recette la debloquerait pour toutes les parties a la fois. + */ +UCLASS(BlueprintType) +class SURVIVAL_PROJET_API UCraftingRecipeDataAsset : public UPrimaryDataAsset +{ + GENERATED_BODY() + +public: + /** + * Identifiant stable, independant du nom de l'asset. + * Servira a la sauvegarde des recettes decouvertes : renommer un asset ne + * doit pas faire oublier au joueur ce qu'il avait debloque. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe") + FName RecipeId; + + /** Ce que la fabrication consomme. Une recette sans cout est acceptee (objet gratuit). */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe") + TArray Ingredients; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe") + TObjectPtr ResultItem; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe", meta = (ClampMin = "1")) + int32 ResultQuantity = 1; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe") + ECraftingStation RequiredStation = ECraftingStation::Hands; + + // Pas de duree de fabrication ici : le craft de menu est instantane, comme + // dans Valheim ou Raft. La fabrication qui prend du temps est celle des + // postes qui travaillent SANS le joueur, et ceux-la ne sont pas des recettes. + + /** + * Nom affiche, quand celui du resultat ne suffit pas. + * Laisse vide dans l'immense majorite des cas : dupliquer le nom de l'objet + * dans la recette, c'est se garantir deux textes qui divergeront un jour. + */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe|Display") + FText DisplayNameOverride; + + /** Icone affichee dans la grille de craft. Vide = celle du resultat. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Recipe|Display") + TObjectPtr IconOverride; + + /** Le nom a afficher, override compris. Passe toujours par ici, jamais par ResultItem directement. */ + UFUNCTION(BlueprintPure, Category = "Recipe") + FText GetDisplayName() const; + + /** L'icone a afficher, override compris. */ + UFUNCTION(BlueprintPure, Category = "Recipe") + UTexture2D* GetIcon() const; + +#if WITH_EDITOR + /** + * Une recette sans resultat ne planterait pas : elle ne ferait simplement + * RIEN, en consommant les ingredients. C'est exactement le genre de bug qu'on + * cherche pendant une heure -- autant que l'editeur le refuse a la sauvegarde. + */ + virtual EDataValidationResult IsDataValid(class FDataValidationContext& Context) const override; +#endif +}; diff --git a/Source/Survival_projet/Public/CraftingRecipeSlotWidget.h b/Source/Survival_projet/Public/CraftingRecipeSlotWidget.h new file mode 100644 index 0000000..f82d6b6 --- /dev/null +++ b/Source/Survival_projet/Public/CraftingRecipeSlotWidget.h @@ -0,0 +1,73 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "CraftingRecipeSlotWidget.generated.h" + +class UCraftingRecipeDataAsset; +class UCraftingWidget; +class UImage; +class UTextBlock; + +/** + * Une case de la grille de craft : l'icone d'une recette, cliquable. + * + * Passive comme la case d'inventaire : elle n'interroge jamais le composant de + * fabrication, c'est la page parente qui lui pousse son etat. Elle ne gere que + * son propre clic, parce que c'est elle qui recoit l'evenement souris. + */ +UCLASS(Abstract) +class SURVIVAL_PROJET_API UCraftingRecipeSlotWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** Appele une fois a la creation : dit a la case quelle recette elle represente. */ + void InitSlot(UCraftingRecipeDataAsset* InRecipe, UCraftingWidget* InOwningGrid); + + /** Affiche ou masque le cadre de selection. */ + UFUNCTION(BlueprintCallable, Category = "Crafting") + void SetSelected(bool bSelected); + + /** + * Assombrit la case quand les ressources manquent. + * La recette reste visible et cliquable : le joueur doit pouvoir consulter + * ce qui lui manque, sinon il ne sait pas quoi aller chercher. + */ + UFUNCTION(BlueprintCallable, Category = "Crafting") + void SetCraftable(bool bCraftable); + + UCraftingRecipeDataAsset* GetRecipe() const { return Recipe; } + +protected: + virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + + /** A nommer exactement "RecipeIcon" dans le Widget Blueprint. */ + UPROPERTY(meta = (BindWidget)) + TObjectPtr RecipeIcon; + + /** Facultatif : le nom sous l'icone, si tu veux une grille legendee. */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr RecipeNameText; + + /** Facultatif : cadre affiche quand la case est selectionnee. */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr SelectionBorder; + + /** Teinte de l'icone quand la recette est realisable. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting") + FLinearColor CraftableTint = FLinearColor::White; + + /** Teinte quand il manque des ressources. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Crafting") + FLinearColor UncraftableTint = FLinearColor(0.3f, 0.3f, 0.3f, 1.f); + +private: + UPROPERTY(Transient) + TObjectPtr Recipe; + + /** La page parente, a prevenir du clic. */ + TWeakObjectPtr OwningGrid; +}; diff --git a/Source/Survival_projet/Public/CraftingWidget.h b/Source/Survival_projet/Public/CraftingWidget.h new file mode 100644 index 0000000..60a30da --- /dev/null +++ b/Source/Survival_projet/Public/CraftingWidget.h @@ -0,0 +1,160 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "CraftingRecipeDataAsset.h" +#include "CraftingWidget.generated.h" + +class UButton; +class UCraftingComponent; +class UCraftingIngredientRowWidget; +class UCraftingRecipeSlotWidget; +class UImage; +class UInventoryComponent; +class UPanelWidget; +class UTextBlock; +class UUniformGridPanel; + +/** + * La page de fabrication : la grille de recettes a gauche, le detail de la + * recette choisie a droite. + * + * Elle ne calcule rien. UCraftingComponent lui dit quelles recettes afficher, + * lesquelles sont realisables, et fabrique quand on clique. Elle s'abonne a + * l'inventaire et aux postes accessibles pour se rafraichir -- jamais de Tick + * pour relire un etat. + */ +UCLASS(Abstract) +class SURVIVAL_PROJET_API UCraftingWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + + /** + * (Re)branche la page sur le pawn actuellement possede. + * Indispensable apres un respawn : le nouveau pawn a de nouveaux composants, + * et les anciens abonnements pointeraient vers des objets detruits. + */ + void BindToOwningPawn(); + + /** Relit la liste, l'etat de chaque case et le panneau de detail. */ + UFUNCTION(BlueprintCallable, Category = "Crafting") + void Refresh(); + + /** Appele par une case quand on clique dessus. */ + void NotifyRecipeClicked(UCraftingRecipeDataAsset* InRecipe); + + /** + * Restreint l'affichage a un seul poste. Count = tout ce qui est accessible. + * Purement visuel : ce filtre ne peut jamais rendre une recette realisable. + */ + UFUNCTION(BlueprintCallable, Category = "Crafting") + void SetStationFilter(ECraftingStation InFilter); + +protected: + /** A nommer exactement "RecipeGrid" dans le Widget Blueprint. */ + UPROPERTY(meta = (BindWidget)) + TObjectPtr RecipeGrid; + + /** L'icone du resultat, dans le panneau de droite. */ + UPROPERTY(meta = (BindWidget)) + TObjectPtr ResultIcon; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr ResultNameText; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr CraftButton; + + /** Facultatif : la description de l'objet produit. */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr ResultDescriptionText; + + /** Facultatif : "x2" quand la recette produit plusieurs exemplaires. Masque sinon. */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr ResultQuantityText; + + /** + * Facultatif : le panneau de droite dans son ensemble. Masque tant qu'aucune + * recette n'est choisie, pour ne pas afficher un cadre vide au premier + * affichage. + */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr DetailsPanel; + + /** Facultatif : message affiche quand aucune recette n'est disponible ici. */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr EmptyGridMessage; + + /** + * Le conteneur des lignes de cout. Declare en UPanelWidget et non en + * UVerticalBox : ca laisse le choix d'un Wrap Box ou d'un Grid Panel dans le + * Blueprint sans toucher au C++, puisqu'on ne fait qu'y ajouter des enfants. + */ + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr IngredientList; + + /** Ton WBP_RecipeSlot. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting") + TSubclassOf RecipeSlotClass; + + /** Ton WBP_IngredientRow. Sans elle, la liste de cout reste vide. */ + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting") + TSubclassOf IngredientRowClass; + + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Crafting", meta = (ClampMin = "1")) + int32 ColumnCount = 5; + +private: + UFUNCTION() + void HandleInventoryChanged(); + + UFUNCTION() + void HandleStationsChanged(); + + UFUNCTION() + void HandleCraftClicked(); + + /** Recree les cases uniquement si la liste affichee a change. */ + void RebuildGrid(const TArray& NewRecipes); + + /** Pousse l'etat realisable/non a chaque case et met a jour le panneau. */ + void RefreshStates(); + + void RefreshDetails(); + + /** + * Remplit la liste de cout de la recette choisie. + * + * Les lignes sont RECYCLEES et non recreees : cette fonction repasse a + * chaque changement d'inventaire, donc a chaque ramassage. Detruire puis + * reconstruire trois widgets par branche ramassee serait du gaspillage. + */ + void RefreshIngredients(); + + UCraftingComponent* ResolveCrafting() const; + + /** Detache les deux abonnements. Appele avant de rebrancher, et a la destruction. */ + void UnbindDelegates(); + + TWeakObjectPtr BoundCrafting; + + TWeakObjectPtr BoundInventory; + + UPROPERTY(Transient) + TObjectPtr SelectedRecipe; + + /** UPROPERTY obligatoire : sans ca le GC collecterait les cases. */ + UPROPERTY(Transient) + TArray> RecipeSlots; + + UPROPERTY(Transient) + TArray> IngredientRows; + + ECraftingStation StationFilter = ECraftingStation::Count; +}; diff --git a/Source/Survival_projet/Public/FpsPlayer.h b/Source/Survival_projet/Public/FpsPlayer.h index f150572..561dadf 100644 --- a/Source/Survival_projet/Public/FpsPlayer.h +++ b/Source/Survival_projet/Public/FpsPlayer.h @@ -8,13 +8,57 @@ class APickupItem; class UCameraComponent; +class UCraftingComponent; class UInputAction; class UInputMappingContext; class UInteractionComponent; class UInventoryComponent; +class UItemDataAsset; class USurvivalStatsComponent; struct FInputActionValue; +/** + * Ressort amorti à une dimension, partagé par les effets de caméra. + * + * Pourquoi un ressort plutôt qu'une courbe paramétrée par le temps : un ressort + * n'a pas d'animation « en cours » qu'il faudrait annuler pour en jouer une + * autre. Une nouvelle impulsion s'ajoute simplement à l'état courant, position + * et vitesse restent continues, donc le mouvement est interruptible à n'importe + * quel instant sans jamais produire de saut. C'est exactement ce que le crouch + * demande quand le joueur martèle la touche. + */ +struct FCameraSpring +{ + /** Déplacement courant, dans l'unité de ce que le ressort pilote. */ + float Offset = 0.f; + + /** Vitesse courante, par seconde. */ + float Velocity = 0.f; + + /** Impulsion pas encore versée, étalée sur RampTime pour éviter l'à-coup. */ + float PendingImpulse = 0.f; + + void AddImpulse(float Strength) { PendingImpulse += Strength; } + + /** Remet le ressort au repos, sans transition (respawn, téléportation). */ + void Reset() + { + Offset = 0.f; + Velocity = 0.f; + PendingImpulse = 0.f; + } + + /** + * Intègre le ressort d'un pas de temps et renvoie le nouvel Offset. + * + * @param Frequency fréquence propre en Hz : vitesse de retour au repos. + * @param Damping 1 = retour net sans rebond, en dessous ça oscille. + * @param RampTime durée sur laquelle une impulsion est versée (0 = d'un coup). + * @param MaxOffset garde-fou d'amplitude. + */ + float Update(float Step, float Frequency, float Damping, float RampTime, float MaxOffset); +}; + /** * Personnage joueur en vue première personne. * Les assets d'input (Input Mapping Context + Input Actions) se branchent @@ -48,6 +92,9 @@ public: UFUNCTION(BlueprintPure, Category = "Inventaire") UInventoryComponent* GetInventoryComponent() const { return InventoryComponent; } + UFUNCTION(BlueprintPure, Category = "Crafting") + UCraftingComponent* GetCraftingComponent() const { return CraftingComponent; } + UFUNCTION(BlueprintPure, Category = "Survie") USurvivalStatsComponent* GetSurvivalStats() const { return SurvivalStats; } @@ -100,6 +147,20 @@ public: UFUNCTION() void ApplyUserSettings(); + /** + * Pose la transform de la camera juste avant que la vue soit lue. + * + * Le gestionnaire de camera passe par ici pour construire le point de vue : + * c'est le seul endroit ou l'on est certain de lire la position de l'acteur + * et la demi-hauteur de la capsule au MEME instant. Le Tick du pawn, lui, + * n'a aucune dependance de tick vis-a-vis du CharacterMovement (verifie dans + * Character.cpp : seul le Mesh en recoit une), donc la frame ou la capsule + * change de taille pouvait etre calculee avec l'ancienne demi-hauteur -- + * soit un decalage de 28 cm sur une frame, a chaque accroupissement. + * Bonus : le regard n'a plus une frame de retard sur la souris. + */ + virtual void CalcCamera(float DeltaTime, FMinimalViewInfo& OutResult) override; + protected: // Called when the game starts or when spawned virtual void BeginPlay() override; @@ -133,6 +194,13 @@ protected: UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Inventaire") TObjectPtr InventoryComponent; + /** + * Fabrication. Sur le pawn comme l'inventaire : c'est ce corps qui fabrique, + * et il lit l'inventaire du meme acteur. + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Crafting") + TObjectPtr CraftingComponent; + /** Vie, faim, soif. Sur le pawn : c'est ce corps qui a faim. */ UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Survie") TObjectPtr SurvivalStats; @@ -259,17 +327,44 @@ protected: UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.01")) float CrouchTransitionDuration = 0.4f; - /** Tangage (degrés) au pic de la transition. Positif = le regard plonge. */ + /** + * Tangage (degrés) au pic de la bascule. Positif = le regard plonge. + * + * Le tilt n'est plus une courbe indexée sur l'avancement de la transition + * mais un ressort relancé à chaque changement d'état : c'est ce qui permet + * de s'accroupir et de se relever coup sur coup sans que la caméra saute. + * L'impulsion est normalisée pour que le pic tombe bien sur cette valeur. + */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi") float CrouchTiltPitch = 3.f; /** - * Roulis (degrés) au pic de la transition : bascule sur le côté. + * Roulis (degrés) au pic de la bascule : bascule sur le côté. * A 0 par defaut, le mouvement reste purement vertical. */ UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi") float CrouchTiltRoll = 0.f; + /** + * Fréquence du ressort de bascule (Hz). Le pic est atteint après environ + * 1/(2*PI*f) secondes : 0.9 Hz place le sommet vers 0.18 s, soit à peu près + * au milieu d'une transition de 0.4 s. Plus haut = plus sec et plus court. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.1")) + float CrouchTiltFrequency = 0.9f; + + /** Amortissement de la bascule. 1 = aucun rebond ; en dessous, la tête oscille. */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0.05", ClampMax = "2.0")) + float CrouchTiltDamping = 1.f; + + /** + * Durée (s) sur laquelle l'impulsion de bascule est versée dans le ressort. + * A 0 la rotation démarre à pleine vitesse angulaire en une frame, ce qui + * se lit comme un claquement ; quelques centièmes suffisent à l'adoucir. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Accroupi", meta = (ClampMin = "0")) + float CrouchTiltRampTime = 0.05f; + // ------------------------------------------------------------------ // Réglages : impacts caméra (saut / atterrissage) // ------------------------------------------------------------------ @@ -379,13 +474,34 @@ protected: void UseSelectedItem(); private: - /** Fait avancer toutes les animations de caméra et les applique. */ + /** Fait avancer toutes les animations de caméra (intégration uniquement). */ void UpdateCameraEffects(float DeltaTime); + /** + * Recopie l'état des animations dans la transform de la caméra. + * Séparée de l'intégration : appelée à la fois en fin de Tick et depuis + * CalcCamera, elle doit rester idempotente et sans notion de temps. + */ + void ApplyCameraTransform(); + + /** Relance la bascule de tête. Direction : +1 on s'accroupit, -1 on se relève. */ + void AddCrouchTilt(float Direction); + /** Branche sur OnDied du composant de survie. */ UFUNCTION() void HandleDeath(); + /** + * Le resultat d'une fabrication n'est pas entre dans l'inventaire : on le + * pose aux pieds du joueur plutot que de le laisser disparaitre. + * + * C'est le pawn qui fait ca et non UCraftingComponent : le composant n'a pas + * a savoir comment on engendre un objet dans le monde, et un etabli qui + * porterait le meme composant deposerait son surplus sur sa propre table. + */ + UFUNCTION() + void HandleCraftOverflow(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); @@ -400,17 +516,15 @@ private: /** Valeur visée par CrouchAlpha. */ float CrouchTargetAlpha = 0.f; - /** +1 quand on s'accroupit, -1 quand on se relève : inverse le sens du tilt. */ - float CrouchTiltDirection = 1.f; + /** Déplacement vertical de la caméra (cm) : sauts, atterrissages, explosions. */ + FCameraSpring PunchSpring; - /** Décalage vertical courant de la caméra (cm), état du ressort d'impact. */ - float PunchOffset = 0.f; - - /** Vitesse du ressort d'impact (cm/s). */ - float PunchVelocity = 0.f; - - /** Impulsion restant à injecter dans le ressort, étalée sur PunchImpulseRampTime. */ - float PendingPunch = 0.f; + /** + * Bascule de tête pendant l'accroupissement, en unités normalisées : + * son pic vaut 1, ce sont CrouchTiltPitch et CrouchTiltRoll qui le + * convertissent en degrés. + */ + FCameraSpring CrouchTiltSpring; /** Vitesse verticale mémorisée pendant la chute, pour calibrer l'atterrissage. */ float LastFallSpeed = 0.f; diff --git a/Source/Survival_projet/Public/FpsPlayerController.h b/Source/Survival_projet/Public/FpsPlayerController.h index ddc5786..948c6e8 100644 --- a/Source/Survival_projet/Public/FpsPlayerController.h +++ b/Source/Survival_projet/Public/FpsPlayerController.h @@ -3,6 +3,9 @@ #pragma once #include "CoreMinimal.h" +// ESlateVisibility n'est que declare en avant par le moteur : il faut le type +// complet pour en stocker une valeur par defaut. +#include "Components/SlateWrapperTypes.h" #include "GameFramework/PlayerController.h" #include "FpsPlayerController.generated.h" @@ -11,7 +14,7 @@ class UHotbarWidget; class UInputAction; class UInventoryComponent; class UInteractionPromptWidget; -class UInventoryWidget; +class UInventoryScreenWidget; class UPauseMenuWidget; class UScreenFadeComponent; class USettingsMenuWidget; @@ -32,10 +35,17 @@ class SURVIVAL_PROJET_API AFpsPlayerController : public APlayerController public: AFpsPlayerController(); - /** Ouvre ou ferme l'inventaire. */ + /** Ouvre ou ferme l'ecran, toujours sur l'onglet Inventaire. */ UFUNCTION(BlueprintCallable, Category = "UI") void ToggleInventory(); + /** + * Ouvre l'ecran directement sur l'onglet Fabrication. + * Depuis l'onglet Inventaire elle bascule, depuis l'onglet Fabrication elle referme. + */ + UFUNCTION(BlueprintCallable, Category = "UI") + void ToggleCrafting(); + UFUNCTION(BlueprintPure, Category = "UI") bool IsInventoryOpen() const { return bInventoryOpen; } @@ -99,6 +109,14 @@ protected: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") TObjectPtr ToggleInventoryAction; + /** + * Ouverture directe sur l'onglet Fabrication, valeur Digital (bool). + * Sur le controller comme ToggleInventoryAction : elle doit repondre alors + * meme que l'input du pawn est coupe, ecran ouvert. + */ + UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Input") + TObjectPtr ToggleCraftingAction; + /** * Jette l'objet survole, valeur Digital (bool). Ctrl enfonce = un seul * exemplaire au lieu de la pile entiere. @@ -146,9 +164,13 @@ protected: UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI") TSubclassOf InteractionPromptClass; - /** Ton WBP_Inventory. */ + /** + * Ton WBP_InventoryScreen : l'ecran a onglets qui contient la grille + * d'inventaire ET la page de fabrication. Ce n'est plus WBP_Inventory + * directement, celui-ci est devenu une page a l'interieur. + */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI") - TSubclassOf InventoryClass; + TSubclassOf InventoryScreenClass; /** Ton WBP_Hotbar, affiche en permanence. */ UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI") @@ -201,7 +223,7 @@ protected: TObjectPtr InteractionPrompt; UPROPERTY(Transient, BlueprintReadOnly, Category = "UI") - TObjectPtr InventoryWidget; + TObjectPtr InventoryScreen; UPROPERTY(Transient, BlueprintReadOnly, Category = "UI") TObjectPtr Hotbar; @@ -246,6 +268,26 @@ private: UFUNCTION() void HandleSettingsCloseRequested(); + /** + * Point d'entree unique de l'ouverture et de la fermeture de l'ecran. + * + * Tab et la touche de fabrication passent tous les deux par ici : le mode + * d'input, la souris et la barre rapide sont ainsi decides a un seul endroit. + * Deux chemins separes finiraient par diverger. + */ + void SetInventoryScreenOpen(bool bOpen, bool bShowCraftingTab); + + /** L'ecran a change d'onglet : la barre rapide n'a rien a faire sous la page de craft. */ + UFUNCTION() + void HandleInventoryTabChanged(bool bInventoryTabActive); + + /** + * Etat unique de la barre rapide : visible partout, sauf par-dessus l'onglet + * Fabrication. Passer par une seule fonction evite les cas ou la fermeture + * de l'ecran laisserait la barre cachee. + */ + void UpdateHotbarVisibility(); + /** Jette la pile survolee dans la grille, si l'inventaire est ouvert. */ void DropHoveredItem(); @@ -266,6 +308,17 @@ private: bool bInventoryOpen = false; + /** Onglet Fabrication affiche. N'a de sens que si bInventoryOpen est vrai. */ + bool bCraftingTabActive = false; + + /** + * Visibilite d'origine de la barre rapide, relevee a sa creation. + * On la restaure telle quelle plutot que de forcer Visible : le Blueprint a + * peut-etre choisi SelfHitTestInvisible, et l'ecraser changerait + * silencieusement son comportement au clic. + */ + ESlateVisibility HotbarDefaultVisibility = ESlateVisibility::Visible; + bool bPauseMenuOpen = false; /** diff --git a/Source/Survival_projet/Public/InventoryScreenWidget.h b/Source/Survival_projet/Public/InventoryScreenWidget.h new file mode 100644 index 0000000..5c36518 --- /dev/null +++ b/Source/Survival_projet/Public/InventoryScreenWidget.h @@ -0,0 +1,111 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "InventoryScreenWidget.generated.h" + +class UButton; +class UCraftingWidget; +class UInventoryWidget; +class UWidgetSwitcher; + +/** + * L'onglet visible a change. true = Inventaire, false = Fabrication. + * + * L'ecran se contente d'annoncer. Ce que ca implique pour le reste du HUD -- + * masquer la barre rapide, par exemple -- ne le regarde pas : il serait alors + * un widget reutilisable qui connait la pile de son hote, exactement ce qu'on + * evite partout ailleurs. + */ +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryScreenTabChanged, bool, bInventoryTabActive); + +/** + * L'ecran que Tab ouvre : une barre d'onglets et deux pages, Inventaire et + * Fabrication. + * + * Il ne contient aucune logique de l'une ni de l'autre. Les pages restent des + * widgets autonomes -- la grille d'inventaire n'a pas change d'une ligne en + * devenant une page -- et l'ecran ne fait que les montrer, les rebrancher apres + * un respawn, et relayer au controller ce dont il a besoin. + * + * Classe Abstract : on cree un WBP_InventoryScreen qui en herite. + */ +UCLASS(Abstract) +class SURVIVAL_PROJET_API UInventoryScreenWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + virtual void NativeConstruct() override; + + /** Rebranche les deux pages sur le pawn possede. A appeler depuis OnPossess. */ + void BindToOwningPawn(); + + /** + * Index de la case d'inventaire survolee, ou INDEX_NONE. + * + * Renvoie INDEX_NONE des que l'onglet Fabrication est affiche : sans ca, la + * touche "jeter" agirait sur une case invisible, sous le panneau de craft. + */ + int32 GetHoveredSlotIndex() const; + + /** Revient sur l'onglet Inventaire. Appele a chaque ouverture par Tab. */ + UFUNCTION(BlueprintCallable, Category = "UI") + void ShowInventoryTab(); + + /** Bascule sur l'onglet Fabrication. Appele par la touche dediee. */ + UFUNCTION(BlueprintCallable, Category = "UI") + void ShowCraftingTab(); + + UPROPERTY(BlueprintAssignable, Category = "UI") + FOnInventoryScreenTabChanged OnTabChanged; + +protected: + /** + * Absorbe ou traite les depots qui ne visent aucune page. + * Meme regle que dans la grille : lache hors des cases, l'objet est jete. + */ + virtual bool NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation) override; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr TabSwitcher; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr InventoryTabButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr CraftingTabButton; + + /** + * Ton WBP_Inventory, pose comme enfant du WidgetSwitcher et renomme + * "InventoryPage". Les deux pages sont obligatoires : un ecran a onglets + * auquel il manque une page n'a aucun sens, autant que le Blueprint refuse + * de compiler. + */ + UPROPERTY(meta = (BindWidget)) + TObjectPtr InventoryPage; + + /** Ton WBP_Crafting, renomme "CraftingPage". */ + UPROPERTY(meta = (BindWidget)) + TObjectPtr CraftingPage; + +private: + UFUNCTION() + void HandleInventoryTabClicked(); + + UFUNCTION() + void HandleCraftingTabClicked(); + + /** + * Affiche une page et grise son bouton d'onglet. + * + * On passe le widget et non un index en dur : l'ordre des enfants du + * WidgetSwitcher se change d'un glisser dans la Designer, et un 0/1 code ici + * inverserait silencieusement les onglets le jour ou ca arrive. + */ + void ShowPage(UWidget* Page); + + bool IsInventoryTabActive() const; +}; diff --git a/Source/Survival_projet/Public/SettingsControlsWidget.h b/Source/Survival_projet/Public/SettingsControlsWidget.h index 5dfd78f..b1d3210 100644 --- a/Source/Survival_projet/Public/SettingsControlsWidget.h +++ b/Source/Survival_projet/Public/SettingsControlsWidget.h @@ -68,6 +68,9 @@ protected: UPROPERTY(meta = (BindWidget)) TObjectPtr ToggleInventoryRow; + UPROPERTY(meta = (BindWidget)) + TObjectPtr ToggleCraftingRow; + UPROPERTY(meta = (BindWidget)) TObjectPtr DropItemRow;