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(),