Implement proposals_3.md: box rarity fix, ASCII categories, separators, crafting hints

- Fix box rarity display to use box definition rarity in detail panel
- Replace emoji category icons with ASCII abbreviations (BOX, CSM, MAT, etc.)
- Add category separator rows between groups in non-compact inventory
- Show equipped/not equipped status for cosmetic items
- Show crafting recipe hints for materials when workstations are unlocked
- Add rarity stars (★) in loot reveal table for Rare+ items
This commit is contained in:
Samuel Bouchet 2026-03-14 09:40:49 +01:00
parent 7b3a3d5a58
commit 6e5bc6e35e
5 changed files with 193 additions and 13 deletions

View file

@ -528,5 +528,10 @@
"stats.play_time": "Play time",
"panel.locked.stats": "Open more boxes to unlock...",
"panel.locked.resources": "Discoveries await...",
"panel.locked.inventory": "Your collection grows..."
"panel.locked.inventory": "Your collection grows...",
"inventory.equipped": "Equipped",
"inventory.not_equipped": "Not equipped",
"inventory.crafting_hint": "Used in crafting",
"inventory.recipes_available": "Recipes"
}

View file

@ -528,5 +528,10 @@
"stats.play_time": "Temps de jeu",
"panel.locked.stats": "Ouvre plus de boîtes pour débloquer...",
"panel.locked.resources": "Des découvertes t'attendent...",
"panel.locked.inventory": "Ta collection grandit..."
"panel.locked.inventory": "Ta collection grandit...",
"inventory.equipped": "Équipé",
"inventory.not_equipped": "Non équipé",
"inventory.crafting_hint": "Utilisé en artisanat",
"inventory.recipes_available": "Recettes"
}

63
proposals_3.md Normal file
View file

@ -0,0 +1,63 @@
# Propositions d'améliorations du rendu (Round 3)
Basé sur l'analyse des captures PlaythroughCapture et InventoryRenderCapture après implémentation de proposals_2.md.
## 1. Rareté des boîtes mal présentée dans le détail
**Constat** : Le panneau de détail des boîtes affiche la rareté comme texte brut ("Légendaire", "Rare") sur une ligne isolée sans label. De plus, la rareté affichée provient de la définition de box (Rare, Legendary) tandis que la première ligne montre "(Commun)" — qui est la rareté item. C'est confus.
**Solution** : Afficher la rareté de la boîte avec un label clair, par exemple "Rarity: Legendary" avec la couleur appropriée, et supprimer la rareté item "(Commun)" de la première ligne pour les boîtes, ou la remplacer par la rareté box.
## 2. Catégorie emoji illisible en plain text ("??")
**Constat** : Les icônes emoji de catégorie (📦, 🧪, etc.) se rendent en "??" dans les captures plain text et dans certains terminaux non-Unicode. Cela rend la colonne catégorie inutile.
**Solution** : Utiliser des abréviations ASCII comme fallback quand le rendu est en mode NoColors/plain : BOX, CSM, LOR, ADV, KEY, COS, MAT, CRF, MET, COK, MUS. Garder les emojis pour le mode couleur.
## 3. Séparateur visuel entre catégories d'inventaire
**Constat** : L'inventaire affiche tous les objets dans une liste plate. Quand le joueur a 80+ types d'objets, il est difficile de distinguer visuellement les boîtes des consommables des matériaux.
**Solution** : Ajouter une ligne séparatrice légère ou un en-tête de groupe quand la catégorie change dans le tableau d'inventaire (ex: une ligne vide ou un séparateur "---" entre les groupes).
## 4. Détail cosmétique sans statut d'équipement
**Constat** : Le panneau de détail des cosmétiques affiche seulement "Emplacement: Yeux" sans indiquer si l'objet est actuellement équipé ou non.
**Solution** : Ajouter un indicateur "[Equipped]" / "[Équipé]" quand le cosmétique correspond à l'apparence actuelle du joueur. Clés : `"inventory.equipped": "Equipped"`, `"inventory.not_equipped": "Not equipped"`.
## 5. Détail matériau avec recettes de crafting
**Constat** : Le détail des matériaux affiche uniquement "Bronze (Lingot)" sans contexte. Le joueur ne sait pas si ce matériau sera utile.
**Solution** : Si des workstations sont débloquées, afficher les recettes connues qui utilisent ce matériau. Sinon, afficher "Used in crafting" / "Utilisé en artisanat" comme hint.
## 6. Résumé compact des ressources dans le loot reveal
**Constat** : L'inline resource summary (Proposal 4 original) fonctionne mais n'est pas localisé proprement. Le format `[Health 5/10 | Gold 3/5]` utilise les noms en anglais plutôt que les noms localisés.
**Solution** : S'assurer que le résumé inline des ressources utilise `_loc.Get()` pour les noms de ressources (déjà fait dans le code, vérifier que les noms sont bien localisés dans toutes les situations).
## 7. Icône de rareté dans le loot reveal
**Constat** : Le loot reveal en mode tableau affiche le nom et la rareté mais sans indicateur visuel distinctif pour les drops rares/épiques/légendaires. Tout se ressemble.
**Solution** : Ajouter un symbole devant les items de rareté élevée dans le loot reveal : ★ pour Rare, ★★ pour Epic, ★★★ pour Legendary, ★★★★ pour Mythic.
## 8. Nombre total de boîtes ouvertes dans le header de l'inventaire
**Constat** : L'en-tête de l'inventaire montre "Inventaire (1-15/83)" avec le nombre de types d'objets mais pas le nombre total de boîtes ouvertes, ce qui est une métrique de progression clé.
**Solution** : Ajouter le compteur de boîtes dans le sous-titre de l'inventaire quand il est en mode compact (dans le full layout) : "Inventaire (15 types) | 200 boxes".
## 9. Temps de jeu formaté de manière plus lisible
**Constat** : Le temps de jeu dans le StatsPanel affiche "0m 00s" en test, mais en jeu réel il affichera "45m 23s" ou "2h 15m". Le format pourrait être plus lisible.
**Solution** : Utiliser un format adaptatif : "23s" pour < 1min, "5m 23s" pour < 1h, "2h 15m" pour < 24h, "1d 2h" pour >= 24h. Supprimer les zéros non significatifs.
## 10. Indication du nombre de recettes disponibles pour un matériau
**Constat** : Avec la proposition 5 ci-dessus, on peut afficher les recettes, mais le joueur devrait aussi savoir combien d'ingrédients il a vs. combien sont nécessaires.
**Solution** : Dans le détail matériau, afficher pour chaque recette connue si le joueur a suffisamment d'ingrédients ou combien il lui manque : "Bronze Ingot (2/3 needed)".

View file

@ -132,6 +132,30 @@ public static class InventoryPanel
_ => "•"
};
/// <summary>
/// Returns an ASCII 3-letter abbreviation for a category (terminal-safe fallback).
/// </summary>
private static string CategoryAbbrev(ItemCategory cat) => cat switch
{
ItemCategory.Box => "BOX",
ItemCategory.Key => "KEY",
ItemCategory.Consumable => "CSM",
ItemCategory.LoreFragment => "LOR",
ItemCategory.Cosmetic => "COS",
ItemCategory.Material => "MAT",
ItemCategory.Meta => "MET",
ItemCategory.AdventureToken => "ADV",
ItemCategory.Cookie => "COK",
ItemCategory.Music => "MUS",
ItemCategory.CraftedItem => "CRF",
ItemCategory.WorkstationBlueprint => "BLP",
ItemCategory.Badge => "BDG",
ItemCategory.Map => "MAP",
ItemCategory.StoryItem => "STR",
ItemCategory.QuestItem => "QST",
_ => "..."
};
/// <summary>
/// Returns a localized rarity label.
/// </summary>
@ -163,7 +187,7 @@ public static class InventoryPanel
var table = new Table()
.Border(TableBorder.Rounded)
.AddColumn(new TableColumn($"[bold]{Markup.Escape(colName)}[/]").Width(MaxNameWidth))
.AddColumn(new TableColumn("").Centered().Width(2))
.AddColumn(new TableColumn("").Centered().Width(3))
.AddColumn(new TableColumn($"[bold]{Markup.Escape(colRarity)}[/]").Centered())
.AddColumn(new TableColumn($"[bold]{Markup.Escape(colQty)}[/]").RightAligned());
@ -173,19 +197,27 @@ public static class InventoryPanel
int clampedOffset = Math.Clamp(scrollOffset, 0, Math.Max(0, totalItems - maxRows));
var visible = grouped.Skip(clampedOffset).Take(maxRows).ToList();
ItemCategory? prevCategory = null;
for (int i = 0; i < visible.Count; i++)
{
var item = visible[i];
int globalIndex = clampedOffset + i;
bool isSelected = globalIndex == selectedIndex;
// Category separator between different groups (non-compact only)
if (!compact && prevCategory is not null && prevCategory != item.Category)
{
table.AddEmptyRow();
}
prevCategory = item.Category;
// Resolve localized name, truncate to fit column with "► " or " " prefix
string name = ResolveName(item, registry, loc);
int maxDisplayName = MaxNameWidth - 2; // account for prefix
if (name.Length > maxDisplayName)
name = name[..(maxDisplayName - 1)] + "…";
string catIcon = LocalizeCategory(item.Category, loc);
string catAbbrev = CategoryAbbrev(item.Category);
string rarity = LocalizeRarity(item.Rarity, loc);
string color = RarityColor(item.Rarity);
@ -194,7 +226,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]{catIcon}[/]",
$"[bold on grey23]{catAbbrev}[/]",
$"[bold {color} on grey23]{Markup.Escape(rarity)}[/]",
$"[bold on grey23]{item.TotalQty}[/]");
}
@ -202,7 +234,7 @@ public static class InventoryPanel
{
table.AddRow(
$"[{color}] {Markup.Escape(name)}[/]",
$"[dim]{catIcon}[/]",
$"[dim]{catAbbrev}[/]",
$"[{color}]{Markup.Escape(rarity)}[/]",
item.TotalQty.ToString());
}
@ -261,9 +293,30 @@ public static class InventoryPanel
string color = RarityColor(item.Rarity);
// Item name — resolve via item def or box def
// For boxes, use the box definition's rarity instead of the item rarity
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)})[/]"));
string displayRarityLabel;
string displayColor;
if (item.Category == ItemCategory.Box && registry is not null)
{
var boxDefHeader = registry.GetBox(item.DefId);
if (boxDefHeader is not null)
{
displayRarityLabel = LocalizeRarity(boxDefHeader.Rarity, loc);
displayColor = RarityColor(boxDefHeader.Rarity);
}
else
{
displayRarityLabel = LocalizeRarity(item.Rarity, loc);
displayColor = color;
}
}
else
{
displayRarityLabel = LocalizeRarity(item.Rarity, loc);
displayColor = color;
}
rows.Add(new Markup($"[bold {displayColor}]{Markup.Escape(name)}[/] [dim]({Markup.Escape(displayRarityLabel)})[/]"));
// Description if available (item def)
if (item.Def?.DescriptionKey is not null && loc is not null)
@ -276,15 +329,12 @@ public static class InventoryPanel
switch (item.Category)
{
case ItemCategory.Box:
// Show box rarity, description, and teaser
// Show box description and teaser
if (registry is not null)
{
var boxDef = registry.GetBox(item.DefId);
if (boxDef is not null && loc is not null)
{
string boxRarityLabel = loc.Get($"rarity.{boxDef.Rarity.ToString().ToLower()}");
string boxRarityColor = RarityColor(boxDef.Rarity);
rows.Add(new Markup($"[{boxRarityColor}]{Markup.Escape(boxRarityLabel)}[/]"));
string boxDesc = loc.Get(boxDef.DescriptionKey);
rows.Add(new Markup($"[italic dim]{Markup.Escape(boxDesc)}[/]"));
}
@ -325,12 +375,55 @@ public static class InventoryPanel
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)}[/]"));
// Check if this cosmetic is currently equipped
if (state?.Appearance is not null && item.Def.CosmeticValue is not null)
{
bool isEquipped = item.Def.CosmeticSlot.Value switch
{
CosmeticSlot.Hair => string.Equals(state.Appearance.HairStyle.ToString(), item.Def.CosmeticValue, StringComparison.OrdinalIgnoreCase),
CosmeticSlot.Eyes => string.Equals(state.Appearance.EyeStyle.ToString(), item.Def.CosmeticValue, StringComparison.OrdinalIgnoreCase),
CosmeticSlot.Body => string.Equals(state.Appearance.BodyStyle.ToString(), item.Def.CosmeticValue, StringComparison.OrdinalIgnoreCase),
CosmeticSlot.Legs => string.Equals(state.Appearance.LegStyle.ToString(), item.Def.CosmeticValue, StringComparison.OrdinalIgnoreCase),
CosmeticSlot.Arms => string.Equals(state.Appearance.ArmStyle.ToString(), item.Def.CosmeticValue, StringComparison.OrdinalIgnoreCase),
_ => false
};
if (isEquipped)
{
string equippedLabel = loc?.Get("inventory.equipped") ?? "Equipped";
rows.Add(new Markup($"[green]✓ {Markup.Escape(equippedLabel)}[/]"));
}
else
{
string notEquippedLabel = loc?.Get("inventory.not_equipped") ?? "Not equipped";
rows.Add(new Markup($"[dim]{Markup.Escape(notEquippedLabel)}[/]"));
}
}
break;
case ItemCategory.Material when item.Def?.MaterialType is not null:
string matType = loc?.Get($"material.{item.Def.MaterialType.Value.ToString().ToLower()}") ?? item.Def.MaterialType.Value.ToString();
string matForm = loc?.Get($"material.form.{(item.Def.MaterialForm ?? MaterialForm.Raw).ToString().ToLower()}") ?? item.Def.MaterialForm?.ToString() ?? "Raw";
rows.Add(new Markup($"[dim]{Markup.Escape(matType)} ({Markup.Escape(matForm)})[/]"));
// Show crafting hints for this material
if (registry?.Recipes is not null)
{
var matchingRecipes = registry.Recipes.Values
.Where(r => r.Ingredients.Any(ing => ing.ItemDefinitionId == item.DefId))
.ToList();
if (matchingRecipes.Count > 0 && state?.UnlockedWorkstations.Count > 0)
{
string recipesLabel = loc?.Get("inventory.recipes_available") ?? "Recipes";
var recipeNames = matchingRecipes
.Select(r => loc?.Get(r.NameKey) ?? r.Id)
.ToArray();
rows.Add(new Markup($"[dim cyan]{Markup.Escape(recipesLabel)}: {Markup.Escape(string.Join(", ", recipeNames))}[/]"));
}
else if (matchingRecipes.Count > 0)
{
string craftHint = loc?.Get("inventory.crafting_hint") ?? "Used in crafting";
rows.Add(new Markup($"[dim cyan]{Markup.Escape(craftHint)}[/]"));
}
}
break;
case ItemCategory.Cookie:

View file

@ -90,8 +90,10 @@ public sealed class SpectreRenderer : IRenderer
{
string color = RarityColor(rarity);
string localizedRarity = _loc.Get($"rarity.{rarity.ToLower()}");
string stars = RarityStars(rarity);
string displayName = stars.Length > 0 ? $"{stars}{name}" : name;
table.AddRow(
$"[{color}]{Markup.Escape(name)}[/]",
$"[{color}]{Markup.Escape(displayName)}[/]",
$"[{color}]{Markup.Escape(localizedRarity)}[/]");
}
@ -360,6 +362,18 @@ public sealed class SpectreRenderer : IRenderer
_ => "white"
};
/// <summary>
/// Returns rarity star prefix for Rare and above items.
/// </summary>
private static string RarityStars(string rarity) => rarity.ToLowerInvariant() switch
{
"rare" => "★ ",
"epic" => "★★ ",
"legendary" => "★★★ ",
"mythic" => "★★★★ ",
_ => ""
};
private static Color RarityColorValue(string rarity) => rarity.ToLowerInvariant() switch
{
"common" => Color.White,