(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>
This commit is contained in:
@@ -0,0 +1,419 @@
|
||||
# Système de cuisson
|
||||
|
||||
Postes de transformation posés dans le monde : feu de camp aujourd'hui, fourneau et
|
||||
séchoir demain. Le joueur dépose un objet dessus, attend, et le récupère transformé.
|
||||
|
||||
**Fichiers** : `CookingStation.h/.cpp`, plus le bloc *Cooking* / *Fuel* de `ItemDataAsset.h`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Le principe
|
||||
|
||||
Le modèle est celui de Raft, assumé jusqu'au bout : **aucune interface**. Pas de fenêtre,
|
||||
pas de barre de progression, pas de compteur. Le joueur tient sa viande crue dans la barre
|
||||
rapide, vise un emplacement du grill, appuie sur Interagir, et regarde le mesh changer.
|
||||
|
||||
Trois conséquences qui structurent tout le reste :
|
||||
|
||||
- **Le poste ne commente jamais son propre état.** Le prompt d'interaction n'affiche que des
|
||||
actions réalisables, exactement comme un objet à ramasser. Ce qui cuit, ce qui manque, ce
|
||||
qui brûle se lit sur les flammes, sur le nombre de bûches et sur le mesh de la nourriture.
|
||||
- **Le feu s'allume et s'arrête tout seul.** Il n'y a pas de geste « allumer ». Le bois brûle
|
||||
s'il y a du travail, et attend sinon.
|
||||
- **Aucun `Tick`.** Toute la temporalité passe par des timers.
|
||||
|
||||
Ce n'est **pas** un système de recettes. La cuisson n'est pas un choix fait dans un menu,
|
||||
c'est une propriété de l'objet — voir §2. Le `CLAUDE.md` l'avait tranché dès le craft :
|
||||
*« Fourneau et feu de cuisson n'ont volontairement PAS leur entrée dans `ECraftingStation` »*.
|
||||
|
||||
---
|
||||
|
||||
## 2. Les données — `UItemDataAsset`
|
||||
|
||||
### 2.1 La chaîne de transformation
|
||||
|
||||
Cuire n'est pas une recette mais un **chaînage d'objets**. « Steak cru » pointe vers « Steak
|
||||
cuit », qui pointe vers « Steak brûlé ». Chaque maillon est un DataAsset complet, donc le
|
||||
changement de visuel sur le feu ne coûte pas une ligne de code : c'est le `WorldMesh` du
|
||||
maillon suivant.
|
||||
|
||||
| Champ | Type | Rôle |
|
||||
|---|---|---|
|
||||
| `bCanBeCooked` | `bool` | L'objet se transforme sur un feu actif |
|
||||
| `CookDuration` | `float` | Secondes de feu **actif** nécessaires |
|
||||
| `CookedResult` | `UItemDataAsset*` | Ce qu'il devient |
|
||||
| `bCanBurn` | `bool` | L'objet se dégrade s'il reste sur le feu |
|
||||
| `BurnDuration` | `float` | Secondes avant dégradation |
|
||||
| `BurntResult` | `UItemDataAsset*` | Ce qu'il devient |
|
||||
| `bIsFuel` | `bool` | L'objet peut alimenter un feu |
|
||||
| `FuelDuration` | `float` | Secondes de combustion par exemplaire |
|
||||
|
||||
`bCanBeCooked` et `bCanBurn` sont **volontairement indépendants**. Une seule chaîne uniforme
|
||||
imposerait le même comportement à tout le monde, alors qu'on veut pouvoir décrire :
|
||||
|
||||
- un objet qui cuit puis brûle (viande crue → cuite → brûlée) ;
|
||||
- un objet qui brûle sans jamais avoir été cuisinable (une planche jetée dans les flammes) ;
|
||||
- un plat qui reste indéfiniment au chaud sans se gâcher (`bCanBurn` décoché).
|
||||
|
||||
Si les deux sont cochés sur le même asset, **la cuisson passe en premier**. C'est le maillon
|
||||
*cuit* qui portera `bCanBurn` pour continuer vers le brûlé.
|
||||
|
||||
### 2.2 Ce qui n'est PAS dans le DataAsset
|
||||
|
||||
Le temps déjà passé sur le feu. C'est de l'état, il vit sur la station. Le mettre dans le
|
||||
DataAsset ferait cuire tous les steaks du monde en même temps — c'est la règle générale du
|
||||
projet : *le DataAsset décrit ce qu'un objet **est**, jamais où il en est*.
|
||||
|
||||
### 2.3 Le visuel : `WorldMeshMaterial`
|
||||
|
||||
`UItemDataAsset` porte, à côté de `WorldMesh`, un **`WorldMeshMaterial`** optionnel qui
|
||||
n'écrase que le premier slot de matériau.
|
||||
|
||||
Il existe pour la cuisson : dès qu'un pack partage une géométrie entre plusieurs aliments —
|
||||
c'est le cas de `StylizedFood_JC`, 73 meshes pour 4 instances de matériau — une chaîne à
|
||||
trois étapes demanderait trois meshes distincts là où un seul suffit avec trois teintes.
|
||||
|
||||
La règle « mesh + matériau optionnel » est centralisée dans **`ApplyWorldVisualsTo()`**, sur
|
||||
le DataAsset. Deux appelants aujourd'hui — l'objet au sol (`APickupItem`) et l'objet sur le
|
||||
feu — et un troisième viendra avec l'objet en main. Dupliquer la règle garantirait qu'un des
|
||||
trois oublie le matériau.
|
||||
|
||||
### 2.4 Validation
|
||||
|
||||
`UItemDataAsset::IsDataValid()` refuse à la sauvegarde :
|
||||
|
||||
- `bCanBeCooked` coché sans `CookedResult` (et l'équivalent pour `bCanBurn`) ;
|
||||
- un résultat qui **pointe sur l'asset lui-même**.
|
||||
|
||||
Ce second cas est le piège vécu : la viande se transforme en elle-même toutes les 20 s, le
|
||||
mesh ne change pas, elle reste `bCanBeCooked` donc non récupérable, et le feu ne s'arrête
|
||||
jamais puisqu'il croit avoir du travail. En jeu, ça ressemble exactement à un timer cassé.
|
||||
|
||||
Le runtime s'en protège aussi : `GetNextStage()` ignore un résultat auto-référencé, un
|
||||
avertissement part dans l'Output Log au moment du dépôt, et l'objet reste récupérable pour
|
||||
ne pas être séquestré sur le grill.
|
||||
|
||||
---
|
||||
|
||||
## 3. Le carburant — une pile d'objets
|
||||
|
||||
Le foyer **n'est pas une jauge de secondes**, c'est un `TArray<FFuelSlotState>` : chaque
|
||||
entrée mémorise *quel objet* a été donné et *combien de temps il lui reste*.
|
||||
|
||||
```
|
||||
Fuel[0] ← celle qui brûle. Son compte à rebours est FuelTimer.
|
||||
Fuel[1..n] ← attendent leur tour, à leur durée pleine.
|
||||
```
|
||||
|
||||
Quand `Fuel[0]` est consommée, elle est retirée du tableau et tout avance d'un cran. Le foyer
|
||||
se lit donc toujours « les N premières bûches sont pleines », ce qui rend l'affichage trivial
|
||||
et sans état supplémentaire.
|
||||
|
||||
**Pourquoi ce modèle plutôt qu'un total en secondes** : une branche à 10 s et une planche à
|
||||
3 min occupent la même place. Le joueur voit ce qu'il a donné au feu, pas un pourcentage
|
||||
abstrait, et mélanger les combustibles reste lisible. Avec une jauge, il fallait dix branches
|
||||
pour voir bouger deux bûches.
|
||||
|
||||
### 3.1 La capacité
|
||||
|
||||
`GetFuelCapacity()` renvoie **le nombre de bûches taguées** dans le Blueprint. La capacité
|
||||
est une décision de mise en scène ; la régler à deux endroits garantirait qu'ils finissent
|
||||
par se contredire. `FallbackFuelCapacity` ne sert qu'aux postes sans bûche visible — un four
|
||||
fermé, par exemple.
|
||||
|
||||
### 3.2 Les bûches visibles
|
||||
|
||||
Les meshes portant le tag **`FuelLog`** (réglable via `FuelIndicatorTag`) sont recensés une
|
||||
fois au `BeginPlay` par `CacheFuelIndicators()`, puis **triés par nom**.
|
||||
|
||||
- Un tag plutôt qu'une liste de composants : le nombre de bûches change sans recompiler, et
|
||||
un renommage ne casse rien. Une `TArray<FComponentReference>` ferait de toute façon la même
|
||||
recherche par nom en interne, pour un coût identique — la seule différence serait qu'elle
|
||||
casserait en silence au moindre renommage.
|
||||
- Le tri est indispensable : `GetComponents` ne promet aucun ordre, et sans lui la bûche qui
|
||||
disparaît en premier changerait d'une compilation du Blueprint à l'autre.
|
||||
- Tri **alphabétique**, donc `Log_10` passerait avant `Log_2`. Au-delà de neuf bûches,
|
||||
nomme-les `Log_01`, `Log_02`…
|
||||
- **`Log_1` doit être au fond du tas, `Log_N` au sommet** : la dernière disparaît en premier.
|
||||
|
||||
`RefreshFuelIndicators()` n'a plus rien à calculer : une bûche visible **est** un objet
|
||||
stocké. Il n'y a ni palier, ni timer d'affichage.
|
||||
|
||||
---
|
||||
|
||||
## 4. L'acteur `ACookingStation`
|
||||
|
||||
### 4.1 Composants
|
||||
|
||||
| Composant | Rôle |
|
||||
|---|---|
|
||||
| `BaseMesh` | Racine, le foyer. Collision `QueryAndPhysics` — on n'entre pas dans un feu |
|
||||
| `InteractionSphere` | Ce que le trace d'interaction touche. Bloque uniquement `Visibility` |
|
||||
| `CookSlot_0` … `CookSlot_3` | Un par emplacement de cuisson : porte la nourriture **et** sert à la visée |
|
||||
| `FireEffect` | Niagara. `AutoActivate` désactivé — c'est le C++ qui décide |
|
||||
| `FireLight` | Halo du foyer, allumé avec la flamme |
|
||||
|
||||
Les `CookSlot_` sont créés **dans le constructeur**, en nombre fixe (`MaxCookingSlots = 4`).
|
||||
C'est le prix à payer pour pouvoir les positionner **à la souris** dans le viewport du
|
||||
Blueprint : des composants engendrés à l'exécution ne seraient réglables qu'en tapant des
|
||||
coordonnées à l'aveugle. `CookingSlotCount` limite ceux réellement utilisés.
|
||||
|
||||
**`FireLight` est `Movable`.** Le défaut d'un composant lumière est `Static`, c'est-à-dire
|
||||
cuit dans le lightmap : l'allumer au runtime n'aurait aucun effet à l'écran. Ses ombres sont
|
||||
désactivées par défaut — une lumière ponctuelle projette sur six faces de cubemap, chaque
|
||||
frame, pour chaque objet à portée. C'est de loin le poste le plus cher du système.
|
||||
|
||||
### 4.2 Réglages exposés
|
||||
|
||||
**Catégorie `Cooking`**
|
||||
|
||||
| Propriété | Défaut | Effet |
|
||||
|---|---|---|
|
||||
| `CookingSlotCount` | 2 | Emplacements de cuisson utilisés (1-4) |
|
||||
| `CookingMeshScale` | 1.0 | Échelle du mesh de nourriture posé |
|
||||
| `SlotAimHalfAngle` | 25° | Cône de visée autour de chaque emplacement |
|
||||
| `CookSpeedMultiplier` | 1.0 | Divise les durées. Un fourneau va plus vite |
|
||||
| `bAllowTakingRawItems` | ☐ | Autorise à reprendre ce qui n'a pas fini de cuire |
|
||||
| `bShowRemainingTimeInPrompt` | ☐ | Ajoute les secondes au prompt — **outil de réglage** |
|
||||
| `bHighlightOnFocus` | ☑ | Custom Depth quand le poste est visé |
|
||||
|
||||
**Catégorie `Cooking\|Fuel`**
|
||||
|
||||
| Propriété | Défaut | Effet |
|
||||
|---|---|---|
|
||||
| `FuelIndicatorTag` | `FuelLog` | Tag des meshes de bûches |
|
||||
| `FallbackFuelCapacity` | 5 | Capacité si aucune bûche n'est taguée |
|
||||
| `bFuelIndicatorsUseItemMesh` | ☐ | Chaque bûche prend le mesh de son combustible |
|
||||
| `StartingFuelItem` / `StartingFuelCount` | — | Bois déjà présent au démarrage |
|
||||
|
||||
**Catégorie `Cooking\|Sound`** — `LightSound`, `ExtinguishSound`, `PlaceItemSound`. Tous
|
||||
facultatifs, un champ vide ne joue rien.
|
||||
|
||||
### 4.3 API publique
|
||||
|
||||
```cpp
|
||||
bool Light(); // Normalement jamais appelée : UpdateFireState décide
|
||||
void Extinguish();
|
||||
bool HasPendingWork() const; // Un emplacement a-t-il encore une étape à franchir
|
||||
bool IsLit() const;
|
||||
int32 GetFuelCapacity() const;
|
||||
int32 GetFuelCount() const;
|
||||
float GetFuelRemaining() const; // Somme sur tout le foyer
|
||||
float GetSlotTimeRemaining(int32) const;
|
||||
|
||||
FOnCookingStationChanged OnStationChanged; // Dépôt, retrait, transformation, allumage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. L'interaction
|
||||
|
||||
### 5.1 Viser un emplacement
|
||||
|
||||
`ResolveAimedSlot()` prend le point de vue réel de l'interactor via
|
||||
`GetActorEyesViewPoint()` — sur un pawn possédé, c'est le point de vue du controller, donc
|
||||
exactement ce que le joueur voit. La station ne dépend ainsi pas d'`AFpsPlayer`.
|
||||
|
||||
Le choix se fait par **produit scalaire**, pas par distance à l'écran : c'est l'écart
|
||||
*angulaire* au centre du viseur qui compte, et il ne demande ni projection ni accès au
|
||||
viewport. Sous le cône `SlotAimHalfAngle`, aucun emplacement n'est visé et le joueur
|
||||
s'adresse au foyer lui-même — c'est ce qui lui permet d'ajouter du bois sans devoir éviter le
|
||||
grill du regard.
|
||||
|
||||
Aucune collision par emplacement : la transform du `CookSlot_` suffit. Zéro requête physique
|
||||
supplémentaire.
|
||||
|
||||
### 5.2 Priorité des actions
|
||||
|
||||
`ResolveAction()` calcule **une seule fois** ce que ferait un appui, et le résultat sert à la
|
||||
fois au texte du prompt et à l'exécution. Deux cascades de `if` séparées finiraient par
|
||||
diverger, et le joueur lirait une chose en en obtenant une autre.
|
||||
|
||||
| # | Condition | Action | Prompt |
|
||||
|---|---|---|---|
|
||||
| 1 | Emplacement visé, occupé, récupérable | `TakeItem` | `Pick up {objet}` |
|
||||
| 2 | Emplacement visé libre + objet cuisinable en main | `PlaceItem` | `Cook {objet}` |
|
||||
| 3 | Du bois dans l'inventaire + place au foyer | `AddFuel` | `Add {bois} to the fire` |
|
||||
| — | Sinon | `None` | *(vide)* |
|
||||
|
||||
Récupérer passe avant tout : sans ça, impossible de sortir sa viande en ayant du bois en main.
|
||||
|
||||
Le bois n'a **pas besoin d'être en main** : `FindFuelSource()` prend le slot actif s'il
|
||||
contient du carburant, sinon le premier slot de l'inventaire qui en contient. La main sert à
|
||||
*choisir* le combustible, pas à autoriser le geste — et le prompt nomme toujours ce qui va
|
||||
partir, pour qu'on ne brûle jamais ses planches par surprise.
|
||||
|
||||
**Un exemplaire par appui.** Donner du bois est irréversible ; vider vingt branches d'un coup
|
||||
sur un feu qui n'en demandait qu'une ne se rattrape pas.
|
||||
|
||||
### 5.3 Ce qui peut être repris
|
||||
|
||||
`CanTakeFromSlot()` : on ne retire pas ce qui est **en train de cuire**.
|
||||
|
||||
Le critère est porté par l'objet lui-même, sans état à maintenir : **tant qu'il sait encore
|
||||
cuire (`bCanBeCooked`), il est cru, donc engagé.** Dès qu'il ne sait plus que brûler, c'est
|
||||
qu'il a fini sa cuisson et qu'on peut le sauver.
|
||||
|
||||
C'est plus subtil qu'un booléen « a déjà transformé » : avec celui-ci, reposer une viande
|
||||
cuite sur le feu l'aurait re-verrouillée jusqu'à ce qu'elle carbonise.
|
||||
|
||||
Un second filet libère les objets dont la chaîne est cassée — sans lui, une donnée mal
|
||||
branchée séquestrerait l'objet sur le grill pour toujours.
|
||||
|
||||
`bAllowTakingRawItems` permet de revenir sur cette règle sans recompiler.
|
||||
|
||||
---
|
||||
|
||||
## 6. Le cycle du feu
|
||||
|
||||
### 6.1 `UpdateFireState()`, point de décision unique
|
||||
|
||||
```
|
||||
Le feu brûle ⟺ il y a du bois ET il y a du travail
|
||||
```
|
||||
|
||||
Toutes les actions qui changent le contenu du foyer ou du grill y passent : dépôt, retrait,
|
||||
ajout de bois, fin d'étape. Laisser chaque action décider aurait fini par oublier un cas —
|
||||
typiquement le dernier objet retiré, laissant le feu se consumer dans le vide.
|
||||
|
||||
Il n'y a donc **pas d'action « allumer »**. Le joueur met du bois, pose sa viande, ça part.
|
||||
|
||||
### 6.2 « Du travail », précisément
|
||||
|
||||
`HasPendingWork()` compte comme travail **tout objet ayant une étape devant lui, y compris la
|
||||
carbonisation**.
|
||||
|
||||
C'est un arbitrage délibéré : pris au pied de la lettre, « le feu s'arrête quand tout est
|
||||
fini de cuire » l'éteindrait à l'instant précis où la viande devient cuite, et la
|
||||
sur-cuisson ne se produirait jamais. Oublier sa viande doit rester puni. Le feu ne s'arrête
|
||||
donc qu'une fois la viande brûlée — bout de chaîne.
|
||||
|
||||
*Si ce comportement doit changer un jour, c'est la seule condition de `HasPendingWork()`.*
|
||||
|
||||
### 6.3 Les timers
|
||||
|
||||
| Timer | Porté par | Ce qu'il fait |
|
||||
|---|---|---|
|
||||
| `FCookingSlotState::Timer` | Chaque emplacement | Fin d'étape → `HandleSlotFinished` |
|
||||
| `FuelTimer` | La station | Fin de `Fuel[0]` → `HandleFuelConsumed` |
|
||||
|
||||
**Aucun `Tick`, et aucun timer périodique.** Éteindre le feu met les timers de cuisson en
|
||||
**pause** : la cuisson reprend exactement où elle s'était arrêtée, sans qu'une seule ligne ne
|
||||
compte le temps à la main.
|
||||
|
||||
Poser un objet sur un feu éteint est autorisé : le compte à rebours existe, il est
|
||||
immédiatement mis en pause et attend qu'on alimente le foyer.
|
||||
|
||||
Le carburant, lui, ne se met pas en pause mais se **resynchronise** : `SyncFuelFromTimer()`
|
||||
recopie le restant du timer dans `Fuel[0]` avant toute modification. Feu allumé, c'est le
|
||||
timer qui fait autorité ; feu éteint, c'est `RemainingSeconds`.
|
||||
|
||||
`EndPlay` nettoie tous les timers. Ils portent un délégué vers `this`, et les laisser tourner
|
||||
après la destruction de l'acteur ne se voit qu'au changement de niveau, sous la forme d'un
|
||||
crash sans pile lisible.
|
||||
|
||||
### 6.4 Débordement
|
||||
|
||||
`TakeItemToInventory()` tente l'ajout **avant** de vider l'emplacement. Inventaire plein : la
|
||||
nourriture reste sur le feu — et continue donc de cuire, ce qui est la bonne sanction plutôt
|
||||
que de la faire disparaître.
|
||||
|
||||
---
|
||||
|
||||
## 7. Guide éditeur
|
||||
|
||||
### 7.1 La chaîne d'objets
|
||||
|
||||
Crée-les **du dernier vers le premier**, chacun référençant le suivant.
|
||||
|
||||
1. Content Browser → `Content/Game/Data/Item` → clic droit sur un DataAsset existant →
|
||||
**Duplicate** (il hérite du mesh et de l'icône, c'est le plus rapide).
|
||||
2. **`DA_BurntMeat`** : `Item Id` = `Meat_Burnt`, `Display Name` = `Burnt Meat`,
|
||||
`Hunger Restore` = 2, `Health Restore` = -5. **Toute la section Cooking décochée** — c'est
|
||||
ce qui arrête la chaîne.
|
||||
3. **`DA_CookedMeat`** : `Hunger Restore` = 40, `Health Restore` = 5. `Can Be Cooked`
|
||||
**décoché**, `Can Burn` **coché**, `Burn Duration` = 45, `Burnt Result` → `DA_BurntMeat`.
|
||||
4. **`DA_RawMeat`** : `Hunger Restore` = 8, `Health Restore` = **-10** (manger cru punit).
|
||||
`Can Be Cooked` **coché**, `Cook Duration` = 20, `Cooked Result` → `DA_CookedMeat`.
|
||||
5. Donne à chaque étape un visuel distinct : soit un `SM_Food_XX` différent, soit **le même
|
||||
mesh** avec un `World Mesh Material` différent (`MI_StylizedFood_01` / `02` / `04`).
|
||||
6. Le combustible : sur `DA_Branch`, coche **`Is Fuel`** et règle `Fuel Duration`.
|
||||
|
||||
### 7.2 Le Blueprint
|
||||
|
||||
1. Clic droit → **Blueprint Class** → **All Classes** → `CookingStation` → nomme-le
|
||||
`BP_Campfire`.
|
||||
2. `BaseMesh` → le mesh du foyer. Ajoute des Static Mesh enfants pour composer les pierres.
|
||||
3. `FireEffect` → **Niagara System Asset** = `NS_Stylized_Fire_01_Infinite`. Prends bien la
|
||||
variante `_Infinite`, les autres s'arrêtent seules. **Laisse `Auto Activate` décoché.**
|
||||
4. `FireLight` → ajuste intensité, couleur, rayon. Coche `Cast Shadows` **uniquement** sur le
|
||||
feu principal du campement.
|
||||
5. **Les bûches** : ajoute N Static Mesh (`Log_1` … `Log_5`), positionne-les en tas, puis sur
|
||||
chacun → Details → **`Component Tags`** → `+` → **`FuelLog`**.
|
||||
`Log_1` au fond, `Log_N` au sommet.
|
||||
6. **Les emplacements de cuisson** : sélectionne `CookSlot_0`, donne-lui un `Cube` de repérage
|
||||
(Scale 0.15) pour le voir, place-le au-dessus des flammes. Le jeu efface ce mesh au
|
||||
démarrage. Répète selon `CookingSlotCount`.
|
||||
7. Racine → `Cooking` : ajuste `Cooking Slot Count` et `Cooking Mesh Scale`.
|
||||
|
||||
### 7.3 Vérifier que ça marche
|
||||
|
||||
1. Le foyer démarre **vide** : pas de bûche visible, pas de flamme. C'est normal.
|
||||
2. Branche en main, vise le foyer → `[E] Add Branch to the fire` → **une bûche apparaît**.
|
||||
3. Répète : le tas se remplit, puis l'action disparaît une fois la capacité atteinte.
|
||||
4. Viande crue en main, vise le grill → `[E] Cook Raw Meat` → **les flammes partent seules**.
|
||||
5. Pendant la cuisson : **aucun prompt**, impossible de reprendre la viande.
|
||||
6. À la fin : le mesh change et le prompt devient `[E] Pick up Cooked Meat`.
|
||||
7. Laisse traîner : le mesh repasse au brûlé, et le feu s'arrête de lui-même.
|
||||
8. Retire tout le bois en cours de cuisson : les flammes s'éteignent et la cuisson **se fige**.
|
||||
Remets du bois : elle reprend où elle en était, pas de zéro.
|
||||
|
||||
---
|
||||
|
||||
## 8. Dépannage
|
||||
|
||||
| Symptôme | Cause |
|
||||
|---|---|
|
||||
| `Cook X` n'apparaît jamais | Les `CookSlot_` sont restés à l'origine, donc **dans** le foyer. Repositionne-les, ou monte `Slot Aim Half Angle` |
|
||||
| L'objet ne change jamais d'état, jamais récupérable | Chaîne auto-référencée : `Cooked Result` pointe sur l'asset lui-même. **L'Output Log le dit** |
|
||||
| Rien ne se passe quand on pose | Vérifie que `bCanBeCooked` **et** `CookedResult` sont renseignés |
|
||||
| Aucune bûche n'apparaît | Tag `FuelLog` absent, ou mal orthographié |
|
||||
| Les bûches disparaissent dans le désordre | Ordre alphabétique des noms de composants |
|
||||
| Le feu ne démarre pas malgré du bois | Rien à transformer : c'est le comportement voulu |
|
||||
| Pas de flamme mais le feu tourne | `Niagara System Asset` vide, ou variante non-`_Infinite` |
|
||||
| La lumière ne s'allume pas | `Mobility` du `FireLight` repassée à `Static` |
|
||||
| Le mesh de repérage reste visible | Il est effacé au `BeginPlay` — tu regardes le viewport de l'éditeur |
|
||||
| Nourriture énorme ou minuscule | `Cooking Mesh Scale` |
|
||||
|
||||
---
|
||||
|
||||
## 9. Étendre
|
||||
|
||||
**Un fourneau** est déjà possible sans une ligne de C++ : même classe, `CookSpeedMultiplier`
|
||||
à 2, d'autres meshes, plus de `CookSlot_`. Un **séchoir** de même, avec des durées longues et
|
||||
`bCanBurn` décoché sur ses résultats.
|
||||
|
||||
Ce qui demanderait du code :
|
||||
|
||||
- **Une file d'attente sans le joueur** (déposer dix minerais, revenir plus tard) : le modèle
|
||||
actuel est un emplacement = un exemplaire.
|
||||
- **Un allume-feu obligatoire** (silex, briquet) : `Light()` est déjà publique, il ne manque
|
||||
qu'une condition et une action d'interaction.
|
||||
- **La sauvegarde** : `Slots` et `Fuel` sont `Transient`. Le jour où la sauvegarde arrivera,
|
||||
ce sont ces deux tableaux plus les temps restants qu'il faudra sérialiser.
|
||||
- **Séparer « le feu éclaire » de « le feu consomme »** : aujourd'hui les deux sont liés, donc
|
||||
un feu de camp n'éclaire pas quand il n'a rien à cuire. Invisible tant qu'il n'y a pas de
|
||||
cycle jour/nuit, gênant le jour où il y en aura un. Une dizaine de lignes.
|
||||
|
||||
---
|
||||
|
||||
## 10. Ce que ce système a changé ailleurs
|
||||
|
||||
- **`IInteractable::GetInteractionPrompt` prend désormais l'`Interactor`.** Sans lui, la
|
||||
station ne peut pas savoir quel emplacement le joueur regarde. Resservira pour les coffres.
|
||||
- **`UInteractionComponent` met le prompt en cache et diffuse `OnPromptChanged`.** Le texte
|
||||
peut changer sans que la cible change — en balayant d'un emplacement à l'autre sur le même
|
||||
acteur. S'abonner à `OnFocusChanged` seul laissait ces cas figés à l'écran. Le composant ne
|
||||
reconstruit le texte qu'une fois par trace, et ne réveille l'UI que sur un vrai changement.
|
||||
- **`UItemDataAsset::ApplyWorldVisualsTo()`** et le champ `WorldMeshMaterial` (§2.3).
|
||||
- Les `FText` d'`APickupItem` sont passés en anglais, conformément à la convention du projet.
|
||||
@@ -0,0 +1,398 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user