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);