Stop meta box drops when all unlocks obtained, add next-step hints

- BoxEngine: treat meta box chain items as "already obtained" when
  the entire meta unlock sequence is complete, preventing useless
  box_meta_mastery drops that only contain more boxes.
- Add progression hints under completion tracker showing lore fragment
  count, adventure completion count, and a teaser for the final adventure.
This commit is contained in:
Samuel Bouchet 2026-03-15 20:00:47 +01:00
parent a8d2903b94
commit d079faa2e6
6 changed files with 42 additions and 0 deletions

View file

@ -43,6 +43,9 @@
"loot.category": "Category", "loot.category": "Category",
"ui.feature_unlocked": "NEW FEATURE UNLOCKED: {0}", "ui.feature_unlocked": "NEW FEATURE UNLOCKED: {0}",
"ui.completion": "Completion: {0}%", "ui.completion": "Completion: {0}%",
"hint.lore": "Lore Fragments: {0}/{1}",
"hint.adventures": "Adventures completed: {0}/{1}",
"hint.destiny": "Complete all adventures to unlock the final adventure…",
"prompt.what_do": "What do you do?", "prompt.what_do": "What do you do?",
"prompt.invalid_choice": "Please enter a number between 1 and {0}.", "prompt.invalid_choice": "Please enter a number between 1 and {0}.",

View file

@ -43,6 +43,9 @@
"loot.category": "Catégorie", "loot.category": "Catégorie",
"ui.feature_unlocked": "NOUVELLE FONCTIONNALITÉ : {0}", "ui.feature_unlocked": "NOUVELLE FONCTIONNALITÉ : {0}",
"ui.completion": "Complétion : {0}%", "ui.completion": "Complétion : {0}%",
"hint.lore": "Fragments de Lore : {0}/{1}",
"hint.adventures": "Aventures terminées : {0}/{1}",
"hint.destiny": "Termine toutes les aventures pour débloquer l'aventure finale…",
"prompt.what_do": "Que fais-tu ?", "prompt.what_do": "Que fais-tu ?",
"prompt.invalid_choice": "Entre un nombre entre 1 et {0}.", "prompt.invalid_choice": "Entre un nombre entre 1 et {0}.",

View file

@ -1013,6 +1013,27 @@ public static class Program
+ _state.VisibleStats.Count; + _state.VisibleStats.Count;
_renderContext.CompletionPercent = total > 0 ? (int)(unlocked * 100.0 / total) : 0; _renderContext.CompletionPercent = total > 0 ? (int)(unlocked * 100.0 / total) : 0;
// Build next-step hints
var hints = new List<string>();
// Lore fragments
var totalLore = _registry.Items.Values.Count(i => i.Category == ItemCategory.LoreFragment);
var ownedLore = _state.Inventory.Count(i =>
_registry.GetItem(i.DefinitionId)?.Category == ItemCategory.LoreFragment);
if (ownedLore < totalLore)
hints.Add(_loc.Get("hint.lore", ownedLore, totalLore));
// Adventures completed
var completedAdv = _state.CompletedAdventures.Count;
if (completedAdv < totalAdventures)
hints.Add(_loc.Get("hint.adventures", completedAdv, totalAdventures));
// Destiny adventure not yet unlocked — show condition
if (!_state.UnlockedAdventures.Contains(AdventureTheme.Destiny))
hints.Add(_loc.Get("hint.destiny"));
_renderContext.NextHints = hints;
} }
private static string GetUIFeatureLocKey(UIFeature feature) => feature switch private static string GetUIFeatureLocKey(UIFeature feature) => feature switch

View file

@ -40,6 +40,9 @@ public sealed class RenderContext
/// <summary>Completion percentage (0-100), updated before each render call.</summary> /// <summary>Completion percentage (0-100), updated before each render call.</summary>
public int CompletionPercent { get; set; } public int CompletionPercent { get; set; }
/// <summary>Short hints about what to do next, displayed under completion tracker.</summary>
public List<string> NextHints { get; set; } = [];
/// <summary> /// <summary>
/// Builds a <see cref="RenderContext"/> that mirrors the features already unlocked in a /// Builds a <see cref="RenderContext"/> that mirrors the features already unlocked in a
/// <see cref="GameState"/>. /// <see cref="GameState"/>.

View file

@ -497,7 +497,11 @@ public sealed class SpectreRenderer : IRenderer
rightItems.Add(CraftingPanel.Render(state, _registry, _loc)); rightItems.Add(CraftingPanel.Render(state, _registry, _loc));
if (context.HasCompletionTracker) if (context.HasCompletionTracker)
{
rightItems.Add(new Markup($"[bold cyan]{Markup.Escape(_loc.Get("ui.completion", context.CompletionPercent))}[/]")); rightItems.Add(new Markup($"[bold cyan]{Markup.Escape(_loc.Get("ui.completion", context.CompletionPercent))}[/]"));
foreach (var hint in context.NextHints)
rightItems.Add(new Markup($" [dim]{Markup.Escape(hint)}[/]"));
}
IRenderable rightPanel = rightItems.Count > 0 IRenderable rightPanel = rightItems.Count > 0
? new Rows(rightItems) ? new Rows(rightItems)
@ -545,6 +549,10 @@ public sealed class SpectreRenderer : IRenderer
AnsiConsole.Write(CraftingPanel.Render(state, _registry, _loc)); AnsiConsole.Write(CraftingPanel.Render(state, _registry, _loc));
if (context.HasCompletionTracker) if (context.HasCompletionTracker)
{
AnsiConsole.Write(new Rule($"[bold cyan]{Markup.Escape(_loc.Get("ui.completion", context.CompletionPercent))}[/]").RuleStyle("cyan")); AnsiConsole.Write(new Rule($"[bold cyan]{Markup.Escape(_loc.Get("ui.completion", context.CompletionPercent))}[/]").RuleStyle("cyan"));
foreach (var hint in context.NextHints)
AnsiConsole.MarkupLine($" [dim]{Markup.Escape(hint)}[/]");
}
} }
} }

View file

@ -221,6 +221,10 @@ public class BoxEngine(ContentRegistry registry)
if (itemDef.MetaUnlock.HasValue && state.UnlockedUIFeatures.Contains(itemDef.MetaUnlock.Value)) if (itemDef.MetaUnlock.HasValue && state.UnlockedUIFeatures.Contains(itemDef.MetaUnlock.Value))
return true; return true;
// Meta box chain: treat as obtained when the entire chain is exhausted
if (Array.IndexOf(MetaBoxChain, itemDefId) >= 0 && MetaEngine.GetNextMetaItemId(state, registry) is null)
return true;
// Resource visibility already unlocked (only Meta items, not consumables) // Resource visibility already unlocked (only Meta items, not consumables)
if (itemDef.ResourceType.HasValue && itemDef.Category == ItemCategory.Meta if (itemDef.ResourceType.HasValue && itemDef.Category == ItemCategory.Meta
&& state.VisibleResources.Contains(itemDef.ResourceType.Value)) && state.VisibleResources.Contains(itemDef.ResourceType.Value))