Files
Unreal_EmberWild/Docs/Crafting.md
T
Mathew 41d6e6a7f3 (Feat) Add Storage Chests + Item Details Panel
Coffres : AStorageContainer, un UInventoryComponent configure en conteneur,
affiche comme panneau de WBP_InventoryScreen plutot que dans un ecran a lui.
Deplacement unifie par UInventoryComponent::TransferSlot / TransferAllTo.

Panneau de detail : UItemDetailsWidget, pose DANS WBP_Inventory a droite de la
grille. Icone, nom, separation, description, barre d'usure. Il recoit un couple
(inventaire, index) et s'abonne a OnInventoryChanged, donc une charge consommee
sous le curseur se voit.

Une case ne connait plus sa grille : elle diffuse OnHoverChanged. La barre
rapide s'y abonne aussi et relaie par le PlayerController, seul a posseder a la
fois le HUD et l'ecran. Le panneau efface la case qu'on QUITTE et non lui-meme,
sinon passer de la barre a la grille viderait ce qui vient d'etre affiche.

L'ecran allume et eteint le panneau (SetItemDetailsEnabled) : WBP_Inventory
etant instancie deux fois en mode coffre, une case a cocher par instance serait
un bug qui attend qu'on oublie de la decocher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 22:51:24 +02:00

399 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Système de fabrication
Documentation du système de craft : architecture, API, câblage éditeur, décisions de design
et dépannage.
---
## 1. Vue d'ensemble
Quatre couches, chacune ignorante de celle du dessus.
```
DONNÉES DA_Craft_* ──► DA_RecipeBook
LOGIQUE UCraftingComponent (sur le pawn)
│ lit UInventoryComponent du même acteur
INTERFACE UCraftingWidget ──► UCraftingRecipeSlotWidget
└──► UCraftingIngredientRowWidget
CONTRÔLE AFpsPlayerController (ouvre, ferme, décide du HUD)
MONDE ACraftingStation (l'établi, via l'interaction)
```
Règle qui gouverne tout le reste : **le composant calcule, les widgets affichent.**
Aucun widget n'interroge l'inventaire directement.
---
## 2. Les données
### `UCraftingRecipeDataAsset`
Un asset par recette (`DA_Craft_*`). Donnée pure, **aucun état**.
| Champ | Rôle |
|---|---|
| `RecipeId` | identifiant stable pour la sauvegarde des recettes débloquées |
| `Ingredients` | tableau de `FCraftIngredient { Item, Quantity }` |
| `ResultItem` / `ResultQuantity` | ce qui est produit |
| `RequiredStation` | `Hands` ou `Workbench` |
| `DisplayNameOverride` / `IconOverride` | à laisser vides sauf cas particulier |
`GetDisplayName()` et `GetIcon()` retombent sur le `ResultItem` quand les overrides sont vides.
**Passer toujours par ces deux fonctions**, jamais par `ResultItem` directement.
`IsDataValid()` refuse à la sauvegarde une recette sans résultat, et signale un `RecipeId` vide
ou un objet listé deux fois dans les ingrédients. Ces trois erreurs ne planteraient pas — elles
produiraient un comportement faux et silencieux.
### `UCraftingRecipeBook`
Le catalogue (`DA_RecipeBook`). Un simple `TArray` de recettes, dont **l'ordre est l'ordre
d'affichage de la grille**.
Un catalogue plutôt qu'un scan de l'AssetManager : contenu déterministe, rien ne se charge par
surprise, et l'ordre devient une donnée de design réglée à la souris. Un catalogue plutôt qu'une
liste sur le composant, aussi : un établi pourra pointer son propre livre.
### `ECraftingStation`
```cpp
enum class ECraftingStation : uint8 { Hands, Workbench, Count /*sentinelle*/ };
```
`Count` n'est jamais assignée à une recette. Elle sert de valeur « tout » pour le filtre
d'affichage et permet de parcourir l'enum pour construire des boutons.
**Fourneau et feu de cuisson n'y figurent pas volontairement** : ils ne passent par aucun menu.
Ce seront des acteurs posés dans le monde où l'on dépose une ressource et où le mesh change
d'état, façon Raft. Leur file d'attente leur appartiendra.
---
## 3. La logique — `UCraftingComponent`
Sur le pawn, à côté de `UInventoryComponent`, qu'il résout une fois à l'initialisation.
### API
| Appel | Usage |
|---|---|
| `GatherRecipes(Out, StationFilter)` | peupler la grille |
| `CanCraft(Recipe, OutReason)` | activer ou griser un bouton |
| `GatherIngredientStatus(Recipe, Out)` | les compteurs « 2/3 » |
| `Craft(Recipe)` | le clic |
| `HasStation(Station)` | test ponctuel |
| `AddAvailableStation` / `RemoveAvailableStation` | par un poste de travail |
Délégués : `OnCraftSucceeded(Recipe)`, `OnAvailableStationsChanged()`,
`OnCraftOverflow(Item, Quantity, RemainingUses)`.
`GatherRecipes` n'a **pas** de valeur par défaut sur `StationFilter` : UHT refuse une entrée
`UMETA(Hidden)` comme défaut, et dé-cacher `Count` la rendrait sélectionnable dans le champ
`Required Station` d'une recette. L'appelant écrit `ECraftingStation::Count` pour « tout ».
### Postes disponibles
`TMap<ECraftingStation, int32>` de **compteurs**, pas un `TSet`. Deux établis dont les portées
se chevauchent s'ajoutent tous les deux ; sortir du premier ne doit pas retirer l'accès alors
qu'on est encore devant le second. `Hands` est initialisée à 1 et ne peut pas être retirée.
La diffusion de `OnAvailableStationsChanged` n'a lieu que sur les transitions **0 → 1** et
**1 → 0**. Entrer dans un deuxième établi ne reconstruit pas la grille pour rien.
**Disponibilité et filtre d'affichage sont deux notions distinctes.** La disponibilité est une
*condition* de fabrication, le filtre est *visuel*. Les mélanger finirait par rendre un craft
impossible à cause d'un bouton d'interface, et ce genre de bug ne se comprend jamais sur le
moment.
### `Craft()` — l'ordre compte
```
1. CanCraft() → refus, rien n'est touché
2. RemoveItem() par ingrédient → consommation
3. AddItem(résultat) → ajout
4. reste > 0 ? OnCraftOverflow → le pawn le pose au sol
```
**On consomme avant d'ajouter.** Refuser parce que l'inventaire est plein serait faux :
consommer trois branches libère justement la case du résultat. Et si le résultat ne rentre
malgré tout pas, il part par `OnCraftOverflow` — **aucune ressource ne disparaît, ni à l'entrée
ni à la sortie**.
Le composant ne fait pas apparaître le pickup lui-même : il ne sait pas engendrer un acteur, et
un établi qui porterait le même composant déposerait son surplus sur sa propre table. C'est
`AFpsPlayer::HandleCraftOverflow()` qui s'en charge, abonné dans son `BeginPlay`. Si personne
n'écoute, un `UE_LOG(Error)` le signale au lieu de perdre l'objet en silence.
Le résultat passe par `AddItem()`, donc **exactement comme un ramassage** : piles entamées
d'abord, puis barre rapide, puis grille.
---
## 4. L'interface
### `UInventoryScreenWidget` — l'écran à onglets
Ce que Tab ouvre. Il ne contient aucune logique : les deux pages restent des widgets autonomes,
et **`UInventoryWidget` n'a pas changé d'une ligne** en devenant une page.
| BindWidget | Type | Obligatoire |
|---|---|---|
| `TabSwitcher` | Widget Switcher | oui |
| `InventoryTabButton` | Button | oui |
| `CraftingTabButton` | Button | oui |
| `InventoryPage` | ton `WBP_Inventory` | oui |
| `CraftingPage` | ton `WBP_Crafting` | oui |
- `GetHoveredSlotIndex()` renvoie `INDEX_NONE` dès que l'onglet Fabrication est affiché, sinon
la touche « jeter » agirait sur une case cachée sous le panneau de craft.
- `ShowPage()` prend le **widget**, jamais un index : l'ordre des enfants d'un WidgetSwitcher se
change d'un glisser dans la Designer, un `0`/`1` en dur inverserait silencieusement les onglets.
- Diffuse `OnTabChanged(bool bInventoryTabActive)` et **rien d'autre**. Ce que ça implique pour
le HUD ne le regarde pas.
### `UCraftingWidget` — la page de fabrication
| BindWidget | Type | |
|---|---|---|
| `RecipeGrid` | Uniform Grid Panel | obligatoire |
| `ResultIcon` | Image | obligatoire |
| `ResultNameText` | Text Block | obligatoire |
| `CraftButton` | Button | obligatoire |
| `DetailsPanel` | n'importe quel Widget | optionnel |
| `IngredientList` | n'importe quel Panel | optionnel |
| `ResultQuantityText` | Text Block | optionnel |
| `ResultDescriptionText` | Text Block | optionnel |
| `EmptyGridMessage` | n'importe quel Widget | optionnel |
Réglages : `RecipeSlotClass`, `IngredientRowClass`, `ColumnCount`.
`IngredientList` est déclaré en **`UPanelWidget`** et non en `UVerticalBox` : le code ne fait qu'y
ajouter des enfants, donc passer à un Wrap Box ou un Grid Panel ne demande pas une ligne de C++.
`DetailsPanel` est déclaré en `UWidget` : le nom peut donc être porté par une **Border**, ce qui
masque le fond en même temps que le contenu quand rien n'est sélectionné.
**Rafraîchissement, sans jamais de Tick :**
- abonné à `OnInventoryChanged` → recalcule seulement les teintes et l'état du bouton
- abonné à `OnAvailableStationsChanged` → recalcule la liste
`RebuildGrid` compare la nouvelle liste à l'ancienne et **sort immédiatement si elle est
identique**. Les lignes d'ingrédient sont **recyclées** et non recréées, puisque `RefreshDetails`
repasse à chaque objet ramassé.
`HandleCraftClicked` ne re-vérifie pas `CanCraft` : `Craft()` le refait de toute façon, et
dupliquer la condition, c'est se garantir qu'elles divergeront.
### `UCraftingRecipeSlotWidget` — une case
`RecipeIcon` obligatoire ; `RecipeNameText` et `SelectionBorder` optionnels.
Réglages : `CraftableTint`, `UncraftableTint`.
Les recettes non réalisables restent **visibles et cliquables**, seulement assombries : le joueur
doit pouvoir consulter ce qui lui manque, sinon il ne sait pas quoi aller chercher.
> **La racine du Widget Blueprint doit avoir `Visibility = Visible`.** En
> `Self Hit Test Invisible`, la case ne reçoit jamais le clic.
### `UCraftingIngredientRowWidget` — une ligne de coût
`IngredientCountText` obligatoire ; `IngredientIcon` et `IngredientNameText` optionnels.
Réglages : `EnoughColor`, `MissingColor`, `CountFormat` (`{0}` possédé, `{1}` requis).
Elle n'interroge jamais l'inventaire : le composant lui remet un `FCraftIngredientStatus` déjà
calculé. Une ligne qui compterait elle-même referait le travail autant de fois qu'il y a
d'ingrédients, et pourrait afficher autre chose que ce sur quoi le bouton s'appuie.
---
## 5. Le contrôleur
### Ouverture et fermeture
Tout passe par **`SetInventoryScreenOpen(bOpen, bShowCraftingTab)`** — point d'entrée unique. Le
mode d'input, le curseur, la coupure d'input du pawn, la barre rapide et le poste actif sont
décidés à un seul endroit. Deux chemins parallèles finiraient par diverger.
| Appel | Comportement |
|---|---|
| `ToggleInventory()` | Tab — ouvre/ferme, toujours sur l'onglet Inventaire |
| `ToggleCrafting()` | touche dédiée — ouvre sur Fabrication, bascule depuis Inventaire, referme depuis Fabrication |
| `OpenCraftingAtStation(Station)` | depuis un établi — accorde le poste puis ouvre |
Quand l'écran est **déjà ouvert**, on ne fait que changer d'onglet : repasser par le mode d'input
serait au mieux inutile, au pire nuisible — `SetIgnoreLookInput` gère un compteur qu'un appel en
trop déséquilibrerait durablement.
### Le poste actif
`ActiveStation` retient le poste accordé par le dernier `OpenCraftingAtStation`. Il est rendu
dans `SetInventoryScreenOpen(false, …)`, donc **quel que soit le chemin de sortie** — Tab, Échap,
la mort. On fabrique à l'établi parce qu'on est en train de s'en servir, pas parce qu'on en a
croisé un un jour.
### La barre rapide
| Situation | Barre rapide |
|---|---|
| écran fermé | visible |
| onglet Inventaire | visible |
| onglet Fabrication | masquée |
Une seule fonction décide : `UpdateHotbarVisibility()`. Sinon, fermer l'écran depuis l'onglet
craft laisserait la barre cachée pour le reste de la partie.
Sa visibilité d'origine est **relevée à la création** et restaurée telle quelle, plutôt que
forcée à `Visible` : le Blueprint a peut-être choisi `SelfHitTestInvisible`, et l'écraser
changerait son comportement au clic sans qu'on l'ait demandé.
### Remappage
L'action est exposée dans l'onglet Touches sous l'identifiant **`ToggleCrafting`**, qui doit
correspondre **exactement** au champ `Name` des *Player Mappable Key Settings* du mapping dans
`IMC_Default`. Une faute de frappe donne une ligne « Non assignée » sans le moindre message.
---
## 6. L'établi — `ACraftingStation`
Un acteur interactif, détecté par le trace de `UInteractionComponent` comme un objet au sol.
Il ne fabrique rien et ne contient aucune recette : il ouvre l'écran en annonçant **son** type de
poste, et le composant du joueur filtre en conséquence.
**Un seul `BP_CraftingStation` suffit pour tous les postes** — c'est `StationType` qui change par
instance, comme `ItemData` sur `BP_Pickup`.
### Pourquoi pas de sphère d'interaction
`APickupItem` en a une parce que les meshes d'asset packs sont souvent livrés **sans collision
simple**, et que le trace est lancé en `bTraceComplex = false` — il les traverserait.
Pour un meuble construit, c'est l'inverse : le trace s'arrête au **premier bloqueur**, donc une
sphère englobant la structure serait touchée avant le mesh. Le prompt apparaîtrait en visant le
vide autour, et la sphère masquerait un objet posé au pied du meuble.
Le `Mesh` est donc réglé explicitement sur le profil **`BlockAll`** : il empêche le joueur de
traverser *et* bloque `Visibility` pour que le trace le trouve. Un profil laissant passer
`Visibility` rendrait le poste inutilisable sans qu'aucune erreur ne le signale.
> **Prérequis** : le Static Mesh doit avoir une collision simple. Vérifie avec
> **Show → Simple Collision** dans l'éditeur de mesh. Sinon, `Collision → Add Box Simplified
> Collision`, ou pose une Box Collision dédiée dans le Blueprint.
---
## 7. Ajouter une recette
1. `Content/Game/Data/Crafting` → clic droit → **Miscellaneous → Data Asset** → classe
**`CraftingRecipeDataAsset`** → nommer `DA_Craft_<Objet>`.
2. Remplir : `RecipeId`, `Ingredients`, `ResultItem`, `ResultQuantity`, `RequiredStation`.
3. Ouvrir **`DA_RecipeBook`** et ajouter la recette dans `Recipes`, **à sa place dans l'ordre
d'affichage**.
4. Vérifier que le `ResultItem` a bien une **icône** — sans elle la case sera vide et tu croiras
à un bug de code.
5. Sélectionner les assets → clic droit → **Asset Actions → Validate Assets** → 0 erreur.
Aucune recompilation, aucun Blueprint à toucher.
## 8. Ajouter un poste de travail
1. Ajouter la valeur dans `ECraftingStation`, **avant `Count`**.
2. Créer un `BP_` dérivé de `CraftingStation`, régler son `Mesh` et son `StationType`.
3. Poser l'acteur dans le niveau.
Rien d'autre : la grille, le filtrage et le bouton suivent.
---
## 9. Récapitulatif des noms à ne pas casser
Un `BindWidget` obligatoire mal orthographié **empêche le Blueprint de compiler**, avec le nom du
coupable dans le Compiler Results. Un `BindWidgetOptional` mal orthographié **échoue en silence**.
| Widget Blueprint | Noms obligatoires | Noms optionnels |
|---|---|---|
| `WBP_InventoryScreen` | `TabSwitcher`, `InventoryTabButton`, `CraftingTabButton`, `InventoryPage`, `CraftingPage` | — |
| `WBP_Crafting` | `RecipeGrid`, `ResultIcon`, `ResultNameText`, `CraftButton` | `DetailsPanel`, `IngredientList`, `ResultQuantityText`, `ResultDescriptionText`, `EmptyGridMessage` |
| `WBP_RecipeSlot` | `RecipeIcon` | `RecipeNameText`, `SelectionBorder` |
| `WBP_IngredientRow` | `IngredientCountText` | `IngredientIcon`, `IngredientNameText` |
Assignations à ne pas oublier :
| Où | Champ | Valeur |
|---|---|---|
| `BP_FpsPlayer``CraftingComponent` | Recipe Book | `DA_RecipeBook` |
| `BP_FpsPlayerController` | Inventory Screen Class | `WBP_InventoryScreen` |
| `BP_FpsPlayerController` | Toggle Crafting Action | `IA_ToggleCrafting` |
| `WBP_Crafting` | Recipe Slot Class | `WBP_RecipeSlot` |
| `WBP_Crafting` | Ingredient Row Class | `WBP_IngredientRow` |
---
## 10. Pièges UMG rencontrés
Tous ont coûté du temps au moins une fois.
- **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`.
- **Pour un cadre de taille fixe, le Size Box va autour de la Border.** Le widget le plus externe
gagne toujours.
- **Une Border ne peint rien avec `Draw As = Image` sans texture.** Pour un aplat, `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.
- **Un Uniform Grid Panel répartit la largeur qu'on lui donne en parts égales.** En `Fill`, les
cases se retrouvent espacées ; il faut `Left` ou `Center` sur son slot pour qu'il prenne sa
taille désirée. L'espacement se règle avec `Slot Padding`.
- **`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 fausse la mise en page dans la Designer.** Y mettre une
valeur représentative — le code l'écrase à l'exécution.
- **L'aperçu `Fill Screen`** étire la racine à 1920 × 1080. Basculer sur `Desired Size` pour voir
la vraie taille.
---
## 11. Dépannage
| Symptôme | Cause probable |
|---|---|
| Grille vide, message « aucune recette » | `Recipe Book` non assigné, ou toutes les recettes en `Workbench` |
| Grille vide, aucun message | `RecipeSlotClass` non assignée — voir Output Log |
| Une case ne réagit pas au clic | racine de `WBP_RecipeSlot` en `Self Hit Test Invisible` |
| Cases vides sans icône | le `ResultItem` n'a pas d'icône |
| Panneau de détail toujours affiché | `DetailsPanel` non nommé, donc non lié |
| Liste d'ingrédients vide | `IngredientRowClass` non assignée |
| Le bouton reste grisé avec les ressources | `RequiredStation` inaccessible — pas devant l'établi |
| Ligne « Non assignée » dans le menu Touches | `Name` des Player Mappable Key Settings ≠ `ToggleCrafting` |
| Le prompt de l'établi n'apparaît jamais | le mesh n'a pas de collision simple |
| Objets perdus après un craft | `OnCraftOverflow` sans abonné — cherche l'`Error` dans l'Output Log |
Les messages du système sont préfixés `UCraftingComponent`, `UCraftingWidget` ou
`UInventoryWidget` dans l'Output Log.
---
## 12. Reste à faire
- **Boutons de filtre par poste** — `SetStationFilter()` existe, `ECraftingStation::Count` est la
sentinelle « tout ».
- **Recettes réelles** — les deux `DA_Craft_Test_*` sont du jetable bâti sur `DA_Branch` et
`DA_Rocher`.
- **Recettes à découvrir** — `RecipeId` est prévu pour ça ; la liste des identifiants connus ira
dans la sauvegarde, jamais dans le DataAsset.
- **Retour joueur** — son de fabrication sur `OnCraftSucceeded`, message quand le résultat tombe
au sol.
- **Postes qui travaillent sans le joueur** — fourneau, séchoir. Un **autre système** : acteurs du
monde avec file d'attente et changement de mesh, sans passer par ce menu.