Improve inventory UX: localized names, smart sorting, icons, box details, lore progress

- Resolve box names via BoxDefinition.NameKey instead of showing raw IDs
- Reorder inventory categories: Box → Consumable → Lore → Cosmetic → Material → Meta
- Replace English category/rarity text with emoji icons and localized rarity labels
- Show box description in detail panel when selecting a box item
- Add lore collection progress counter (N/10) in lore fragment detail panel
- Add FR rarity translations (Commun, Peu commun, Rare, Épique, Légendaire, Mythique)
This commit is contained in:
Samuel Bouchet 2026-03-13 23:36:50 +01:00
parent 930128a766
commit 71a35cd19d
6 changed files with 225 additions and 26 deletions

View file

@ -478,5 +478,13 @@
"inventory.effect": "Effect", "inventory.effect": "Effect",
"inventory.press_enter_use": "Press Enter to use", "inventory.press_enter_use": "Press Enter to use",
"inventory.item_used": "{0} used!", "inventory.item_used": "{0} used!",
"inventory.cosmetic_slot": "Slot" "inventory.cosmetic_slot": "Slot",
"inventory.lore_progress": "Collected",
"rarity.common": "Common",
"rarity.uncommon": "Uncommon",
"rarity.rare": "Rare",
"rarity.epic": "Epic",
"rarity.legendary": "Legendary",
"rarity.mythic": "Mythic"
} }

View file

@ -478,5 +478,13 @@
"inventory.effect": "Effet", "inventory.effect": "Effet",
"inventory.press_enter_use": "Appuyer sur Entrée pour utiliser", "inventory.press_enter_use": "Appuyer sur Entrée pour utiliser",
"inventory.item_used": "{0} utilisé !", "inventory.item_used": "{0} utilisé !",
"inventory.cosmetic_slot": "Emplacement" "inventory.cosmetic_slot": "Emplacement",
"inventory.lore_progress": "Collectés",
"rarity.common": "Commun",
"rarity.uncommon": "Peu commun",
"rarity.rare": "Rare",
"rarity.epic": "Épique",
"rarity.legendary": "Légendaire",
"rarity.mythic": "Mythique"
} }

View file

@ -637,7 +637,7 @@ public static class Program
// Detail panel for selected item // Detail panel for selected item
var selectedGroup = grouped[selectedIndex]; var selectedGroup = grouped[selectedIndex];
var detailPanel = InventoryPanel.RenderDetailPanel(selectedGroup, _registry, _loc); var detailPanel = InventoryPanel.RenderDetailPanel(selectedGroup, _registry, _loc, _state);
if (detailPanel is not null) if (detailPanel is not null)
AnsiConsole.Write(detailPanel); AnsiConsole.Write(detailPanel);

View file

@ -31,6 +31,25 @@ public static class InventoryPanel
/// <summary>Compact row count for inline layout mode (fits in ~24-line terminals).</summary> /// <summary>Compact row count for inline layout mode (fits in ~24-line terminals).</summary>
public const int CompactVisibleRows = 6; public const int CompactVisibleRows = 6;
/// <summary>
/// Custom sort order for categories — interactive items first.
/// </summary>
private static int CategorySortOrder(ItemCategory cat) => cat switch
{
ItemCategory.Box => 0,
ItemCategory.Consumable => 1,
ItemCategory.LoreFragment => 2,
ItemCategory.AdventureToken => 3,
ItemCategory.Key => 4,
ItemCategory.Cosmetic => 5,
ItemCategory.Material => 6,
ItemCategory.CraftedItem => 7,
ItemCategory.Meta => 8,
ItemCategory.Cookie => 9,
ItemCategory.Music => 10,
_ => 11
};
/// <summary> /// <summary>
/// Returns the total number of distinct item groups in the inventory. /// Returns the total number of distinct item groups in the inventory.
/// </summary> /// </summary>
@ -38,7 +57,7 @@ public static class InventoryPanel
state.Inventory.GroupBy(i => i.DefinitionId).Count(); state.Inventory.GroupBy(i => i.DefinitionId).Count();
/// <summary> /// <summary>
/// Returns grouped inventory items sorted by category then definition id. /// Returns grouped inventory items sorted by category (interactive first) then definition id.
/// </summary> /// </summary>
public static List<InventoryGroup> GetGroupedItems(GameState state, ContentRegistry? registry = null) public static List<InventoryGroup> GetGroupedItems(GameState state, ContentRegistry? registry = null)
{ {
@ -55,11 +74,73 @@ public static class InventoryPanel
def?.Rarity ?? ItemRarity.Common, def?.Rarity ?? ItemRarity.Common,
g.First()); g.First());
}) })
.OrderBy(x => x.Category) .OrderBy(x => CategorySortOrder(x.Category))
.ThenBy(x => x.DefId) .ThenBy(x => x.DefId)
.ToList(); .ToList();
} }
/// <summary>
/// Resolves a localized name for an inventory item, falling back to box name or definition id.
/// </summary>
private static string ResolveName(InventoryGroup item, ContentRegistry? registry, LocalizationManager? loc)
{
// Item definition has a name key
if (item.Def is not null && loc is not null)
return loc.Get(item.Def.NameKey);
// No item def — might be a box
if (registry is not null && loc is not null)
{
var boxDef = registry.GetBox(item.DefId);
if (boxDef is not null)
return loc.Get(boxDef.NameKey);
}
return item.DefId;
}
/// <summary>
/// Returns a localized category label.
/// </summary>
private static string LocalizeCategory(ItemCategory cat, LocalizationManager? loc)
{
if (loc is null) return CategoryIcon(cat);
return CategoryIcon(cat);
}
/// <summary>
/// Returns a compact emoji icon for a category.
/// </summary>
private static string CategoryIcon(ItemCategory cat) => cat switch
{
ItemCategory.Box => "📦",
ItemCategory.Key => "🔑",
ItemCategory.Consumable => "🧪",
ItemCategory.LoreFragment => "📜",
ItemCategory.Cosmetic => "👗",
ItemCategory.Material => "🔩",
ItemCategory.Meta => "⚙",
ItemCategory.AdventureToken => "🗺",
ItemCategory.Cookie => "🍪",
ItemCategory.Music => "🎵",
ItemCategory.CraftedItem => "🔨",
ItemCategory.WorkstationBlueprint => "🏗",
ItemCategory.Badge => "🏅",
ItemCategory.Map => "🗺",
ItemCategory.StoryItem => "📖",
ItemCategory.QuestItem => "❓",
_ => "•"
};
/// <summary>
/// Returns a localized rarity label.
/// </summary>
private static string LocalizeRarity(ItemRarity rarity, LocalizationManager? loc)
{
if (loc is null) return rarity.ToString();
return loc.Get($"rarity.{rarity.ToString().ToLower()}");
}
/// <summary> /// <summary>
/// Builds a renderable inventory table. /// Builds a renderable inventory table.
/// <paramref name="compact"/> uses fewer visible rows for inline layout mode. /// <paramref name="compact"/> uses fewer visible rows for inline layout mode.
@ -78,7 +159,7 @@ public static class InventoryPanel
var table = new Table() var table = new Table()
.Border(TableBorder.Rounded) .Border(TableBorder.Rounded)
.AddColumn(new TableColumn("[bold]Name[/]").Width(MaxNameWidth)) .AddColumn(new TableColumn("[bold]Name[/]").Width(MaxNameWidth))
.AddColumn(new TableColumn("[bold]Cat.[/]").Centered()) .AddColumn(new TableColumn("").Centered().Width(2))
.AddColumn(new TableColumn("[bold]Rarity[/]").Centered()) .AddColumn(new TableColumn("[bold]Rarity[/]").Centered())
.AddColumn(new TableColumn("[bold]Qty[/]").RightAligned()); .AddColumn(new TableColumn("[bold]Qty[/]").RightAligned());
@ -95,14 +176,12 @@ public static class InventoryPanel
bool isSelected = globalIndex == selectedIndex; bool isSelected = globalIndex == selectedIndex;
// Resolve localized name, truncate if needed // Resolve localized name, truncate if needed
string name = item.Def is not null && loc is not null string name = ResolveName(item, registry, loc);
? loc.Get(item.Def.NameKey)
: item.DefId;
if (name.Length > MaxNameWidth) if (name.Length > MaxNameWidth)
name = name[..(MaxNameWidth - 1)] + "…"; name = name[..(MaxNameWidth - 1)] + "…";
string category = item.Category.ToString(); string catIcon = LocalizeCategory(item.Category, loc);
string rarity = item.Rarity.ToString(); string rarity = LocalizeRarity(item.Rarity, loc);
string color = RarityColor(item.Rarity); string color = RarityColor(item.Rarity);
if (isSelected) if (isSelected)
@ -110,7 +189,7 @@ public static class InventoryPanel
// Highlighted row: reverse video style with arrow indicator // Highlighted row: reverse video style with arrow indicator
table.AddRow( table.AddRow(
$"[bold {color} on grey23]► {Markup.Escape(name)}[/]", $"[bold {color} on grey23]► {Markup.Escape(name)}[/]",
$"[bold on grey23]{Markup.Escape(category)}[/]", $"[bold on grey23]{catIcon}[/]",
$"[bold {color} on grey23]{Markup.Escape(rarity)}[/]", $"[bold {color} on grey23]{Markup.Escape(rarity)}[/]",
$"[bold on grey23]{item.TotalQty}[/]"); $"[bold on grey23]{item.TotalQty}[/]");
} }
@ -118,7 +197,7 @@ public static class InventoryPanel
{ {
table.AddRow( table.AddRow(
$"[{color}] {Markup.Escape(name)}[/]", $"[{color}] {Markup.Escape(name)}[/]",
$"[dim]{Markup.Escape(category)}[/]", $"[dim]{catIcon}[/]",
$"[{color}]{Markup.Escape(rarity)}[/]", $"[{color}]{Markup.Escape(rarity)}[/]",
item.TotalQty.ToString()); item.TotalQty.ToString());
} }
@ -156,20 +235,22 @@ public static class InventoryPanel
public static IRenderable? RenderDetailPanel( public static IRenderable? RenderDetailPanel(
InventoryGroup? item, InventoryGroup? item,
ContentRegistry? registry = null, ContentRegistry? registry = null,
LocalizationManager? loc = null) LocalizationManager? loc = null,
GameState? state = null)
{ {
if (item?.Def is null) if (item is null)
return null; return null;
var rows = new List<IRenderable>(); var rows = new List<IRenderable>();
string color = RarityColor(item.Rarity); string color = RarityColor(item.Rarity);
// Item name // Item name — resolve via item def or box def
string name = loc is not null ? loc.Get(item.Def.NameKey) : item.DefId; string name = ResolveName(item, registry, loc);
rows.Add(new Markup($"[bold {color}]{Markup.Escape(name)}[/] [dim]({Markup.Escape(item.Rarity.ToString())})[/]")); string rarityLabel = LocalizeRarity(item.Rarity, loc);
rows.Add(new Markup($"[bold {color}]{Markup.Escape(name)}[/] [dim]({Markup.Escape(rarityLabel)})[/]"));
// Description if available // Description if available (item def)
if (item.Def.DescriptionKey is not null && loc is not null) if (item.Def?.DescriptionKey is not null && loc is not null)
{ {
string desc = loc.Get(item.Def.DescriptionKey); string desc = loc.Get(item.Def.DescriptionKey);
rows.Add(new Markup($"[italic]{Markup.Escape(desc)}[/]")); rows.Add(new Markup($"[italic]{Markup.Escape(desc)}[/]"));
@ -178,7 +259,20 @@ public static class InventoryPanel
// Category-specific details // Category-specific details
switch (item.Category) switch (item.Category)
{ {
case ItemCategory.Consumable when item.Def.ResourceType.HasValue && item.Def.ResourceAmount.HasValue: case ItemCategory.Box:
// Show box description if available
if (registry is not null)
{
var boxDef = registry.GetBox(item.DefId);
if (boxDef is not null && loc is not null)
{
string boxDesc = loc.Get(boxDef.DescriptionKey);
rows.Add(new Markup($"[italic dim]{Markup.Escape(boxDesc)}[/]"));
}
}
break;
case ItemCategory.Consumable when item.Def?.ResourceType is not null && item.Def.ResourceAmount is not null:
string resName = loc?.Get($"resource.{item.Def.ResourceType.Value.ToString().ToLower()}") ?? item.Def.ResourceType.Value.ToString(); string resName = loc?.Get($"resource.{item.Def.ResourceType.Value.ToString().ToLower()}") ?? item.Def.ResourceType.Value.ToString();
string sign = item.Def.ResourceAmount.Value >= 0 ? "+" : ""; string sign = item.Def.ResourceAmount.Value >= 0 ? "+" : "";
rows.Add(new Markup($"[green]{Markup.Escape(loc?.Get("inventory.effect") ?? "Effect")}:[/] {sign}{item.Def.ResourceAmount.Value} {Markup.Escape(resName)}")); rows.Add(new Markup($"[green]{Markup.Escape(loc?.Get("inventory.effect") ?? "Effect")}:[/] {sign}{item.Def.ResourceAmount.Value} {Markup.Escape(resName)}"));
@ -194,14 +288,25 @@ public static class InventoryPanel
rows.Add(new Markup("")); rows.Add(new Markup(""));
rows.Add(new Markup($"[italic dim]{Markup.Escape(loreText)}[/]")); rows.Add(new Markup($"[italic dim]{Markup.Escape(loreText)}[/]"));
} }
// Show collection progress
if (state is not null)
{
int collected = state.Inventory
.Select(i => i.DefinitionId)
.Where(id => id.StartsWith("lore_"))
.Distinct()
.Count();
string progressLabel = loc?.Get("inventory.lore_progress") ?? "Collected";
rows.Add(new Markup($"[dim]{Markup.Escape(progressLabel)}: {collected}/10[/]"));
}
break; break;
case ItemCategory.Cosmetic when item.Def.CosmeticSlot.HasValue: case ItemCategory.Cosmetic when item.Def?.CosmeticSlot is not null:
string slotName = loc?.Get($"cosmetic.slot.{item.Def.CosmeticSlot.Value.ToString().ToLower()}") ?? item.Def.CosmeticSlot.Value.ToString(); string slotName = loc?.Get($"cosmetic.slot.{item.Def.CosmeticSlot.Value.ToString().ToLower()}") ?? item.Def.CosmeticSlot.Value.ToString();
rows.Add(new Markup($"[dim]{Markup.Escape(loc?.Get("inventory.cosmetic_slot") ?? "Slot")}: {Markup.Escape(slotName)}[/]")); rows.Add(new Markup($"[dim]{Markup.Escape(loc?.Get("inventory.cosmetic_slot") ?? "Slot")}: {Markup.Escape(slotName)}[/]"));
break; break;
case ItemCategory.Material when item.Def.MaterialType.HasValue: case ItemCategory.Material when item.Def?.MaterialType is not null:
rows.Add(new Markup($"[dim]{Markup.Escape(item.Def.MaterialType.Value.ToString())} ({Markup.Escape(item.Def.MaterialForm?.ToString() ?? "Raw")})[/]")); rows.Add(new Markup($"[dim]{Markup.Escape(item.Def.MaterialType.Value.ToString())} ({Markup.Escape(item.Def.MaterialForm?.ToString() ?? "Raw")})[/]"));
break; break;
} }

78
suggestions_2.md Normal file
View file

@ -0,0 +1,78 @@
# Suggestions d'amélioration #2
Basées sur l'analyse des rendus InventoryRenderCapture et PlaythroughCapture.
| # | Suggestion | Priorité | Statut |
|---|-----------|----------|--------|
| 1 | Noms localisés pour les boîtes dans l'inventaire | ★★★★★ | ✅ DONE |
| 2 | Tri de l'inventaire : consommables et lore avant matériaux | ★★★★★ | ✅ DONE |
| 3 | Colonnes Category/Rarity localisées | ★★★★☆ | ✅ DONE |
| 4 | Panneau de détail pour les boîtes (contenu possible) | ★★★★☆ | ✅ DONE |
| 5 | Compteur de lore collectés (N/10) dans le panneau détails | ★★★★☆ | ✅ DONE |
| 6 | Icônes/emojis par catégorie dans l'inventaire | ★★★☆☆ |
| 7 | Résumé de l'inventaire dans le panneau compact (nombre par catégorie) | ★★★☆☆ |
| 8 | Panneau de détail pour les items Meta (explication du feature) | ★★★☆☆ |
| 9 | Message de bienvenue adaptatif au nombre de boîtes ouvertes | ★★☆☆☆ |
| 10 | Indicateur visuel de scroll restant (↑↓ en haut/bas du tableau) | ★★☆☆☆ |
---
## 1. Noms localisés pour les boîtes dans l'inventaire — ★★★★★
**Constat** : Les boîtes apparaissent avec leur ID technique (`box_not_great`, `box_legendhair`, `box_meta_basics`) au lieu de leur nom localisé. C'est le défaut visuel le plus flagrant dans l'inventaire.
**Solution** : Dans `InventoryPanel.GetGroupedItems()` ou `Render()`, quand l'item n'a pas de `ItemDefinition` (c'est une boîte), résoudre le nom via le `BoxDefinition.NameKey` du registre.
## 2. Tri de l'inventaire : consommables et lore avant matériaux — ★★★★★
**Constat** : L'ordre actuel (Box, Cosmetic, Material, Consumable, Meta, LoreFragment…) met les items interactifs (consommables, lore) loin dans la liste, après des dizaines de cosmétiques et matériaux non-actionnables.
**Solution** : Modifier l'ordre de tri dans `GetGroupedItems()` pour prioriser : Box → Consumable → LoreFragment → Cosmetic → Material → Meta → reste.
## 3. Colonnes Category/Rarity localisées — ★★★★☆
**Constat** : Les colonnes "Cat." et "Rarity" affichent les valeurs enum en anglais (`Consumable`, `Uncommon`, `Material`) même en locale FR. Ça casse l'immersion.
**Solution** : Ajouter des clés de localisation `category.box`, `category.consumable`, `rarity.common`, `rarity.uncommon`, etc. et les utiliser dans `Render()`.
## 4. Panneau de détail pour les boîtes (contenu possible) — ★★★★☆
**Constat** : Sélectionner une boîte dans l'inventaire ne montre aucun détail utile. Le joueur ne sait pas ce qu'elle peut contenir.
**Solution** : Ajouter un cas `ItemCategory.Box` dans `RenderDetailPanel()` qui affiche le nom localisé de la boîte et un indice sur son contenu (rareté, thème).
## 5. Compteur de lore collectés (N/10) dans le panneau détails — ★★★★☆
**Constat** : Quand on sélectionne un fragment de lore, on ne sait pas combien on en a collecté sur le total (10). C'est une mécanique de collection qui bénéficierait d'un indicateur de progression.
**Solution** : Compter les `lore_*` distincts dans l'inventaire et afficher "Fragments : 3/10" dans le panneau de détail des lore.
## 6. Icônes/emojis par catégorie dans l'inventaire — ★★★☆☆
**Constat** : La colonne catégorie est textuelle et prend de la place. Des icônes compactes seraient plus lisibles.
**Solution** : Remplacer le texte de catégorie par des emojis (📦 Box, 🧪 Consumable, 📜 Lore, 👗 Cosmetic, 🔩 Material, ⚙ Meta).
## 7. Résumé de l'inventaire dans le panneau compact — ★★★☆☆
**Constat** : En mode compact (FullLayout), seuls 6 items sont visibles, sans indication du nombre total par catégorie.
**Solution** : Ajouter une ligne de pied de tableau montrant un résumé : "📦3 🧪5 📜2 👗12 🔩8".
## 8. Panneau de détail pour les items Meta — ★★★☆☆
**Constat** : Les items Meta (ex: "Couleurs de texte") n'ont aucune description dans le panneau de détails.
**Solution** : Ajouter des `descriptionKey` pour les items meta dans items.json et les afficher dans le panneau détails.
## 9. Message de bienvenue adaptatif — ★★☆☆☆
**Constat** : Le message de bienvenue est identique que le joueur ait 0 ou 500 boîtes ouvertes. C'est une occasion manquée de donner du feedback.
**Solution** : Adapter le message d'accueil selon la progression.
## 10. Indicateur visuel de scroll dans l'inventaire — ★★☆☆☆
**Constat** : Quand l'inventaire est scrollable, le joueur ne voit que "(1-15/55)" dans le titre. Il n'y a pas d'indication visuelle dans le tableau lui-même.
**Solution** : Ajouter des symboles ▲/▼ en première/dernière ligne quand il y a du contenu au-dessus/en-dessous.

View file

@ -1632,7 +1632,7 @@ public class ContentValidationTests
if (grouped.Count > 0) if (grouped.Count > 0)
{ {
writer.GetStringBuilder().Clear(); writer.GetStringBuilder().Clear();
var detail = InventoryPanel.RenderDetailPanel(grouped[0], registry, loc); var detail = InventoryPanel.RenderDetailPanel(grouped[0], registry, loc, state);
if (detail is not null) if (detail is not null)
{ {
console.Write(detail); console.Write(detail);
@ -1642,7 +1642,7 @@ public class ContentValidationTests
} }
// Show detail panel for specific item types if present // Show detail panel for specific item types if present
var interestingTypes = new[] { ItemCategory.Consumable, ItemCategory.LoreFragment, ItemCategory.Cosmetic, ItemCategory.Material }; var interestingTypes = new[] { ItemCategory.Box, ItemCategory.Consumable, ItemCategory.LoreFragment, ItemCategory.Cosmetic, ItemCategory.Material };
foreach (var cat in interestingTypes) foreach (var cat in interestingTypes)
{ {
var sample = grouped.FirstOrDefault(g => g.Category == cat); var sample = grouped.FirstOrDefault(g => g.Category == cat);
@ -1651,7 +1651,7 @@ public class ContentValidationTests
int idx = grouped.IndexOf(sample); int idx = grouped.IndexOf(sample);
report.AppendLine($"│ ── Detail for [{cat}]: {sample.DefId} ──"); report.AppendLine($"│ ── Detail for [{cat}]: {sample.DefId} ──");
writer.GetStringBuilder().Clear(); writer.GetStringBuilder().Clear();
var detailAlt = InventoryPanel.RenderDetailPanel(sample, registry, loc); var detailAlt = InventoryPanel.RenderDetailPanel(sample, registry, loc, state);
if (detailAlt is not null) if (detailAlt is not null)
{ {
console.Write(detailAlt); console.Write(detailAlt);