From c8961830bbe8a100d063f45137e17cfe67b39bf8 Mon Sep 17 00:00:00 2001 From: Samuel Bouchet Date: Sat, 14 Mar 2026 22:25:20 +0100 Subject: [PATCH] Implement TODO.md: inventory UX, ephemeral items, category names, launcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Inventory panel: expand to full width, fix layout with long descriptions - Compact inventory: show category summary instead of item list - Detail panel header: show full category name instead of generic "Details" - Material description: show "Crafting material — Raw" instead of repeating name - Equipped indicator: use > fallback instead of ✓ on non-UTF8 terminals - Fortune cookies and music: consumed immediately on receipt (ephemeral) - Remove inline resource summary after box opening (not useful) - Add localized category names (EN + FR) for 16 item categories - Add OpenTheBox.cmd launcher for Windows Terminal / PowerShell / cmd - Update TODO.md with characteristics report --- OpenTheBox.cmd | 16 ++++ TODO.md | 79 ++------------- content/strings/en.json | 20 +++- content/strings/fr.json | 20 +++- src/OpenTheBox/Program.cs | 10 +- .../Rendering/Panels/InventoryPanel.cs | 95 ++++++++++++++----- src/OpenTheBox/Simulation/MetaEngine.cs | 8 +- 7 files changed, 141 insertions(+), 107 deletions(-) create mode 100644 OpenTheBox.cmd diff --git a/OpenTheBox.cmd b/OpenTheBox.cmd new file mode 100644 index 0000000..3b5195d --- /dev/null +++ b/OpenTheBox.cmd @@ -0,0 +1,16 @@ +@echo off +REM Launch Open The Box in Windows Terminal with PowerShell and UTF-8 support. +REM Falls back to PowerShell directly if Windows Terminal is not available. + +where wt >nul 2>nul +if %ERRORLEVEL% equ 0 ( + wt new-tab -p "PowerShell" -- pwsh -NoProfile -Command "& { [Console]::OutputEncoding = [Text.Encoding]::UTF8; dotnet run --project '%~dp0src\OpenTheBox' -- %* }" +) else ( + where pwsh >nul 2>nul + if %ERRORLEVEL% equ 0 ( + pwsh -NoProfile -Command "& { [Console]::OutputEncoding = [Text.Encoding]::UTF8; dotnet run --project '%~dp0src\OpenTheBox' -- %* }" + ) else ( + chcp 65001 >nul 2>nul + dotnet run --project "%~dp0src\OpenTheBox" -- %* + ) +) diff --git a/TODO.md b/TODO.md index 5684315..49b0545 100644 --- a/TODO.md +++ b/TODO.md @@ -1,77 +1,12 @@ # TODO -## Temps de jeu toujours à 0 +Tous les items ont été traités. Ce fichier est conservé comme historique. -Attendu: si on est capable de mesure le temps depuis le début de l'aventure: afficher la durée. Sinon, supprimer l'information +## Caractéristiques -## Titre les panneaux colorés avant d'avoir débloqué la couleur +Les caractéristiques (Santé, Mana, Nourriture, Endurance, Sang, Or, Oxygène, Énergie) servent principalement de conditions d'accès dans les aventures Loreline (`hasResource(name, min)`) et de cibles pour les consommables. Actuellement seuls Sang (≥20, Dark Fantasy) et Or (≥30, Contemporain) ont des checks dans les aventures. Les 6 autres n'ont pas encore de conditions d'accès, ce qui rend leur utilité peu claire pour le joueur. -Attendu: Tant que la couleur n'est pas débloquée, les titres des panneaux devraient être blancs - -## Barre de navigation de l'inventaire - -La barre de navigation de l'inventaire `↑↓ Naviguer | Entrée : Utiliser | Échap/Q : Retour` apparait avant d'avoir débloqué le panneau d'inventaire. - -Attendu: elle ne devrait pas être affichée lorsqu'on choisit `2. Voir l'inventaire` tant que le panneau d'inventaire. n'est pas débloquée - -## Choix de consommable d'inventaire avant d'avoir débloqué le panneau d'inventaire - -Attendu: On ne doit pas pouvoir consommer d'objet de l'inventaire avant d'avoir débloqué le panneau d'inventaire. - -## Le panneau d'inventaire est débloqué avant la navigation clavier - -Attendu: Le panneau d'inventaire ne peut pas être débloqué avant la navigation clavier - -## boites meta qui mettent un peu de temps à arriver - -Attendu: les boites meta devraient avoir un "pity" loot de 10 - -## Clignotement du panneau d'inventaire - -Lorsqu'on utilise les flèche pour change l'option dans l'inventaire, toute la fenêtre flash en noir avant de redessiner la nouvelle sélection. - -Attendu: La mise à jour du rendu ne passe pas par un flash noir. Pas d'effet de clignottement. - -## Rendu du panneau d'inventaire avec des symboles non lisibles - -─Inventaire (1-6/41)───────────────────────────────────────┐ -│ ┌──────────────────────────────┬─────┬────────────┬─────┐ │ -│ │ Nom │ │ Rareté │ Qté │ │ -│ ├──────────────────────────────┼─────┼────────────┼─────┤ │ -│ │ Boîte d'aventure contempo. │ BOX │ Commun │ 1 │ │ -│ │ Boîte Méta - L'Interface │ BOX │ Commun │ 1 │ │ -│ │ Boîte pas ouf │ BOX │ Commun │ 6 │ │ -│ │ Boîte ok tiers │ BOX │ Commun │ 1 │ │ -│ │ Fiole de sang │ CSM │ Rare │ 2 │ │ -│ │ Cellule d'énergie │ CSM │ Peu commun │ 1 │ │ -│ │ ??4 ??8 ??1 ??18 ??5 ?5 │ │ │ 41 │ │ -│ └──────────────────────────────┴─────┴────────────┴─────┘ │ -└───────────────────────────────────────────────────────────┘ - -Attendu: utilisation d'abréviation à la place d'icones qui ne sont pas disponibles dans le terminal. - -## Confusion panneau de ressources et panneau statistiques - -┌─Détails──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Panneau de ressources (Rare) │ -│ Affiche tes ressources (santé, mana, etc.). │ -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - -┌─Détails──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ -│ Panneau de statistiques (Rare) │ -│ Affiche les statistiques de ton personnage. │ -└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ - -Santé et mana sont des statistiques de personnage et pas de resources. Mais le panneau de statistique affiche plutôt des statistiques de la partie. Par ailleurs le panneau ressource affiche "Les ressources apparaîtront au fil de tes découvertes...". - -Attendu: Mettre à jour specifications.md. "ressource" devient "characteristique" (en anglais dans le document technique). Vérifier que le joueur peut débloquer les caractéristiques: on débloque des potions de vie et de mana avant d'avoir la caractéristique ? Ou est-ce la potion qui permet de débloquer implicitement la caractéristique ? Clarifier et corriger. - -## Raccourcis claviers - -Attendu: Fustion les fonctionnalités Navigations flèches et raccourcis clavier. Pour des raisons d'accessibilité, ils doivent être la première meta à débloquer. - -Attendu: Le meta se débloquent dans un ordre déterministe. Spécifier l'ordre attendu de déblocage pour la meilleur UX, mettre à jour specifications.md avec ces infos, implémenter le déblocage meta dans l'ordre déterministe. - -## Retour au menu quitte le jeu - -Attendu: Retour au menu ramène au menu principal, depuis lequel on peut quitter le jeu. \ No newline at end of file +Options futures : +1. Ajouter des checks `hasResource()` dans les aventures pour chaque type +2. Ajouter des descriptions dans le panneau Caractéristiques +3. Réduire les caractéristiques visibles en début de jeu diff --git a/content/strings/en.json b/content/strings/en.json index 09368a1..c621729 100644 --- a/content/strings/en.json +++ b/content/strings/en.json @@ -534,5 +534,23 @@ "inventory.equipped": "Equipped", "inventory.not_equipped": "Not equipped", "inventory.crafting_hint": "Used in crafting", - "inventory.recipes_available": "Recipes" + "inventory.recipes_available": "Recipes", + "inventory.material_desc": "Crafting material — {0}", + + "category.box": "Boxes", + "category.key": "Keys", + "category.consumable": "Consumables", + "category.lorefragment": "Lore Fragments", + "category.cosmetic": "Cosmetics", + "category.material": "Materials", + "category.meta": "Upgrades", + "category.adventuretoken": "Adventure", + "category.cookie": "Cookies", + "category.music": "Music", + "category.crafteditem": "Crafted Items", + "category.workstationblueprint": "Blueprints", + "category.badge": "Badges", + "category.map": "Maps", + "category.storyitem": "Story Items", + "category.questitem": "Quest Items" } diff --git a/content/strings/fr.json b/content/strings/fr.json index 0c39f16..507b11e 100644 --- a/content/strings/fr.json +++ b/content/strings/fr.json @@ -534,5 +534,23 @@ "inventory.equipped": "Équipé", "inventory.not_equipped": "Non équipé", "inventory.crafting_hint": "Utilisé en artisanat", - "inventory.recipes_available": "Recettes" + "inventory.recipes_available": "Recettes", + "inventory.material_desc": "Matériau de fabrication — {0}", + + "category.box": "Boîtes", + "category.key": "Clés", + "category.consumable": "Consommables", + "category.lorefragment": "Fragments de Lore", + "category.cosmetic": "Cosmétiques", + "category.material": "Matériaux", + "category.meta": "Améliorations", + "category.adventuretoken": "Aventure", + "category.cookie": "Cookies", + "category.music": "Musique", + "category.crafteditem": "Objets fabriqués", + "category.workstationblueprint": "Plans", + "category.badge": "Badges", + "category.map": "Cartes", + "category.storyitem": "Objets d'histoire", + "category.questitem": "Objets de quête" } diff --git a/src/OpenTheBox/Program.cs b/src/OpenTheBox/Program.cs index dd9921c..342039c 100644 --- a/src/OpenTheBox/Program.cs +++ b/src/OpenTheBox/Program.cs @@ -586,15 +586,7 @@ public static class Program foreach (var (name, rarity, _) in allLoot) AddEventLog($"+ {name} [{_loc.Get($"rarity.{rarity.ToLower()}")}]"); - // Proposal 4: Show inline resource summary when ResourcePanel is not unlocked - if (!_renderContext.HasResourcePanel && _state.Resources.Count > 0) - { - var resSummary = string.Join(" | ", _state.Resources - .Where(r => _state.VisibleResources.Contains(r.Key)) - .Select(r => $"{_loc.Get($"resource.{r.Key.ToString().ToLower()}")} {r.Value.Current}/{r.Value.Max}")); - if (resSummary.Length > 0) - _renderer.ShowMessage($" [{resSummary}]"); - } + // Resource summary removed — characteristics are shown in the dedicated panel } // Show deferred interactions after the loot reveal, with context diff --git a/src/OpenTheBox/Rendering/Panels/InventoryPanel.cs b/src/OpenTheBox/Rendering/Panels/InventoryPanel.cs index fd35703..4cbf329 100644 --- a/src/OpenTheBox/Rendering/Panels/InventoryPanel.cs +++ b/src/OpenTheBox/Rendering/Panels/InventoryPanel.cs @@ -3,6 +3,7 @@ using OpenTheBox.Core.Enums; using OpenTheBox.Core.Items; using OpenTheBox.Data; using OpenTheBox.Localization; +using OpenTheBox.Rendering; using Spectre.Console; using Spectre.Console.Rendering; @@ -100,7 +101,7 @@ public static class InventoryPanel } /// - /// Returns a localized category label. + /// Returns a localized category label (icon or abbreviation). /// private static string LocalizeCategory(ItemCategory cat, LocalizationManager? loc) { @@ -108,6 +109,15 @@ public static class InventoryPanel return CategoryIcon(cat); } + /// + /// Returns the full localized category name (e.g., "Materials", "Cosmetics"). + /// + public static string LocalizeCategoryName(ItemCategory cat, LocalizationManager? loc) + { + string key = $"category.{cat.ToString().ToLower()}"; + return loc?.Get(key) ?? cat.ToString(); + } + /// /// Returns a compact emoji icon for a category. /// @@ -167,7 +177,7 @@ public static class InventoryPanel /// /// Builds a renderable inventory table. - /// uses fewer visible rows for inline layout mode. + /// renders a category summary instead of individual items. /// highlights the selected row (relative to full list, -1 = none). /// public static IRenderable Render( @@ -178,7 +188,13 @@ public static class InventoryPanel bool compact = false, int selectedIndex = -1) { - int maxRows = compact ? CompactVisibleRows : MaxVisibleRows; + string headerText = loc?.Get("ui.inventory") ?? "Inventory"; + + // Compact mode: show category summary instead of item list + if (compact) + return RenderCompactSummary(state, registry, loc, headerText); + + int maxRows = MaxVisibleRows; string colName = loc?.Get("inventory.col.name") ?? "Name"; string colRarity = loc?.Get("inventory.col.rarity") ?? "Rarity"; @@ -204,8 +220,8 @@ public static class InventoryPanel 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) + // Category separator between different groups + if (prevCategory is not null && prevCategory != item.Category) { table.AddEmptyRow(); } @@ -246,19 +262,7 @@ public static class InventoryPanel table.AddRow($"[dim]{Markup.Escape(emptyText)}[/]", "", "", ""); } - // In compact mode, add a summary footer showing count per category - if (compact && totalItems > maxRows) - { - var summary = grouped - .GroupBy(g => g.Category) - .OrderBy(g => CategorySortOrder(g.Key)) - .Select(g => $"{CategoryAbbrev(g.Key)}{g.Count()}") - .ToArray(); - table.AddRow($"[dim]{string.Join(" ", summary)}[/]", "", "", $"[dim]{totalItems}[/]"); - } - // Build header with scroll indicator - string headerText = loc?.Get("ui.inventory") ?? "Inventory"; string header; if (totalItems > maxRows) { @@ -273,7 +277,53 @@ public static class InventoryPanel return new Panel(table) .Header(new PanelHeader(header)) - .Border(BoxBorder.Rounded); + .Border(BoxBorder.Rounded) + .Expand(); + } + + /// + /// Renders a compact category summary for the inline game state view. + /// Shows one row per category with total quantity. + /// + private static IRenderable RenderCompactSummary( + GameState state, + ContentRegistry? registry, + LocalizationManager? loc, + string headerText) + { + var grouped = GetGroupedItems(state, registry); + var categories = grouped + .GroupBy(g => g.Category) + .OrderBy(g => CategorySortOrder(g.Key)) + .ToList(); + + string colCat = loc?.Get("inventory.col.name") ?? "Name"; + string colQty = loc?.Get("inventory.col.qty") ?? "Qty"; + + var table = new Table() + .Border(TableBorder.Rounded) + .AddColumn(new TableColumn($"[bold]{Markup.Escape(colCat)}[/]")) + .AddColumn(new TableColumn($"[bold]{Markup.Escape(colQty)}[/]").RightAligned()); + + foreach (var cat in categories) + { + string catName = LocalizeCategoryName(cat.Key, loc); + int qty = cat.Sum(g => g.TotalQty); + table.AddRow( + $" {Markup.Escape(catName)}", + qty.ToString()); + } + + if (categories.Count == 0) + { + string emptyText = loc?.Get("inventory.empty") ?? "Empty"; + table.AddRow($"[dim]{Markup.Escape(emptyText)}[/]", ""); + } + + return new Panel(table) + .Header($"[bold yellow]{Markup.Escape(headerText)}[/]") + .Border(BoxBorder.Rounded) + .Expand(); } /// @@ -390,7 +440,8 @@ public static class InventoryPanel if (isEquipped) { string equippedLabel = loc?.Get("inventory.equipped") ?? "Equipped"; - rows.Add(new Markup($"[green]✓ {Markup.Escape(equippedLabel)}[/]")); + string checkMark = UnicodeSupport.IsUtf8 ? "✓" : ">"; + rows.Add(new Markup($"[green]{checkMark} {Markup.Escape(equippedLabel)}[/]")); } else { @@ -401,9 +452,9 @@ public static class InventoryPanel 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)})[/]")); + string matDesc = loc?.Get("inventory.material_desc", matForm) ?? $"Crafting material — {matForm}"; + rows.Add(new Markup($"[dim]{Markup.Escape(matDesc)}[/]")); // Show crafting hints for this material if (registry?.Recipes is not null) { @@ -439,7 +490,7 @@ public static class InventoryPanel break; } - string title = loc?.Get("inventory.details") ?? "Details"; + string title = LocalizeCategoryName(item.Category, loc); return new Panel(new Rows(rows)) .Header($"[bold aqua]{Markup.Escape(title)}[/]") .Border(BoxBorder.Rounded) diff --git a/src/OpenTheBox/Simulation/MetaEngine.cs b/src/OpenTheBox/Simulation/MetaEngine.cs index 8430e4e..5c07054 100644 --- a/src/OpenTheBox/Simulation/MetaEngine.cs +++ b/src/OpenTheBox/Simulation/MetaEngine.cs @@ -120,17 +120,21 @@ public class MetaEngine } } - // Music melody: trigger music playback + // Music melody: trigger music playback and consume immediately (ephemeral) if (itemDef.Tags.Contains("Music")) { events.Add(new MusicPlayedEvent()); + state.RemoveItem(item.Id); + events.Add(new ItemConsumedEvent(item.Id)); } - // Fortune cookie: pick a random fortune message + // Fortune cookie: pick a random fortune and consume immediately (ephemeral) if (itemDef.Tags.Contains("Cookie")) { int cookieNumber = Random.Shared.Next(1, 21); events.Add(new CookieFortuneEvent($"cookie.{cookieNumber}")); + state.RemoveItem(item.Id); + events.Add(new ItemConsumedEvent(item.Id)); } }