diff --git a/content/strings/en.json b/content/strings/en.json
index a0aa318..3414900 100644
--- a/content/strings/en.json
+++ b/content/strings/en.json
@@ -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"
}
diff --git a/content/strings/fr.json b/content/strings/fr.json
index 934da90..56901b8 100644
--- a/content/strings/fr.json
+++ b/content/strings/fr.json
@@ -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"
}
diff --git a/src/OpenTheBox/Program.cs b/src/OpenTheBox/Program.cs
index afa037c..7a88d70 100644
--- a/src/OpenTheBox/Program.cs
+++ b/src/OpenTheBox/Program.cs
@@ -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);
diff --git a/src/OpenTheBox/Rendering/Panels/InventoryPanel.cs b/src/OpenTheBox/Rendering/Panels/InventoryPanel.cs
index c0cae65..e6faf31 100644
--- a/src/OpenTheBox/Rendering/Panels/InventoryPanel.cs
+++ b/src/OpenTheBox/Rendering/Panels/InventoryPanel.cs
@@ -31,6 +31,25 @@ public static class InventoryPanel
/// Compact row count for inline layout mode (fits in ~24-line terminals).
public const int CompactVisibleRows = 6;
+ ///
+ /// Custom sort order for categories — interactive items first.
+ ///
+ 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
+ };
+
///
/// Returns the total number of distinct item groups in the inventory.
///
@@ -38,7 +57,7 @@ public static class InventoryPanel
state.Inventory.GroupBy(i => i.DefinitionId).Count();
///
- /// Returns grouped inventory items sorted by category then definition id.
+ /// Returns grouped inventory items sorted by category (interactive first) then definition id.
///
public static List 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();
}
+ ///
+ /// Resolves a localized name for an inventory item, falling back to box name or definition id.
+ ///
+ 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;
+ }
+
+ ///
+ /// Returns a localized category label.
+ ///
+ private static string LocalizeCategory(ItemCategory cat, LocalizationManager? loc)
+ {
+ if (loc is null) return CategoryIcon(cat);
+ return CategoryIcon(cat);
+ }
+
+ ///
+ /// Returns a compact emoji icon for a category.
+ ///
+ 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 => "❓",
+ _ => "•"
+ };
+
+ ///
+ /// Returns a localized rarity label.
+ ///
+ private static string LocalizeRarity(ItemRarity rarity, LocalizationManager? loc)
+ {
+ if (loc is null) return rarity.ToString();
+ return loc.Get($"rarity.{rarity.ToString().ToLower()}");
+ }
+
///
/// Builds a renderable inventory table.
/// 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();
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;
}
diff --git a/suggestions_2.md b/suggestions_2.md
new file mode 100644
index 0000000..5d6aadc
--- /dev/null
+++ b/suggestions_2.md
@@ -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.
diff --git a/tests/OpenTheBox.Tests/UnitTest1.cs b/tests/OpenTheBox.Tests/UnitTest1.cs
index 0851d09..e2c75a0 100644
--- a/tests/OpenTheBox.Tests/UnitTest1.cs
+++ b/tests/OpenTheBox.Tests/UnitTest1.cs
@@ -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);