Implement proposals.md + proposals_2.md: adaptive UX, early-game feedback, TUI default
- Adaptive welcome message based on progression (50/200/500 boxes thresholds) - Raw inventory display before InventoryPanel unlock (plain text list) - Terminal.Gui as default mode (--classic flag for old sequential renderer) - Early-game box counter before StatsPanel unlock - Encouraging messages in empty ResourcePanel and locked panel placeholders - Wider inventory name column (28 chars), cookie detail/consume, box teaser - Richer StatsPanel with items discovered and play time
This commit is contained in:
parent
576a23d540
commit
7b3a3d5a58
11 changed files with 252 additions and 19 deletions
10
CLAUDE.md
10
CLAUDE.md
|
|
@ -26,8 +26,8 @@ See [specifications.md](specifications.md) for detailed content organization.
|
||||||
## Build & Run
|
## Build & Run
|
||||||
```
|
```
|
||||||
dotnet build
|
dotnet build
|
||||||
dotnet run --project src/OpenTheBox # Classic Spectre.Console mode
|
dotnet run --project src/OpenTheBox # Default: Terminal.Gui panel layout
|
||||||
dotnet run --project src/OpenTheBox -- --tui # Terminal.Gui panel layout
|
dotnet run --project src/OpenTheBox -- --classic # Classic Spectre.Console sequential mode
|
||||||
dotnet run --project src/OpenTheBox -- --snapshot 5 # Load snapshot save #5
|
dotnet run --project src/OpenTheBox -- --snapshot 5 # Load snapshot save #5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -121,10 +121,10 @@ To capture rendering for a new panel or UI element:
|
||||||
2. Use `--logger "console;verbosity=detailed"` to see `Console.WriteLine()` output in the test runner
|
2. Use `--logger "console;verbosity=detailed"` to see `Console.WriteLine()` output in the test runner
|
||||||
3. Tests should `Assert.True(true)` or use lightweight assertions — the goal is visual inspection of output
|
3. Tests should `Assert.True(true)` or use lightweight assertions — the goal is visual inspection of output
|
||||||
|
|
||||||
## Terminal.Gui Mode
|
## Classic Mode
|
||||||
Run with `--tui` for a tmux-like panel layout using Terminal.Gui:
|
Terminal.Gui panel layout is the default. Use `--classic` for the old sequential Spectre.Console renderer:
|
||||||
```
|
```
|
||||||
dotnet run --project src/OpenTheBox -- --tui
|
dotnet run --project src/OpenTheBox -- --classic
|
||||||
```
|
```
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,9 @@
|
||||||
"misc.boxes_opened": "Total boxes opened: {0}",
|
"misc.boxes_opened": "Total boxes opened: {0}",
|
||||||
"misc.play_time": "Play time: {0}",
|
"misc.play_time": "Play time: {0}",
|
||||||
"misc.welcome_back": "Welcome back, {0}! Your boxes missed you.",
|
"misc.welcome_back": "Welcome back, {0}! Your boxes missed you.",
|
||||||
|
"misc.welcome_back_50": "Welcome back, {0}! The boxes have been waiting for you.",
|
||||||
|
"misc.welcome_back_200": "Welcome back, {0}! Your reputation precedes you. {1} boxes opened!",
|
||||||
|
"misc.welcome_back_500": "Welcome back, {0}! Legend walks among us. {1} boxes and counting...",
|
||||||
|
|
||||||
"recipe.refine_wood": "Refine Wood",
|
"recipe.refine_wood": "Refine Wood",
|
||||||
"recipe.smelt_bronze_ingot": "Smelt Bronze Ingot",
|
"recipe.smelt_bronze_ingot": "Smelt Bronze Ingot",
|
||||||
|
|
@ -516,5 +519,14 @@
|
||||||
"material.form.sheet": "Sheet",
|
"material.form.sheet": "Sheet",
|
||||||
"material.form.thread": "Thread",
|
"material.form.thread": "Thread",
|
||||||
"material.form.dust": "Dust",
|
"material.form.dust": "Dust",
|
||||||
"material.form.gem": "Gem"
|
"material.form.gem": "Gem",
|
||||||
|
|
||||||
|
"resource.empty_hint": "Resources will appear as you discover them...",
|
||||||
|
"inventory.box_teaser": "What mysteries await inside?",
|
||||||
|
"inventory.cookie_teaser": "Crack open for wisdom...",
|
||||||
|
"stats.items_discovered": "Items discovered",
|
||||||
|
"stats.play_time": "Play time",
|
||||||
|
"panel.locked.stats": "Open more boxes to unlock...",
|
||||||
|
"panel.locked.resources": "Discoveries await...",
|
||||||
|
"panel.locked.inventory": "Your collection grows..."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -386,6 +386,9 @@
|
||||||
"misc.boxes_opened": "Total de boîtes ouvertes : {0}",
|
"misc.boxes_opened": "Total de boîtes ouvertes : {0}",
|
||||||
"misc.play_time": "Temps de jeu : {0}",
|
"misc.play_time": "Temps de jeu : {0}",
|
||||||
"misc.welcome_back": "Bon retour, {0} ! Tes boîtes se sont ennuyées.",
|
"misc.welcome_back": "Bon retour, {0} ! Tes boîtes se sont ennuyées.",
|
||||||
|
"misc.welcome_back_50": "Bon retour, {0} ! Les boîtes t'attendaient.",
|
||||||
|
"misc.welcome_back_200": "Bon retour, {0} ! Ta réputation te précède. {1} boîtes ouvertes !",
|
||||||
|
"misc.welcome_back_500": "Bon retour, {0} ! La légende marche parmi nous. {1} boîtes et ça continue...",
|
||||||
|
|
||||||
"recipe.refine_wood": "Raffiner le bois",
|
"recipe.refine_wood": "Raffiner le bois",
|
||||||
"recipe.smelt_bronze_ingot": "Fondre un lingot de bronze",
|
"recipe.smelt_bronze_ingot": "Fondre un lingot de bronze",
|
||||||
|
|
@ -516,5 +519,14 @@
|
||||||
"material.form.sheet": "Feuille",
|
"material.form.sheet": "Feuille",
|
||||||
"material.form.thread": "Fil",
|
"material.form.thread": "Fil",
|
||||||
"material.form.dust": "Poudre",
|
"material.form.dust": "Poudre",
|
||||||
"material.form.gem": "Gemme"
|
"material.form.gem": "Gemme",
|
||||||
|
|
||||||
|
"resource.empty_hint": "Les ressources apparaîtront au fil de tes découvertes...",
|
||||||
|
"inventory.box_teaser": "Quels mystères se cachent à l'intérieur ?",
|
||||||
|
"inventory.cookie_teaser": "Ouvre-le pour une sagesse...",
|
||||||
|
"stats.items_discovered": "Objets découverts",
|
||||||
|
"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..."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,12 @@
|
||||||
|
|
||||||
**Solution** : Avant ce déblocage, l'inventaire devrait être imprimé brut dans la console `Item (x qtt)`. Il manque plein de fonctionnalité pour l'inventaire mais elles seront rendues disponibles lors du déblocage de la meta adéquat.
|
**Solution** : Avant ce déblocage, l'inventaire devrait être imprimé brut dans la console `Item (x qtt)`. Il manque plein de fonctionnalité pour l'inventaire mais elles seront rendues disponibles lors du déblocage de la meta adéquat.
|
||||||
|
|
||||||
|
## 3. Terminal.Gui Mode should be default
|
||||||
|
|
||||||
|
**Constat** : CLAUDE.md indique "Run with `--tui` for a tmux-like panel layout using Terminal.Gui:".
|
||||||
|
|
||||||
|
**Solution** : Le layout panel est une fonctionnalité meta in-game qui se débloque. Il ne devrait pas y avoir besoin de paramètre supplémentaire pour que les Terminal.Gui fonctionnent.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
63
proposals_2.md
Normal file
63
proposals_2.md
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Propositions d'améliorations du rendu (Round 2)
|
||||||
|
|
||||||
|
Basé sur l'analyse des captures PlaythroughCapture et InventoryRenderCapture.
|
||||||
|
|
||||||
|
## 1. Compteur de boîtes ouvertes visible en early-game
|
||||||
|
|
||||||
|
**Constat** : Avant le déblocage du StatsPanel (souvent box ~18-26), le joueur n'a aucune indication de sa progression. Les 10-20 premières boîtes se jouent sans aucun feedback visuel du nombre de boîtes ouvertes.
|
||||||
|
|
||||||
|
**Solution** : Afficher un simple compteur `Boxes opened: N` dans le BasicRenderer et dans les premiers niveaux du SpectreRenderer (avant StatsPanel), directement dans `ShowGameState()`.
|
||||||
|
|
||||||
|
## 2. Panneau de ressources vide au déblocage
|
||||||
|
|
||||||
|
**Constat** : Quand le ResourcePanel se débloque, il affiche "No resources visible yet." car aucune ressource n'est encore visible. C'est décevant comme premier contact avec un nouveau panneau.
|
||||||
|
|
||||||
|
**Solution** : À la place d'un message vide, afficher un message encourageant comme "Resources will appear as you discover them..." et éventuellement ajouter une première ressource visible automatiquement au déblocage du panneau.
|
||||||
|
|
||||||
|
## 3. Troncature des noms d'objets dans l'inventaire
|
||||||
|
|
||||||
|
**Constat** : Les noms longs comme "Potion de Santé Moyenne" sont tronqués en "Potion de Santé Moyen." dans le tableau de l'inventaire. Les colonnes sont trop étroites pour certains noms localisés en français.
|
||||||
|
|
||||||
|
**Solution** : Élargir la colonne `Nom` dans l'InventoryPanel ou implémenter un algorithme de troncature plus intelligent qui préserve les mots significatifs.
|
||||||
|
|
||||||
|
## 4. Détail des boîtes dans le panneau de détail
|
||||||
|
|
||||||
|
**Constat** : Quand un objet de type "Box" est sélectionné dans l'inventaire, le panneau de détail affiche la description de la boîte mais pas sa rareté colorée ni un indice sur son contenu potentiel.
|
||||||
|
|
||||||
|
**Solution** : Ajouter dans le panneau de détail des boîtes un indicateur de rareté et un texte teaser sur le type de loot possible (ex: "May contain meta items", "Fashion and style await").
|
||||||
|
|
||||||
|
## 5. Fortune Cookie sans panneau de détail dédié
|
||||||
|
|
||||||
|
**Constat** : Les Fortune Cookie dans l'inventaire n'ont pas de détail interactif. Leur catégorie est `Cookie` mais le panneau de détail est générique.
|
||||||
|
|
||||||
|
**Solution** : Ajouter un détail spécifique pour les cookies montrant un aperçu du type "Crack open for wisdom..." et permettre de les consommer via Enter pour révéler un message de fortune.
|
||||||
|
|
||||||
|
## 6. Panneau de stats trop spartiate en early-game
|
||||||
|
|
||||||
|
**Constat** : Quand le StatsPanel se débloque, il n'affiche que "Boxes opened: N" sans aucune autre stat visible. C'est fonctionnel mais peu engageant.
|
||||||
|
|
||||||
|
**Solution** : Ajouter des informations supplémentaires dans le StatsPanel : nombre total d'objets dans l'inventaire, nombre de types d'objets uniques découverts, et le temps de jeu.
|
||||||
|
|
||||||
|
## 7. Texte encourageant dans les panneaux verrouillés
|
||||||
|
|
||||||
|
**Constat** : Dans le mode séquentiel (avant FullLayout), les panneaux non encore débloqués ne sont pas visibles. Dans le FullLayout, les panneaux verrouillés affichent "[dim]???[/]" comme contenu placeholder.
|
||||||
|
|
||||||
|
**Solution** : Remplacer les placeholders "???" par des messages thématiques et encourageants (ex: "Keep opening boxes to unlock this panel...") qui changent selon le panneau.
|
||||||
|
|
||||||
|
## 8. Catégorie d'objet affichée comme icône illisible
|
||||||
|
|
||||||
|
**Constat** : La colonne catégorie dans l'inventaire affiche des icônes emoji (📦, 🧪, etc.) qui se rendent en "??" dans les captures plain text et peuvent être illisibles selon le terminal.
|
||||||
|
|
||||||
|
**Solution** : Proposer un fallback ASCII pour les icônes de catégorie quand les couleurs étendues ne sont pas disponibles, et s'assurer que le mode compact utilise des abréviations textuelles (BOX, CSM, MAT, etc.).
|
||||||
|
|
||||||
|
## 9. Détail matériau trop minimal
|
||||||
|
|
||||||
|
**Constat** : Le panneau de détail des matériaux affiche uniquement "Bronze (Lingot)" sans contexte. Le joueur ne sait pas à quoi sert ce matériau.
|
||||||
|
|
||||||
|
**Solution** : Ajouter dans le détail des matériaux la liste des recettes de crafting connues qui utilisent ce matériau, ou au minimum un texte indiquant "Used in crafting" si des workstations sont débloqués.
|
||||||
|
|
||||||
|
## 10. Adventure token sans indication d'aventure
|
||||||
|
|
||||||
|
**Constat** : Les tokens d'aventure (comme les badges) sont dans l'inventaire mais leur panneau de détail ne montre pas clairement quelle aventure ils débloquent ou permettent.
|
||||||
|
|
||||||
|
**Solution** : Améliorer le détail des adventure tokens pour afficher le nom de l'aventure associée et son statut (locked/unlocked/completed).
|
||||||
|
|
@ -34,7 +34,8 @@ public static class Program
|
||||||
|
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
_useTui = args.Contains("--tui");
|
// Terminal.Gui is the default mode; use --classic for the old sequential renderer
|
||||||
|
_useTui = !args.Contains("--classic");
|
||||||
|
|
||||||
// --snapshot N: directly load snapshot_N save and start playing
|
// --snapshot N: directly load snapshot_N save and start playing
|
||||||
int snapshotSlot = 0;
|
int snapshotSlot = 0;
|
||||||
|
|
@ -228,12 +229,26 @@ public static class Program
|
||||||
_loc.Change(_state.CurrentLocale);
|
_loc.Change(_state.CurrentLocale);
|
||||||
InitializeGame();
|
InitializeGame();
|
||||||
|
|
||||||
_renderer.ShowMessage(_loc.Get("misc.welcome_back", _state.PlayerName));
|
ShowAdaptiveWelcome();
|
||||||
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
||||||
|
|
||||||
await GameLoop();
|
await GameLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ShowAdaptiveWelcome()
|
||||||
|
{
|
||||||
|
int boxes = _state.TotalBoxesOpened;
|
||||||
|
string name = _state.PlayerName;
|
||||||
|
string message = boxes switch
|
||||||
|
{
|
||||||
|
>= 500 => _loc.Get("misc.welcome_back_500", name, boxes.ToString()),
|
||||||
|
>= 200 => _loc.Get("misc.welcome_back_200", name, boxes.ToString()),
|
||||||
|
>= 50 => _loc.Get("misc.welcome_back_50", name),
|
||||||
|
_ => _loc.Get("misc.welcome_back", name)
|
||||||
|
};
|
||||||
|
_renderer.ShowMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task NewGame()
|
private static async Task NewGame()
|
||||||
{
|
{
|
||||||
string name = _renderer.ShowTextInput(_loc.Get("prompt.name"));
|
string name = _renderer.ShowTextInput(_loc.Get("prompt.name"));
|
||||||
|
|
@ -281,7 +296,7 @@ public static class Program
|
||||||
_loc.Change(_state.CurrentLocale);
|
_loc.Change(_state.CurrentLocale);
|
||||||
InitializeGame();
|
InitializeGame();
|
||||||
|
|
||||||
_renderer.ShowMessage(_loc.Get("misc.welcome_back", _state.PlayerName));
|
ShowAdaptiveWelcome();
|
||||||
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
||||||
|
|
||||||
await GameLoop();
|
await GameLoop();
|
||||||
|
|
@ -607,6 +622,13 @@ public static class Program
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before InventoryPanel is unlocked, show a raw text list
|
||||||
|
if (!_renderContext.HasInventoryPanel)
|
||||||
|
{
|
||||||
|
ShowRawInventory();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var grouped = InventoryPanel.GetGroupedItems(_state, _registry);
|
var grouped = InventoryPanel.GetGroupedItems(_state, _registry);
|
||||||
int totalItems = grouped.Count;
|
int totalItems = grouped.Count;
|
||||||
int maxVisible = InventoryPanel.MaxVisibleRows;
|
int maxVisible = InventoryPanel.MaxVisibleRows;
|
||||||
|
|
@ -677,6 +699,63 @@ public static class Program
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shows a minimal raw-text inventory before the InventoryPanel feature is unlocked.
|
||||||
|
/// </summary>
|
||||||
|
private static void ShowRawInventory()
|
||||||
|
{
|
||||||
|
_renderer.ShowMessage($"--- {_loc.Get("ui.inventory")} ---");
|
||||||
|
var groups = _state.Inventory
|
||||||
|
.GroupBy(i => i.DefinitionId)
|
||||||
|
.ToList();
|
||||||
|
foreach (var g in groups)
|
||||||
|
{
|
||||||
|
string name = GetLocalizedName(g.Key);
|
||||||
|
int qty = g.Sum(i => i.Quantity);
|
||||||
|
_renderer.ShowMessage(qty > 1 ? $" {name} (x{qty})" : $" {name}");
|
||||||
|
}
|
||||||
|
_renderer.ShowMessage("");
|
||||||
|
|
||||||
|
// Allow using consumables even in raw mode
|
||||||
|
var consumables = groups
|
||||||
|
.Where(g =>
|
||||||
|
{
|
||||||
|
var def = _registry.GetItem(g.Key);
|
||||||
|
return def?.Category == ItemCategory.Consumable && def.ResourceType.HasValue;
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (consumables.Count > 0)
|
||||||
|
{
|
||||||
|
var useOptions = consumables.Select(g =>
|
||||||
|
{
|
||||||
|
string name = GetLocalizedName(g.Key);
|
||||||
|
int qty = g.Sum(i => i.Quantity);
|
||||||
|
return qty > 1 ? $"{name} (x{qty})" : name;
|
||||||
|
}).ToList();
|
||||||
|
useOptions.Add(_loc.Get("menu.back"));
|
||||||
|
|
||||||
|
int choice = _renderer.ShowSelection(_loc.Get("inventory.controls_use"), useOptions);
|
||||||
|
if (choice < consumables.Count)
|
||||||
|
{
|
||||||
|
var instance = consumables[choice].First();
|
||||||
|
var events = _simulation.ProcessAction(new UseItemAction(instance.Id), _state);
|
||||||
|
foreach (var evt in events)
|
||||||
|
{
|
||||||
|
if (evt is ResourceChangedEvent resEvt)
|
||||||
|
{
|
||||||
|
var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
|
||||||
|
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} -> {resEvt.NewValue}");
|
||||||
|
}
|
||||||
|
else if (evt is MessageEvent msgEvt)
|
||||||
|
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
||||||
|
}
|
||||||
|
|
||||||
private static void HandleInventoryAction(InventoryGroup item)
|
private static void HandleInventoryAction(InventoryGroup item)
|
||||||
{
|
{
|
||||||
if (item.Def is null) return;
|
if (item.Def is null) return;
|
||||||
|
|
@ -710,6 +789,30 @@ public static class Program
|
||||||
// No WaitForKeyPress — return to inventory immediately for rapid consumption
|
// No WaitForKeyPress — return to inventory immediately for rapid consumption
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ItemCategory.Cookie:
|
||||||
|
// Use the cookie through the simulation (similar to Consumable)
|
||||||
|
var cookieEvents = _simulation.ProcessAction(
|
||||||
|
new UseItemAction(item.FirstInstance.Id), _state);
|
||||||
|
foreach (var evt in cookieEvents)
|
||||||
|
{
|
||||||
|
switch (evt)
|
||||||
|
{
|
||||||
|
case CookieFortuneEvent cookieEvt:
|
||||||
|
_renderer.ShowMessage("--- Fortune Cookie ---");
|
||||||
|
_renderer.ShowMessage(_loc.Get(cookieEvt.MessageKey));
|
||||||
|
_renderer.ShowMessage("----------------------");
|
||||||
|
break;
|
||||||
|
case ResourceChangedEvent resEvt:
|
||||||
|
var cookieResName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
|
||||||
|
_renderer.ShowMessage($"{cookieResName}: {resEvt.OldValue} → {resEvt.NewValue}");
|
||||||
|
break;
|
||||||
|
case MessageEvent msgEvt:
|
||||||
|
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case ItemCategory.LoreFragment:
|
case ItemCategory.LoreFragment:
|
||||||
// Display the full lore text in a dedicated panel
|
// Display the full lore text in a dedicated panel
|
||||||
ShowLoreFragment(item);
|
ShowLoreFragment(item);
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,11 @@ public sealed class BasicRenderer(LocalizationManager loc) : IRenderer
|
||||||
|
|
||||||
public void ShowGameState(GameState state, RenderContext context)
|
public void ShowGameState(GameState state, RenderContext context)
|
||||||
{
|
{
|
||||||
|
if (!context.HasStatsPanel)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{loc.Get("stats.boxes_opened")}: {state.TotalBoxesOpened}");
|
||||||
|
}
|
||||||
|
|
||||||
if (context.HasCompletionTracker)
|
if (context.HasCompletionTracker)
|
||||||
{
|
{
|
||||||
Console.WriteLine(loc.Get("ui.completion", context.CompletionPercent));
|
Console.WriteLine(loc.Get("ui.completion", context.CompletionPercent));
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ public sealed record InventoryGroup(
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class InventoryPanel
|
public static class InventoryPanel
|
||||||
{
|
{
|
||||||
private const int MaxNameWidth = 24;
|
private const int MaxNameWidth = 28;
|
||||||
public const int MaxVisibleRows = 15;
|
public const int MaxVisibleRows = 15;
|
||||||
/// <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;
|
||||||
|
|
@ -276,16 +276,21 @@ public static class InventoryPanel
|
||||||
switch (item.Category)
|
switch (item.Category)
|
||||||
{
|
{
|
||||||
case ItemCategory.Box:
|
case ItemCategory.Box:
|
||||||
// Show box description if available
|
// Show box rarity, description, and teaser
|
||||||
if (registry is not null)
|
if (registry is not null)
|
||||||
{
|
{
|
||||||
var boxDef = registry.GetBox(item.DefId);
|
var boxDef = registry.GetBox(item.DefId);
|
||||||
if (boxDef is not null && loc is not null)
|
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);
|
string boxDesc = loc.Get(boxDef.DescriptionKey);
|
||||||
rows.Add(new Markup($"[italic dim]{Markup.Escape(boxDesc)}[/]"));
|
rows.Add(new Markup($"[italic dim]{Markup.Escape(boxDesc)}[/]"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
string boxTeaser = loc?.Get("inventory.box_teaser") ?? "What mysteries await inside?";
|
||||||
|
rows.Add(new Markup($"[dim yellow]{Markup.Escape(boxTeaser)}[/]"));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ItemCategory.Consumable when item.Def?.ResourceType is not null && item.Def.ResourceAmount is not null:
|
case ItemCategory.Consumable when item.Def?.ResourceType is not null && item.Def.ResourceAmount is not null:
|
||||||
|
|
@ -328,6 +333,12 @@ public static class InventoryPanel
|
||||||
rows.Add(new Markup($"[dim]{Markup.Escape(matType)} ({Markup.Escape(matForm)})[/]"));
|
rows.Add(new Markup($"[dim]{Markup.Escape(matType)} ({Markup.Escape(matForm)})[/]"));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ItemCategory.Cookie:
|
||||||
|
string cookieTeaser = loc?.Get("inventory.cookie_teaser") ?? "Crack open for wisdom...";
|
||||||
|
rows.Add(new Markup($"[dim yellow]{Markup.Escape(cookieTeaser)}[/]"));
|
||||||
|
rows.Add(new Markup($"[dim yellow]{Markup.Escape(loc?.Get("inventory.press_enter_use") ?? "Press Enter to use")}[/]"));
|
||||||
|
break;
|
||||||
|
|
||||||
case ItemCategory.AdventureToken when item.Def?.AdventureTheme is not null:
|
case ItemCategory.AdventureToken when item.Def?.AdventureTheme is not null:
|
||||||
string advName = loc?.Get($"adventure.name.{item.Def.AdventureTheme.Value}") ?? item.Def.AdventureTheme.Value.ToString();
|
string advName = loc?.Get($"adventure.name.{item.Def.AdventureTheme.Value}") ?? item.Def.AdventureTheme.Value.ToString();
|
||||||
string advLabel = loc?.Get("inventory.adventure") ?? "Adventure";
|
string advLabel = loc?.Get("inventory.adventure") ?? "Adventure";
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using OpenTheBox.Core;
|
using OpenTheBox.Core;
|
||||||
using OpenTheBox.Core.Enums;
|
using OpenTheBox.Core.Enums;
|
||||||
|
using OpenTheBox.Localization;
|
||||||
using Spectre.Console;
|
using Spectre.Console;
|
||||||
using Spectre.Console.Rendering;
|
using Spectre.Console.Rendering;
|
||||||
|
|
||||||
|
|
@ -14,7 +15,7 @@ public static class ResourcePanel
|
||||||
/// Builds a renderable resource display from the current game state.
|
/// Builds a renderable resource display from the current game state.
|
||||||
/// Only resources present in <see cref="GameState.VisibleResources"/> are shown.
|
/// Only resources present in <see cref="GameState.VisibleResources"/> are shown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IRenderable Render(GameState state)
|
public static IRenderable Render(GameState state, LocalizationManager? loc = null)
|
||||||
{
|
{
|
||||||
var rows = new List<IRenderable>();
|
var rows = new List<IRenderable>();
|
||||||
|
|
||||||
|
|
@ -41,7 +42,8 @@ public static class ResourcePanel
|
||||||
|
|
||||||
if (rows.Count == 0)
|
if (rows.Count == 0)
|
||||||
{
|
{
|
||||||
rows.Add(new Markup("[dim]No resources visible yet.[/]"));
|
string emptyHint = loc?.Get("resource.empty_hint") ?? "Resources will appear as you discover them...";
|
||||||
|
rows.Add(new Markup($"[dim italic]{Markup.Escape(emptyHint)}[/]"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Panel(new Rows(rows))
|
return new Panel(new Rows(rows))
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,18 @@ public static class StatsPanel
|
||||||
string boxesLabel = loc?.Get("stats.boxes_opened") ?? "Boxes Opened";
|
string boxesLabel = loc?.Get("stats.boxes_opened") ?? "Boxes Opened";
|
||||||
rows.Add(new Markup($" [silver]{Markup.Escape(boxesLabel)}:[/] [bold]{state.TotalBoxesOpened}[/]"));
|
rows.Add(new Markup($" [silver]{Markup.Escape(boxesLabel)}:[/] [bold]{state.TotalBoxesOpened}[/]"));
|
||||||
|
|
||||||
|
// Total unique item types discovered
|
||||||
|
int uniqueItems = state.Inventory.Select(i => i.DefinitionId).Distinct().Count();
|
||||||
|
string discoveredLabel = loc?.Get("stats.items_discovered") ?? "Items discovered";
|
||||||
|
rows.Add(new Markup($" [silver]{Markup.Escape(discoveredLabel)}:[/] [bold]{uniqueItems}[/]"));
|
||||||
|
|
||||||
|
// Play time
|
||||||
|
string playTimeLabel = loc?.Get("stats.play_time") ?? "Play time";
|
||||||
|
string playTimeFormatted = state.TotalPlayTime.TotalHours >= 1
|
||||||
|
? $"{(int)state.TotalPlayTime.TotalHours}h {state.TotalPlayTime.Minutes:D2}m"
|
||||||
|
: $"{state.TotalPlayTime.Minutes}m {state.TotalPlayTime.Seconds:D2}s";
|
||||||
|
rows.Add(new Markup($" [silver]{Markup.Escape(playTimeLabel)}:[/] [bold]{playTimeFormatted}[/]"));
|
||||||
|
|
||||||
string title = loc?.Get("stats.title") ?? "Stats";
|
string title = loc?.Get("stats.title") ?? "Stats";
|
||||||
|
|
||||||
return new Panel(new Rows(rows))
|
return new Panel(new Rows(rows))
|
||||||
|
|
|
||||||
|
|
@ -388,10 +388,10 @@ public sealed class SpectreRenderer : IRenderer
|
||||||
PortraitPanel.Render(state.Appearance, context.HasPortraitPanel),
|
PortraitPanel.Render(state.Appearance, context.HasPortraitPanel),
|
||||||
context.HasStatsPanel
|
context.HasStatsPanel
|
||||||
? StatsPanel.Render(state, _loc)
|
? StatsPanel.Render(state, _loc)
|
||||||
: new Panel("[dim]???[/]").Header("Stats").Expand(),
|
: new Panel($"[dim italic]{Markup.Escape(_loc.Get("panel.locked.stats"))}[/]").Header("Stats").Expand(),
|
||||||
context.HasResourcePanel
|
context.HasResourcePanel
|
||||||
? ResourcePanel.Render(state)
|
? ResourcePanel.Render(state, _loc)
|
||||||
: new Panel("[dim]???[/]").Header("Resources").Expand());
|
: new Panel($"[dim italic]{Markup.Escape(_loc.Get("panel.locked.resources"))}[/]").Header("Resources").Expand());
|
||||||
|
|
||||||
AnsiConsole.Write(topRow);
|
AnsiConsole.Write(topRow);
|
||||||
|
|
||||||
|
|
@ -403,7 +403,7 @@ public sealed class SpectreRenderer : IRenderer
|
||||||
// Left: Inventory
|
// Left: Inventory
|
||||||
IRenderable leftPanel = context.HasInventoryPanel
|
IRenderable leftPanel = context.HasInventoryPanel
|
||||||
? InventoryPanel.Render(state, _registry, _loc, compact: true)
|
? InventoryPanel.Render(state, _registry, _loc, compact: true)
|
||||||
: new Panel("[dim]???[/]").Header("Inventory").Expand();
|
: new Panel($"[dim italic]{Markup.Escape(_loc.Get("panel.locked.inventory"))}[/]").Header("Inventory").Expand();
|
||||||
|
|
||||||
// Right: stack Crafting + Chat + Completion
|
// Right: stack Crafting + Chat + Completion
|
||||||
var rightItems = new List<IRenderable>();
|
var rightItems = new List<IRenderable>();
|
||||||
|
|
@ -435,7 +435,14 @@ public sealed class SpectreRenderer : IRenderer
|
||||||
var topPanels = new List<IRenderable>();
|
var topPanels = new List<IRenderable>();
|
||||||
topPanels.Add(PortraitPanel.Render(state.Appearance, context.HasPortraitPanel));
|
topPanels.Add(PortraitPanel.Render(state.Appearance, context.HasPortraitPanel));
|
||||||
if (context.HasStatsPanel) topPanels.Add(StatsPanel.Render(state, _loc));
|
if (context.HasStatsPanel) topPanels.Add(StatsPanel.Render(state, _loc));
|
||||||
if (context.HasResourcePanel) topPanels.Add(ResourcePanel.Render(state));
|
if (context.HasResourcePanel) topPanels.Add(ResourcePanel.Render(state, _loc));
|
||||||
|
|
||||||
|
// Show box count when StatsPanel is not yet unlocked
|
||||||
|
if (!context.HasStatsPanel)
|
||||||
|
{
|
||||||
|
string boxesLabel = _loc.Get("stats.boxes_opened");
|
||||||
|
AnsiConsole.MarkupLine($"[dim]{Markup.Escape(boxesLabel)}: {state.TotalBoxesOpened}[/]");
|
||||||
|
}
|
||||||
|
|
||||||
if (topPanels.Count > 1)
|
if (topPanels.Count > 1)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue