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.press_enter_use": "Press Enter to use",
"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.press_enter_use": "Appuyer sur Entrée pour utiliser",
"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
var selectedGroup = grouped[selectedIndex];
var detailPanel = InventoryPanel.RenderDetailPanel(selectedGroup, _registry, _loc);
var detailPanel = InventoryPanel.RenderDetailPanel(selectedGroup, _registry, _loc, _state);
if (detailPanel is not null)
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>
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>
/// Returns the total number of distinct item groups in the inventory.
/// </summary>
@ -38,7 +57,7 @@ public static class InventoryPanel
state.Inventory.GroupBy(i => i.DefinitionId).Count();
/// <summary>
/// Returns grouped inventory items sorted by category then definition id.
/// Returns grouped inventory items sorted by category (interactive first) then definition id.
/// </summary>
public static List<InventoryGroup> GetGroupedItems(GameState state, ContentRegistry? registry = null)
{
@ -55,11 +74,73 @@ public static class InventoryPanel
def?.Rarity ?? ItemRarity.Common,
g.First());
})
.OrderBy(x => x.Category)
.OrderBy(x => CategorySortOrder(x.Category))
.ThenBy(x => x.DefId)
.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>
/// Builds a renderable inventory table.
/// <paramref name="compact"/> uses fewer visible rows for inline layout mode.
@ -78,7 +159,7 @@ public static class InventoryPanel
var table = new Table()
.Border(TableBorder.Rounded)
.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]Qty[/]").RightAligned());
@ -95,14 +176,12 @@ public static class InventoryPanel
bool isSelected = globalIndex == selectedIndex;
// Resolve localized name, truncate if needed
string name = item.Def is not null && loc is not null
? loc.Get(item.Def.NameKey)
: item.DefId;
string name = ResolveName(item, registry, loc);
if (name.Length > MaxNameWidth)
name = name[..(MaxNameWidth - 1)] + "…";
string category = item.Category.ToString();
string rarity = item.Rarity.ToString();
string catIcon = LocalizeCategory(item.Category, loc);
string rarity = LocalizeRarity(item.Rarity, loc);
string color = RarityColor(item.Rarity);
if (isSelected)
@ -110,7 +189,7 @@ public static class InventoryPanel
// Highlighted row: reverse video style with arrow indicator
table.AddRow(
$"[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 on grey23]{item.TotalQty}[/]");
}
@ -118,7 +197,7 @@ public static class InventoryPanel
{
table.AddRow(
$"[{color}] {Markup.Escape(name)}[/]",
$"[dim]{Markup.Escape(category)}[/]",
$"[dim]{catIcon}[/]",
$"[{color}]{Markup.Escape(rarity)}[/]",
item.TotalQty.ToString());
}
@ -156,20 +235,22 @@ public static class InventoryPanel
public static IRenderable? RenderDetailPanel(
InventoryGroup? item,
ContentRegistry? registry = null,
LocalizationManager? loc = null)
LocalizationManager? loc = null,
GameState? state = null)
{
if (item?.Def is null)
if (item is null)
return null;
var rows = new List<IRenderable>();
string color = RarityColor(item.Rarity);
// Item name
string name = loc is not null ? loc.Get(item.Def.NameKey) : item.DefId;
rows.Add(new Markup($"[bold {color}]{Markup.Escape(name)}[/] [dim]({Markup.Escape(item.Rarity.ToString())})[/]"));
// Item name — resolve via item def or box def
string name = ResolveName(item, registry, loc);
string rarityLabel = LocalizeRarity(item.Rarity, loc);
rows.Add(new Markup($"[bold {color}]{Markup.Escape(name)}[/] [dim]({Markup.Escape(rarityLabel)})[/]"));
// Description if available
if (item.Def.DescriptionKey is not null && loc is not null)
// Description if available (item def)
if (item.Def?.DescriptionKey is not null && loc is not null)
{
string desc = loc.Get(item.Def.DescriptionKey);
rows.Add(new Markup($"[italic]{Markup.Escape(desc)}[/]"));
@ -178,7 +259,20 @@ public static class InventoryPanel
// Category-specific details
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 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)}"));
@ -194,14 +288,25 @@ public static class InventoryPanel
rows.Add(new Markup(""));
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;
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();
rows.Add(new Markup($"[dim]{Markup.Escape(loc?.Get("inventory.cosmetic_slot") ?? "Slot")}: {Markup.Escape(slotName)}[/]"));
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")})[/]"));
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)
{
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)
{
console.Write(detail);
@ -1642,7 +1642,7 @@ public class ContentValidationTests
}
// 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)
{
var sample = grouped.FirstOrDefault(g => g.Category == cat);
@ -1651,7 +1651,7 @@ public class ContentValidationTests
int idx = grouped.IndexOf(sample);
report.AppendLine($"│ ── Detail for [{cat}]: {sample.DefId} ──");
writer.GetStringBuilder().Clear();
var detailAlt = InventoryPanel.RenderDetailPanel(sample, registry, loc);
var detailAlt = InventoryPanel.RenderDetailPanel(sample, registry, loc, state);
if (detailAlt is not null)
{
console.Write(detailAlt);