From 001f682320674334f9f6847b88f1c0f3f1db5bb9 Mon Sep 17 00:00:00 2001 From: Samuel Bouchet Date: Sun, 15 Mar 2026 15:27:02 +0100 Subject: [PATCH] Show unlocked workstations permanently in Atelier panel with idle indicator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Workstations now appear as soon as they are unlocked, not only when a job is active. Idle stations display a waiting-for-ingredients status (⏳/~). Add craft.idle localization key and update empty message to "no workshops unlocked". --- content/strings/en.json | 3 +- content/strings/fr.json | 3 +- .../Rendering/Panels/CraftingPanel.cs | 55 ++++++++++++------- tests/OpenTheBox.Tests/CraftingTests.cs | 9 ++- 4 files changed, 46 insertions(+), 24 deletions(-) diff --git a/content/strings/en.json b/content/strings/en.json index ef69e1c..e79be37 100644 --- a/content/strings/en.json +++ b/content/strings/en.json @@ -395,7 +395,8 @@ "craft.completed": "{0} finished crafting!", "craft.done": "Done", "craft.panel.title": "Workshops", - "craft.panel.empty": "No active workshops.", + "craft.idle": "Waiting for ingredients", + "craft.panel.empty": "No workshops unlocked.", "item.blueprint.foundry": "Foundry Blueprint", "item.blueprint.workbench": "Workbench Blueprint", diff --git a/content/strings/fr.json b/content/strings/fr.json index a94a988..e6dc937 100644 --- a/content/strings/fr.json +++ b/content/strings/fr.json @@ -395,7 +395,8 @@ "craft.completed": "{0} a terminé la fabrication !", "craft.done": "Terminé", "craft.panel.title": "Ateliers", - "craft.panel.empty": "Aucun atelier en activité.", + "craft.idle": "En attente d'ingrédients", + "craft.panel.empty": "Aucun atelier débloqué.", "item.blueprint.foundry": "Plan de Fonderie", "item.blueprint.workbench": "Plan d'Établi", diff --git a/src/OpenTheBox/Rendering/Panels/CraftingPanel.cs b/src/OpenTheBox/Rendering/Panels/CraftingPanel.cs index 795e456..d2c0d73 100644 --- a/src/OpenTheBox/Rendering/Panels/CraftingPanel.cs +++ b/src/OpenTheBox/Rendering/Panels/CraftingPanel.cs @@ -1,5 +1,6 @@ using OpenTheBox.Core; using OpenTheBox.Core.Crafting; +using OpenTheBox.Core.Enums; using OpenTheBox.Data; using OpenTheBox.Localization; using Spectre.Console; @@ -8,46 +9,60 @@ using Spectre.Console.Rendering; namespace OpenTheBox.Rendering.Panels; /// -/// Renders active crafting workstations showing progress bars and completion status. +/// Renders all unlocked crafting workstations: active jobs show progress, +/// idle stations show a waiting indicator. /// public static class CraftingPanel { /// - /// Builds a renderable panel showing all active crafting jobs with their progress. + /// Builds a renderable panel showing all unlocked workstations and their status. /// public static IRenderable Render(GameState state, ContentRegistry? registry = null, LocalizationManager? loc = null) { var rows = new List(); - foreach (var job in state.ActiveCraftingJobs.OrderBy(j => j.StartedAt)) + // Index active jobs by workstation + var jobsByStation = state.ActiveCraftingJobs + .GroupBy(j => j.Workstation) + .ToDictionary(g => g.Key, g => g.OrderBy(j => j.StartedAt).First()); + + // Show all unlocked workstations in stable order + foreach (var station in state.UnlockedWorkstations.OrderBy(w => w.ToString())) { - string name = job.RecipeId; - if (registry is not null && registry.Recipes.TryGetValue(job.RecipeId, out var recipe)) - { - name = loc is not null ? loc.Get(recipe.NameKey) : recipe.NameKey; - } + string stationName = station.ToString(); - string station = job.Workstation.ToString(); - - if (job.IsComplete) + if (jobsByStation.TryGetValue(station, out var job)) { - // Completed: show checkmark - rows.Add(new Markup($" [bold green]✓[/] [yellow]{Markup.Escape(station)}[/]: {Markup.Escape(name)} — [bold green]{Markup.Escape(loc?.Get("craft.done") ?? "Done")}[/]")); + string recipeName = job.RecipeId; + if (registry is not null && registry.Recipes.TryGetValue(job.RecipeId, out var recipe)) + recipeName = loc is not null ? loc.Get(recipe.NameKey) : recipe.NameKey; + + if (job.IsComplete) + { + string check = UnicodeSupport.IsUtf8 ? "✓" : ">"; + rows.Add(new Markup($" [bold green]{check}[/] [yellow]{Markup.Escape(stationName)}[/]: {Markup.Escape(recipeName)} — [bold green]{Markup.Escape(loc?.Get("craft.done") ?? "Done")}[/]")); + } + else + { + int pct = (int)job.ProgressPercent; + int barWidth = 20; + int filled = barWidth * pct / 100; + string bar = new string('#', filled) + new string('-', barWidth - filled); + rows.Add(new Markup($" [yellow]{Markup.Escape(stationName)}[/]: {Markup.Escape(recipeName)} [[{bar}]] {pct}%")); + } } else { - // In progress: show progress bar - int pct = (int)job.ProgressPercent; - int barWidth = 20; - int filled = barWidth * pct / 100; - string bar = new string('#', filled) + new string('-', barWidth - filled); - rows.Add(new Markup($" [yellow]{Markup.Escape(station)}[/]: {Markup.Escape(name)} [[{bar}]] {pct}%")); + // Idle workstation + string idle = UnicodeSupport.IsUtf8 ? "⏳" : "~"; + string idleText = loc?.Get("craft.idle") ?? "Waiting for ingredients"; + rows.Add(new Markup($" [dim]{idle} {Markup.Escape(stationName)}: {Markup.Escape(idleText)}[/]")); } } if (rows.Count == 0) { - rows.Add(new Markup($"[dim]{Markup.Escape(loc?.Get("craft.panel.empty") ?? "No active workshops.")}[/]")); + rows.Add(new Markup($"[dim]{Markup.Escape(loc?.Get("craft.panel.empty") ?? "No workshops unlocked.")}[/]")); } return new Panel(new Rows(rows)) diff --git a/tests/OpenTheBox.Tests/CraftingTests.cs b/tests/OpenTheBox.Tests/CraftingTests.cs index 7ab77b9..b572b1b 100644 --- a/tests/OpenTheBox.Tests/CraftingTests.cs +++ b/tests/OpenTheBox.Tests/CraftingTests.cs @@ -437,14 +437,15 @@ public class CraftingPanelTests var output = RenderHelper.RenderToString(CraftingPanel.Render(state)); Assert.NotEmpty(output); - // Should contain the empty workshop message - Assert.Contains("active", output, StringComparison.OrdinalIgnoreCase); + // Should contain the empty workshop message (no workstations unlocked) + Assert.Contains("unlocked", output, StringComparison.OrdinalIgnoreCase); } [Fact] public void Render_InProgressJob_ShowsProgressBar() { var state = GameState.Create("Test", Locale.EN); + state.UnlockedWorkstations.Add(WorkstationType.Foundry); state.ActiveCraftingJobs.Add(new CraftingJob { Id = Guid.NewGuid(), @@ -466,6 +467,7 @@ public class CraftingPanelTests public void Render_CompletedJob_ShowsDone() { var state = GameState.Create("Test", Locale.EN); + state.UnlockedWorkstations.Add(WorkstationType.Foundry); state.ActiveCraftingJobs.Add(new CraftingJob { Id = Guid.NewGuid(), @@ -492,6 +494,7 @@ public class CraftingPanelTests new RecipeResult("material_wood_refined", 1))); var state = GameState.Create("Test", Locale.EN); + state.UnlockedWorkstations.Add(WorkstationType.Foundry); state.ActiveCraftingJobs.Add(new CraftingJob { Id = Guid.NewGuid(), @@ -513,6 +516,8 @@ public class CraftingPanelTests public void Render_MixedJobs_DoesNotThrow() { var state = GameState.Create("Test", Locale.EN); + state.UnlockedWorkstations.Add(WorkstationType.Foundry); + state.UnlockedWorkstations.Add(WorkstationType.Furnace); state.ActiveCraftingJobs.Add(new CraftingJob { Id = Guid.NewGuid(),