2026-03-10 18:24:01 +01:00
|
|
|
using OpenTheBox.Core;
|
2026-03-11 17:50:37 +01:00
|
|
|
using OpenTheBox.Core.Crafting;
|
2026-03-10 18:24:01 +01:00
|
|
|
using OpenTheBox.Core.Enums;
|
|
|
|
|
using OpenTheBox.Core.Items;
|
|
|
|
|
using OpenTheBox.Data;
|
|
|
|
|
using OpenTheBox.Localization;
|
|
|
|
|
using OpenTheBox.Persistence;
|
|
|
|
|
using OpenTheBox.Rendering;
|
|
|
|
|
using OpenTheBox.Simulation;
|
|
|
|
|
using OpenTheBox.Simulation.Actions;
|
|
|
|
|
using OpenTheBox.Simulation.Events;
|
|
|
|
|
using OpenTheBox.Adventures;
|
|
|
|
|
|
|
|
|
|
namespace OpenTheBox;
|
|
|
|
|
|
|
|
|
|
public static class Program
|
|
|
|
|
{
|
|
|
|
|
private static GameState _state = null!;
|
|
|
|
|
private static ContentRegistry _registry = null!;
|
|
|
|
|
private static LocalizationManager _loc = null!;
|
|
|
|
|
private static SaveManager _saveManager = null!;
|
|
|
|
|
private static GameSimulation _simulation = null!;
|
|
|
|
|
private static RenderContext _renderContext = null!;
|
|
|
|
|
private static IRenderer _renderer = null!;
|
2026-03-11 17:50:37 +01:00
|
|
|
private static CraftingEngine _craftingEngine = null!;
|
2026-03-10 18:24:01 +01:00
|
|
|
private static bool _running = true;
|
|
|
|
|
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
private static readonly string LogFilePath = Path.Combine(
|
|
|
|
|
AppContext.BaseDirectory, "openthebox-error.log");
|
|
|
|
|
|
2026-03-10 18:24:01 +01:00
|
|
|
public static async Task Main(string[] args)
|
|
|
|
|
{
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_saveManager = new SaveManager();
|
|
|
|
|
_loc = new LocalizationManager(Locale.EN);
|
|
|
|
|
_renderContext = new RenderContext();
|
2026-03-11 17:50:37 +01:00
|
|
|
_renderer = RendererFactory.Create(_renderContext, _loc, _registry);
|
2026-03-10 18:24:01 +01:00
|
|
|
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
await MainMenuLoop();
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
LogError(ex);
|
|
|
|
|
Console.ForegroundColor = ConsoleColor.Red;
|
|
|
|
|
Console.WriteLine($"A fatal error occurred. Details have been written to:");
|
|
|
|
|
Console.WriteLine(LogFilePath);
|
|
|
|
|
Console.ResetColor();
|
|
|
|
|
Console.WriteLine("Press any key to exit...");
|
|
|
|
|
Console.ReadKey(intercept: true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void LogError(Exception ex)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var entry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {ex.GetType().Name}: {ex.Message}\n{ex.StackTrace}\n\n";
|
|
|
|
|
File.AppendAllText(LogFilePath, entry);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// If logging itself fails, at least don't hide the original error
|
|
|
|
|
}
|
2026-03-10 18:24:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task MainMenuLoop()
|
|
|
|
|
{
|
2026-03-11 18:33:10 +01:00
|
|
|
// Check for existing saves to determine startup flow
|
|
|
|
|
var existingSaves = _saveManager.ListSlots();
|
|
|
|
|
|
|
|
|
|
if (existingSaves.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
// Saves exist: load locale from the most recent save, skip language prompt
|
|
|
|
|
var mostRecent = existingSaves[0]; // Already sorted by SavedAt descending
|
|
|
|
|
var recentState = _saveManager.Load(mostRecent.Name);
|
|
|
|
|
if (recentState != null)
|
|
|
|
|
{
|
|
|
|
|
_loc.Change(recentState.CurrentLocale);
|
|
|
|
|
_renderer = RendererFactory.Create(_renderContext, _loc, _registry);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// No saves: prompt language first
|
|
|
|
|
_renderer.Clear();
|
|
|
|
|
var langOptions = new List<string> { "English", "Français" };
|
|
|
|
|
int langChoice = _renderer.ShowSelection("Language / Langue", langOptions);
|
|
|
|
|
var selectedLocale = langChoice == 0 ? Locale.EN : Locale.FR;
|
|
|
|
|
_loc.Change(selectedLocale);
|
|
|
|
|
_renderer = RendererFactory.Create(_renderContext, _loc, _registry);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 18:24:01 +01:00
|
|
|
while (_running)
|
|
|
|
|
{
|
|
|
|
|
_renderer.Clear();
|
|
|
|
|
_renderer.ShowMessage("========================================");
|
|
|
|
|
_renderer.ShowMessage(" OPEN THE BOX");
|
|
|
|
|
_renderer.ShowMessage("========================================");
|
|
|
|
|
_renderer.ShowMessage("");
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("game.subtitle"));
|
|
|
|
|
_renderer.ShowMessage("");
|
|
|
|
|
|
2026-03-11 18:33:10 +01:00
|
|
|
// Rebuild saves list (may have changed after new game / save)
|
|
|
|
|
existingSaves = _saveManager.ListSlots();
|
|
|
|
|
|
|
|
|
|
var options = new List<string>();
|
|
|
|
|
var actions = new List<string>();
|
|
|
|
|
|
|
|
|
|
// If saves exist, show "Continuer" as first option with most recent save info
|
|
|
|
|
if (existingSaves.Count > 0)
|
2026-03-10 18:24:01 +01:00
|
|
|
{
|
2026-03-11 18:33:10 +01:00
|
|
|
var recent = existingSaves[0];
|
|
|
|
|
var savedAt = recent.SavedAt.ToLocalTime();
|
|
|
|
|
options.Add($"{_loc.Get("menu.continue")} ({recent.Name} {savedAt:dd/MM/yyyy HH:mm})");
|
|
|
|
|
actions.Add("continue");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
options.Add(_loc.Get("menu.new_game"));
|
|
|
|
|
actions.Add("new_game");
|
|
|
|
|
|
|
|
|
|
if (existingSaves.Count > 1)
|
|
|
|
|
{
|
|
|
|
|
options.Add(_loc.Get("menu.load_game"));
|
|
|
|
|
actions.Add("load_game");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
options.Add(_loc.Get("menu.language"));
|
|
|
|
|
actions.Add("language");
|
|
|
|
|
|
|
|
|
|
options.Add(_loc.Get("menu.quit"));
|
|
|
|
|
actions.Add("quit");
|
2026-03-10 18:24:01 +01:00
|
|
|
|
|
|
|
|
int choice = _renderer.ShowSelection("", options);
|
|
|
|
|
|
2026-03-11 18:33:10 +01:00
|
|
|
switch (actions[choice])
|
2026-03-10 18:24:01 +01:00
|
|
|
{
|
2026-03-11 18:33:10 +01:00
|
|
|
case "continue":
|
|
|
|
|
await ContinueGame(existingSaves[0].Name);
|
|
|
|
|
break;
|
|
|
|
|
case "new_game":
|
|
|
|
|
await NewGame();
|
|
|
|
|
break;
|
|
|
|
|
case "load_game":
|
|
|
|
|
await LoadGame();
|
|
|
|
|
break;
|
|
|
|
|
case "language":
|
|
|
|
|
ChangeLanguage();
|
|
|
|
|
break;
|
|
|
|
|
case "quit":
|
|
|
|
|
_running = false;
|
|
|
|
|
break;
|
2026-03-10 18:24:01 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 18:33:10 +01:00
|
|
|
private static async Task ContinueGame(string slotName)
|
|
|
|
|
{
|
|
|
|
|
var loaded = _saveManager.Load(slotName);
|
|
|
|
|
if (loaded == null)
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowError("Failed to load save.");
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_state = loaded;
|
|
|
|
|
_loc.Change(_state.CurrentLocale);
|
|
|
|
|
InitializeGame();
|
|
|
|
|
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("misc.welcome_back", _state.PlayerName));
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
|
|
|
|
|
await GameLoop();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 18:24:01 +01:00
|
|
|
private static async Task NewGame()
|
|
|
|
|
{
|
|
|
|
|
string name = _renderer.ShowTextInput(_loc.Get("prompt.name"));
|
|
|
|
|
if (string.IsNullOrWhiteSpace(name)) name = "BoxOpener";
|
|
|
|
|
|
|
|
|
|
_state = GameState.Create(name, _loc.CurrentLocale);
|
|
|
|
|
InitializeGame();
|
|
|
|
|
|
|
|
|
|
var starterBox = ItemInstance.Create("box_starter");
|
|
|
|
|
_state.AddItem(starterBox);
|
|
|
|
|
|
|
|
|
|
_renderer.ShowMessage("");
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
_renderer.ShowMessage(_loc.Get("misc.welcome", name));
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderer.ShowMessage(_loc.Get("box.starter.desc"));
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
|
|
|
|
|
await GameLoop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task LoadGame()
|
|
|
|
|
{
|
|
|
|
|
var slots = _saveManager.ListSlots();
|
|
|
|
|
if (slots.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("save.no_saves"));
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var options = slots.Select(s => $"{s.Name} ({s.SavedAt:yyyy-MM-dd HH:mm})").ToList();
|
|
|
|
|
options.Add(_loc.Get("menu.back"));
|
|
|
|
|
|
|
|
|
|
int choice = _renderer.ShowSelection(_loc.Get("save.choose_slot"), options);
|
|
|
|
|
if (choice >= slots.Count) return;
|
|
|
|
|
|
|
|
|
|
var loaded = _saveManager.Load(slots[choice].Name);
|
|
|
|
|
if (loaded == null)
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowError("Failed to load save.");
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_state = loaded;
|
|
|
|
|
_loc.Change(_state.CurrentLocale);
|
|
|
|
|
InitializeGame();
|
|
|
|
|
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("misc.welcome_back", _state.PlayerName));
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
|
|
|
|
|
await GameLoop();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void InitializeGame()
|
|
|
|
|
{
|
|
|
|
|
_registry = ContentRegistry.LoadFromFiles(
|
|
|
|
|
"content/data/items.json",
|
|
|
|
|
"content/data/boxes.json",
|
2026-03-11 17:50:37 +01:00
|
|
|
"content/data/interactions.json",
|
|
|
|
|
"content/data/recipes.json"
|
2026-03-10 18:24:01 +01:00
|
|
|
);
|
|
|
|
|
_simulation = new GameSimulation(_registry);
|
2026-03-11 17:50:37 +01:00
|
|
|
_craftingEngine = new CraftingEngine();
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderContext = RenderContext.FromGameState(_state);
|
2026-03-11 17:50:37 +01:00
|
|
|
_renderer = RendererFactory.Create(_renderContext, _loc, _registry);
|
2026-03-10 18:24:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ChangeLanguage()
|
|
|
|
|
{
|
|
|
|
|
var options = new List<string> { "English", "Francais" };
|
|
|
|
|
int choice = _renderer.ShowSelection(_loc.Get("menu.language"), options);
|
|
|
|
|
|
|
|
|
|
var newLocale = choice == 0 ? Locale.EN : Locale.FR;
|
|
|
|
|
_loc.Change(newLocale);
|
|
|
|
|
|
|
|
|
|
if (_state != null)
|
|
|
|
|
_state.CurrentLocale = newLocale;
|
|
|
|
|
|
2026-03-11 17:50:37 +01:00
|
|
|
_renderer = RendererFactory.Create(_renderContext, _loc, _registry);
|
2026-03-10 18:24:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task GameLoop()
|
|
|
|
|
{
|
|
|
|
|
while (_running)
|
|
|
|
|
{
|
2026-03-11 18:33:10 +01:00
|
|
|
// Auto-save when returning to the hub (if the feature is unlocked)
|
|
|
|
|
if (_state.HasUIFeature(UIFeature.AutoSave))
|
|
|
|
|
{
|
|
|
|
|
_saveManager.Save(_state, _state.PlayerName);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 17:50:37 +01:00
|
|
|
// Tick crafting jobs (InProgress → Completed)
|
|
|
|
|
TickCraftingJobs();
|
|
|
|
|
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderer.Clear();
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
UpdateCompletionPercent();
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderer.ShowGameState(_state, _renderContext);
|
|
|
|
|
|
|
|
|
|
var actions = BuildActionList();
|
|
|
|
|
if (actions.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("error.no_boxes"));
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int choice = _renderer.ShowSelection(
|
|
|
|
|
_loc.Get("prompt.choose_action"),
|
|
|
|
|
actions.Select(a => a.label).ToList());
|
|
|
|
|
|
|
|
|
|
await ExecuteAction(actions[choice].action);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static List<(string label, string action)> BuildActionList()
|
|
|
|
|
{
|
|
|
|
|
var actions = new List<(string label, string action)>();
|
|
|
|
|
|
|
|
|
|
var boxes = _state.Inventory.Where(i => _registry.IsBox(i.DefinitionId)).ToList();
|
|
|
|
|
if (boxes.Count > 0)
|
|
|
|
|
actions.Add((_loc.Get("action.open_box") + $" ({boxes.Count})", "open_box"));
|
|
|
|
|
|
|
|
|
|
if (_state.Inventory.Count > 0)
|
|
|
|
|
actions.Add((_loc.Get("action.inventory"), "inventory"));
|
|
|
|
|
|
|
|
|
|
if (_state.UnlockedAdventures.Count > 0)
|
|
|
|
|
actions.Add((_loc.Get("action.adventure"), "adventure"));
|
|
|
|
|
|
|
|
|
|
if (_state.UnlockedCosmetics.Count > 0)
|
|
|
|
|
actions.Add((_loc.Get("action.appearance"), "appearance"));
|
|
|
|
|
|
2026-03-11 17:50:37 +01:00
|
|
|
var completedJobs = _state.ActiveCraftingJobs.Where(j => j.IsComplete).ToList();
|
|
|
|
|
if (completedJobs.Count > 0)
|
|
|
|
|
actions.Add((_loc.Get("action.collect_crafting") + $" ({completedJobs.Count})", "collect_crafting"));
|
|
|
|
|
|
2026-03-11 18:33:10 +01:00
|
|
|
if (!_state.HasUIFeature(UIFeature.AutoSave))
|
|
|
|
|
actions.Add((_loc.Get("action.save"), "save"));
|
|
|
|
|
|
2026-03-10 18:24:01 +01:00
|
|
|
actions.Add((_loc.Get("action.quit"), "quit"));
|
|
|
|
|
|
|
|
|
|
return actions;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task ExecuteAction(string action)
|
|
|
|
|
{
|
|
|
|
|
switch (action)
|
|
|
|
|
{
|
|
|
|
|
case "open_box": await OpenBoxAction(); break;
|
|
|
|
|
case "inventory": ShowInventory(); break;
|
|
|
|
|
case "adventure": await StartAdventure(); break;
|
|
|
|
|
case "appearance": ChangeAppearance(); break;
|
2026-03-11 17:50:37 +01:00
|
|
|
case "collect_crafting": await CollectCrafting(); break;
|
2026-03-10 18:24:01 +01:00
|
|
|
case "save": SaveGame(); break;
|
|
|
|
|
case "quit": _running = false; break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task OpenBoxAction()
|
|
|
|
|
{
|
|
|
|
|
var boxes = _state.Inventory.Where(i => _registry.IsBox(i.DefinitionId)).ToList();
|
|
|
|
|
if (boxes.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("box.no_boxes"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 20:01:11 +01:00
|
|
|
// Group boxes by type so the list isn't bloated with duplicates
|
|
|
|
|
var grouped = boxes.GroupBy(b => b.DefinitionId).ToList();
|
|
|
|
|
var boxNames = grouped.Select(g =>
|
|
|
|
|
{
|
|
|
|
|
var name = GetLocalizedName(g.Key);
|
|
|
|
|
return g.Count() > 1 ? $"{name} (x{g.Count()})" : name;
|
|
|
|
|
}).ToList();
|
2026-03-10 18:24:01 +01:00
|
|
|
boxNames.Add(_loc.Get("menu.back"));
|
|
|
|
|
|
|
|
|
|
int choice = _renderer.ShowSelection(_loc.Get("prompt.choose_box"), boxNames);
|
2026-03-10 20:01:11 +01:00
|
|
|
if (choice >= grouped.Count) return;
|
2026-03-10 18:24:01 +01:00
|
|
|
|
2026-03-10 20:01:11 +01:00
|
|
|
var boxInstance = grouped[choice].First();
|
2026-03-10 18:24:01 +01:00
|
|
|
|
|
|
|
|
var openAction = new OpenBoxAction(boxInstance.Id)
|
|
|
|
|
{
|
|
|
|
|
BoxDefinitionId = boxInstance.DefinitionId
|
|
|
|
|
};
|
|
|
|
|
var events = _simulation.ProcessAction(openAction, _state);
|
|
|
|
|
|
|
|
|
|
await RenderEvents(events);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task RenderEvents(List<GameEvent> events)
|
|
|
|
|
{
|
2026-03-10 20:34:35 +01:00
|
|
|
// Collect IDs of items that are auto-consumed (auto-opening boxes)
|
|
|
|
|
// so we don't show "You received" for items the player never actually gets
|
|
|
|
|
var autoConsumedIds = events.OfType<ItemConsumedEvent>().Select(e => e.InstanceId).ToHashSet();
|
|
|
|
|
|
2026-03-11 18:33:10 +01:00
|
|
|
// Collect all received items to show as a single grouped loot reveal
|
|
|
|
|
var allLoot = new List<(string name, string rarity, string category)>();
|
|
|
|
|
|
|
|
|
|
// Show only the primary box opening (not auto-opened intermediaries)
|
|
|
|
|
bool primaryBoxShown = false;
|
|
|
|
|
|
2026-03-10 18:24:01 +01:00
|
|
|
foreach (var evt in events)
|
|
|
|
|
{
|
|
|
|
|
switch (evt)
|
|
|
|
|
{
|
|
|
|
|
case BoxOpenedEvent boxEvt:
|
|
|
|
|
var boxDef = _registry.GetBox(boxEvt.BoxId);
|
2026-03-10 20:34:35 +01:00
|
|
|
var boxName = _loc.Get(boxDef?.NameKey ?? boxEvt.BoxId);
|
2026-03-11 18:33:10 +01:00
|
|
|
if (!boxEvt.IsAutoOpen && !primaryBoxShown)
|
2026-03-10 20:34:35 +01:00
|
|
|
{
|
|
|
|
|
_renderer.ShowBoxOpening(boxName, boxDef?.Rarity.ToString() ?? "Common");
|
2026-03-11 18:33:10 +01:00
|
|
|
primaryBoxShown = true;
|
2026-03-10 20:34:35 +01:00
|
|
|
}
|
2026-03-11 18:33:10 +01:00
|
|
|
// Auto-opened boxes are silent — their loot appears in the grouped reveal
|
2026-03-10 18:24:01 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ItemReceivedEvent itemEvt:
|
2026-03-10 20:34:35 +01:00
|
|
|
// Skip loot reveal for auto-consumed items (auto-opening boxes)
|
|
|
|
|
if (autoConsumedIds.Contains(itemEvt.Item.Id))
|
|
|
|
|
break;
|
2026-03-10 18:24:01 +01:00
|
|
|
var itemDef = _registry.GetItem(itemEvt.Item.DefinitionId);
|
2026-03-10 19:29:31 +01:00
|
|
|
var itemBoxDef = itemDef is null ? _registry.GetBox(itemEvt.Item.DefinitionId) : null;
|
2026-03-11 18:33:10 +01:00
|
|
|
allLoot.Add((
|
|
|
|
|
GetLocalizedName(itemEvt.Item.DefinitionId),
|
|
|
|
|
(itemDef?.Rarity ?? itemBoxDef?.Rarity ?? ItemRarity.Common).ToString(),
|
|
|
|
|
(itemDef?.Category ?? ItemCategory.Box).ToString()
|
|
|
|
|
));
|
2026-03-10 18:24:01 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case UIFeatureUnlockedEvent uiEvt:
|
|
|
|
|
_renderContext.Unlock(uiEvt.Feature);
|
2026-03-11 17:50:37 +01:00
|
|
|
_renderer = RendererFactory.Create(_renderContext, _loc, _registry);
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderer.ShowUIFeatureUnlocked(
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
_loc.Get(GetUIFeatureLocKey(uiEvt.Feature)));
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case InteractionTriggeredEvent interEvt:
|
|
|
|
|
_renderer.ShowInteraction(_loc.Get(interEvt.DescriptionKey));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ResourceChangedEvent resEvt:
|
|
|
|
|
var resName = _loc.Get($"resource.{resEvt.Type.ToString().ToLower()}");
|
|
|
|
|
_renderer.ShowMessage($"{resName}: {resEvt.OldValue} -> {resEvt.NewValue}");
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case MessageEvent msgEvt:
|
|
|
|
|
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ChoiceRequiredEvent choiceEvt:
|
|
|
|
|
_renderer.ShowSelection(_loc.Get(choiceEvt.Prompt), choiceEvt.Options);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case LootTableModifiedEvent:
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("interaction.key_no_match"));
|
|
|
|
|
break;
|
|
|
|
|
|
2026-03-11 20:47:20 +01:00
|
|
|
case AdventureUnlockedEvent advUnlockedEvt:
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("adventure.unlocked", advUnlockedEvt.Theme.ToString()));
|
|
|
|
|
break;
|
|
|
|
|
|
2026-03-10 18:24:01 +01:00
|
|
|
case AdventureStartedEvent advEvt:
|
|
|
|
|
await RunAdventure(advEvt.Theme);
|
|
|
|
|
break;
|
Add complete content: recipes, French translations, music/cookie events
- Add 34 crafting recipes (materials, consumables, cosmetics, adventures, boxes)
- Add French translations for all 8 remaining adventures
- Add missing item definitions (resource_max_*, music_melody, cookie_fortune)
- Add MusicPlayedEvent and CookieFortuneEvent for special box types
- Make box_story, box_cookie, box_music reachable from box_of_boxes
- Fix content file copying in csproj (None -> Content)
- Add global.json pinning .NET 10 SDK
- Add graceful Console.Clear() error handling
- Add recipe localization keys (EN/FR)
- Add adventure localization keys (EN/FR)
2026-03-10 18:45:54 +01:00
|
|
|
|
|
|
|
|
case MusicPlayedEvent:
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("box.music.desc"));
|
|
|
|
|
if (OperatingSystem.IsWindows()) PlayMelody();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CookieFortuneEvent cookieEvt:
|
|
|
|
|
_renderer.ShowMessage("--- Fortune Cookie ---");
|
|
|
|
|
_renderer.ShowMessage(_loc.Get(cookieEvt.MessageKey));
|
|
|
|
|
_renderer.ShowMessage("----------------------");
|
|
|
|
|
break;
|
2026-03-11 17:50:37 +01:00
|
|
|
|
|
|
|
|
case CraftingStartedEvent craftEvt:
|
|
|
|
|
var recipeName = _registry.Recipes.TryGetValue(craftEvt.RecipeId, out var recDef)
|
|
|
|
|
? _loc.Get(recDef.NameKey)
|
|
|
|
|
: craftEvt.RecipeId;
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("craft.started", recipeName, craftEvt.Workstation.ToString()));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CraftingCompletedEvent craftDoneEvt:
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("craft.completed", craftDoneEvt.Workstation.ToString()));
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case CraftingCollectedEvent:
|
|
|
|
|
// Item reveals are already handled by individual ItemReceivedEvents
|
|
|
|
|
break;
|
2026-03-10 18:24:01 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 18:33:10 +01:00
|
|
|
// Show all received loot as a single grouped reveal
|
|
|
|
|
if (allLoot.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowLootReveal(allLoot);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ShowInventory()
|
|
|
|
|
{
|
|
|
|
|
_renderer.Clear();
|
|
|
|
|
if (_state.Inventory.Count == 0)
|
|
|
|
|
{
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
_renderer.ShowMessage(_loc.Get("inventory.empty"));
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var grouped = _state.Inventory
|
|
|
|
|
.GroupBy(i => i.DefinitionId)
|
|
|
|
|
.Select(g =>
|
|
|
|
|
{
|
|
|
|
|
var def = _registry.GetItem(g.Key);
|
2026-03-10 19:29:31 +01:00
|
|
|
var bDef = def is null ? _registry.GetBox(g.Key) : null;
|
2026-03-10 20:01:11 +01:00
|
|
|
var baseName = GetLocalizedName(g.Key);
|
2026-03-10 18:24:01 +01:00
|
|
|
return (
|
2026-03-10 20:01:11 +01:00
|
|
|
name: g.Count() > 1 ? $"{baseName} (x{g.Count()})" : baseName,
|
2026-03-10 19:29:31 +01:00
|
|
|
rarity: (def?.Rarity ?? bDef?.Rarity ?? ItemRarity.Common).ToString(),
|
2026-03-10 18:24:01 +01:00
|
|
|
category: (def?.Category ?? ItemCategory.Box).ToString()
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
.OrderBy(i => i.category)
|
|
|
|
|
.ThenBy(i => i.name)
|
|
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
_renderer.ShowLootReveal(grouped);
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task StartAdventure()
|
|
|
|
|
{
|
|
|
|
|
var available = _state.UnlockedAdventures.ToList();
|
|
|
|
|
if (available.Count == 0)
|
|
|
|
|
{
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
_renderer.ShowMessage(_loc.Get("adventure.none_available"));
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var options = available.Select(a =>
|
|
|
|
|
{
|
|
|
|
|
bool completed = _state.CompletedAdventures.Contains(a.ToString());
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
string prefix = completed ? $"[{_loc.Get("adventure.done")}] " : "";
|
|
|
|
|
return prefix + a.ToString();
|
2026-03-10 18:24:01 +01:00
|
|
|
}).ToList();
|
|
|
|
|
options.Add(_loc.Get("menu.back"));
|
|
|
|
|
|
|
|
|
|
int choice = _renderer.ShowSelection(_loc.Get("action.adventure"), options);
|
|
|
|
|
if (choice >= available.Count) return;
|
|
|
|
|
|
|
|
|
|
await RunAdventure(available[choice]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task RunAdventure(AdventureTheme theme)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var adventureEngine = new AdventureEngine(_renderer, _loc);
|
|
|
|
|
var events = await adventureEngine.PlayAdventure(theme, _state);
|
|
|
|
|
|
|
|
|
|
foreach (var evt in events)
|
|
|
|
|
{
|
|
|
|
|
if (evt.Kind == GameEventKind.ItemGranted)
|
|
|
|
|
_state.AddItem(ItemInstance.Create(evt.TargetId, evt.Amount));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("adventure.completed"));
|
|
|
|
|
}
|
|
|
|
|
catch (FileNotFoundException)
|
|
|
|
|
{
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
_renderer.ShowMessage(_loc.Get("adventure.coming_soon", theme.ToString()));
|
2026-03-10 18:24:01 +01:00
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowError($"Adventure error: {ex.Message}");
|
|
|
|
|
}
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void ChangeAppearance()
|
|
|
|
|
{
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
// Deduplicate cosmetics by definition ID (inventory may have multiple instances)
|
2026-03-10 18:24:01 +01:00
|
|
|
var cosmeticItems = _state.Inventory
|
|
|
|
|
.Where(i =>
|
|
|
|
|
{
|
|
|
|
|
var def = _registry.GetItem(i.DefinitionId);
|
|
|
|
|
return def?.Category == ItemCategory.Cosmetic && def.CosmeticSlot.HasValue;
|
|
|
|
|
})
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
.GroupBy(i => i.DefinitionId)
|
|
|
|
|
.Select(g => g.First())
|
2026-03-10 18:24:01 +01:00
|
|
|
.ToList();
|
|
|
|
|
|
|
|
|
|
if (cosmeticItems.Count == 0)
|
|
|
|
|
{
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
_renderer.ShowMessage(_loc.Get("cosmetic.no_cosmetics"));
|
2026-03-10 18:24:01 +01:00
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var options = cosmeticItems.Select(i =>
|
|
|
|
|
{
|
|
|
|
|
var def = _registry.GetItem(i.DefinitionId);
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
var slotKey = $"cosmetic.slot.{def?.CosmeticSlot?.ToString().ToLower()}";
|
|
|
|
|
var slotName = _loc.Get(slotKey);
|
|
|
|
|
return $"[{slotName}] {GetLocalizedName(i.DefinitionId)}";
|
2026-03-10 18:24:01 +01:00
|
|
|
}).ToList();
|
|
|
|
|
options.Add(_loc.Get("menu.back"));
|
|
|
|
|
|
|
|
|
|
int choice = _renderer.ShowSelection(_loc.Get("action.appearance"), options);
|
|
|
|
|
if (choice >= cosmeticItems.Count) return;
|
|
|
|
|
|
|
|
|
|
var action = new EquipCosmeticAction(cosmeticItems[choice].Id);
|
|
|
|
|
var events = _simulation.ProcessAction(action, _state);
|
|
|
|
|
foreach (var evt in events)
|
|
|
|
|
{
|
|
|
|
|
if (evt is CosmeticEquippedEvent cosEvt)
|
Fix adventure parsing, add French accents, fix cosmetic translation bug
- Fix Loreline parsing: escape quotes in dialogue, remove [if] bracket
syntax, remove # in text conflicting with tags
- Add French accents to all 9 .fr.lor translation files (hundreds of fixes)
- Fix cosmetic equip display: use item nameKey lookup instead of
constructing key from cosmeticValue (fixes StardustLegendary MISSING)
- Deduplicate cosmetics in appearance menu
- Localize all hardcoded strings (welcome, inventory, adventure, cosmetic)
- Add new tests: Loreline parsing (19), cosmetic slot keys, slot+value
uniqueness (302 total, 0 failures)
2026-03-11 20:40:07 +01:00
|
|
|
{
|
|
|
|
|
var slotKey = $"cosmetic.slot.{cosEvt.Slot.ToString().ToLower()}";
|
|
|
|
|
var slotName = _loc.Get(slotKey);
|
|
|
|
|
// Look up the cosmetic item definition to get its nameKey
|
|
|
|
|
var cosmeticDef = _registry.Items.Values.FirstOrDefault(
|
|
|
|
|
d => d.CosmeticSlot == cosEvt.Slot &&
|
|
|
|
|
string.Equals(d.CosmeticValue, cosEvt.NewValue, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
var valueName = cosmeticDef is not null
|
|
|
|
|
? _loc.Get(cosmeticDef.NameKey)
|
|
|
|
|
: cosEvt.NewValue;
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("cosmetic.equipped", slotName, valueName));
|
|
|
|
|
}
|
2026-03-10 18:24:01 +01:00
|
|
|
else if (evt is MessageEvent msgEvt)
|
|
|
|
|
_renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? []));
|
|
|
|
|
}
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 17:50:37 +01:00
|
|
|
private static void TickCraftingJobs()
|
|
|
|
|
{
|
|
|
|
|
if (_craftingEngine is null) return;
|
|
|
|
|
var events = _craftingEngine.TickJobs(_state);
|
|
|
|
|
// Completed jobs will be shown in the CraftingPanel; no blocking render here.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static async Task CollectCrafting()
|
|
|
|
|
{
|
|
|
|
|
var events = _craftingEngine.CollectCompleted(_state, _registry);
|
|
|
|
|
|
|
|
|
|
// Run meta pass on newly crafted items (some results may unlock features)
|
|
|
|
|
var newItems = events.OfType<ItemReceivedEvent>().Select(e => e.Item).ToList();
|
|
|
|
|
var metaEngine = new MetaEngine();
|
|
|
|
|
events.AddRange(metaEngine.ProcessNewItems(newItems, _state, _registry));
|
|
|
|
|
|
|
|
|
|
// Cascade: collected results may be ingredients for other recipes
|
|
|
|
|
events.AddRange(_craftingEngine.AutoCraftCheck(_state, _registry));
|
|
|
|
|
|
|
|
|
|
await RenderEvents(events);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 18:24:01 +01:00
|
|
|
private static void SaveGame()
|
|
|
|
|
{
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("save.saving"));
|
|
|
|
|
_saveManager.Save(_state, _state.PlayerName);
|
|
|
|
|
_renderer.ShowMessage(_loc.Get("save.saved", _state.PlayerName));
|
|
|
|
|
_renderer.WaitForKeyPress(_loc.Get("prompt.press_key"));
|
|
|
|
|
}
|
Add complete content: recipes, French translations, music/cookie events
- Add 34 crafting recipes (materials, consumables, cosmetics, adventures, boxes)
- Add French translations for all 8 remaining adventures
- Add missing item definitions (resource_max_*, music_melody, cookie_fortune)
- Add MusicPlayedEvent and CookieFortuneEvent for special box types
- Make box_story, box_cookie, box_music reachable from box_of_boxes
- Fix content file copying in csproj (None -> Content)
- Add global.json pinning .NET 10 SDK
- Add graceful Console.Clear() error handling
- Add recipe localization keys (EN/FR)
- Add adventure localization keys (EN/FR)
2026-03-10 18:45:54 +01:00
|
|
|
|
2026-03-10 19:29:31 +01:00
|
|
|
/// <summary>
|
|
|
|
|
/// Resolves the localized display name for any definition ID (item or box).
|
|
|
|
|
/// </summary>
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
private static void UpdateCompletionPercent()
|
|
|
|
|
{
|
|
|
|
|
if (!_renderContext.HasCompletionTracker) return;
|
|
|
|
|
|
|
|
|
|
var totalCosmetics = _registry.Items.Values.Count(i => i.CosmeticSlot.HasValue);
|
|
|
|
|
var totalAdventures = Enum.GetValues<AdventureTheme>().Length;
|
|
|
|
|
var totalUIFeatures = Enum.GetValues<UIFeature>().Length;
|
|
|
|
|
var totalResources = Enum.GetValues<ResourceType>().Length;
|
|
|
|
|
var totalStats = Enum.GetValues<StatType>().Length;
|
|
|
|
|
var totalFonts = Enum.GetValues<FontStyle>().Length;
|
|
|
|
|
|
|
|
|
|
var total = totalCosmetics + totalAdventures + totalUIFeatures
|
|
|
|
|
+ totalResources + totalStats + totalFonts;
|
|
|
|
|
|
|
|
|
|
var unlocked = _state.UnlockedCosmetics.Count
|
|
|
|
|
+ _state.UnlockedAdventures.Count
|
|
|
|
|
+ _state.UnlockedUIFeatures.Count
|
|
|
|
|
+ _state.VisibleResources.Count
|
|
|
|
|
+ _state.VisibleStats.Count
|
|
|
|
|
+ _state.AvailableFonts.Count;
|
|
|
|
|
|
|
|
|
|
_renderContext.CompletionPercent = total > 0 ? (int)(unlocked * 100.0 / total) : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetUIFeatureLocKey(UIFeature feature) => feature switch
|
|
|
|
|
{
|
|
|
|
|
UIFeature.TextColors => "meta.colors",
|
|
|
|
|
UIFeature.ExtendedColors => "meta.extended_colors",
|
|
|
|
|
UIFeature.ArrowKeySelection => "meta.arrows",
|
|
|
|
|
UIFeature.InventoryPanel => "meta.inventory",
|
|
|
|
|
UIFeature.ResourcePanel => "meta.resources",
|
|
|
|
|
UIFeature.StatsPanel => "meta.stats",
|
|
|
|
|
UIFeature.PortraitPanel => "meta.portrait",
|
|
|
|
|
UIFeature.ChatPanel => "meta.chat",
|
|
|
|
|
UIFeature.FullLayout => "meta.layout",
|
|
|
|
|
UIFeature.KeyboardShortcuts => "meta.shortcuts",
|
|
|
|
|
UIFeature.BoxAnimation => "meta.animation",
|
|
|
|
|
UIFeature.CraftingPanel => "meta.crafting",
|
|
|
|
|
UIFeature.CompletionTracker => "meta.completion",
|
2026-03-11 18:33:10 +01:00
|
|
|
UIFeature.AutoSave => "meta.autosave",
|
Fix bugs, add endgame/completion features, adventure dedup, and comprehensive renderer tests
Bug fixes:
- Fix double "NEW FEATURE UNLOCKED" message and broken enum-to-key mapping for all UIFeatures
- Fix Spectre markup crash when opening inventory with colors unlocked (unescaped rarity brackets)
- Fix latent Spectre markup crash in ResourcePanel (unescaped bar brackets)
- Fix WeightedRandom.PickMultiple picking with replacement causing duplicate drops
- Fix double AddItem bug (BoxEngine + RenderEvents both adding to state)
- Add StatType and FontStyle fields to ItemDefinition (were in JSON but missing from C# record)
New features:
- Endgame box (Mythic) that appears when all 8 resources are discovered, contains Crown of Completion
- Completion percentage tracker as mid-game meta unlock (tier 3), shown in both renderers
- Adventure pool depletion: adventure sub-boxes with already-unlocked themes are removed from loot pool
- Global error handling with log file output (openthebox-error.log)
- Tiered meta progression: 5 sequential meta boxes replacing single box_meta
New tests (180 new, 228 total):
- 5 panel rendering test classes covering all enum values (Portrait, Resource, Stats, Inventory, Chat)
- RenderContext and RendererFactory logic tests
- SpectreRenderer output tests across 4 context configurations
- BasicRenderer output and input method tests
- Simulation state mutation and full-run completion tests
2026-03-11 09:34:30 +01:00
|
|
|
_ => $"meta.{feature.ToString().ToLower()}"
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-10 19:29:31 +01:00
|
|
|
private static string GetLocalizedName(string definitionId)
|
|
|
|
|
{
|
|
|
|
|
var itemDef = _registry.GetItem(definitionId);
|
|
|
|
|
if (itemDef is not null)
|
|
|
|
|
return _loc.Get(itemDef.NameKey);
|
|
|
|
|
|
|
|
|
|
var boxDef = _registry.GetBox(definitionId);
|
|
|
|
|
if (boxDef is not null)
|
|
|
|
|
return _loc.Get(boxDef.NameKey);
|
|
|
|
|
|
|
|
|
|
return _loc.Get(definitionId);
|
|
|
|
|
}
|
|
|
|
|
|
Add complete content: recipes, French translations, music/cookie events
- Add 34 crafting recipes (materials, consumables, cosmetics, adventures, boxes)
- Add French translations for all 8 remaining adventures
- Add missing item definitions (resource_max_*, music_melody, cookie_fortune)
- Add MusicPlayedEvent and CookieFortuneEvent for special box types
- Make box_story, box_cookie, box_music reachable from box_of_boxes
- Fix content file copying in csproj (None -> Content)
- Add global.json pinning .NET 10 SDK
- Add graceful Console.Clear() error handling
- Add recipe localization keys (EN/FR)
- Add adventure localization keys (EN/FR)
2026-03-10 18:45:54 +01:00
|
|
|
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
|
|
|
|
private static void PlayMelody()
|
|
|
|
|
{
|
|
|
|
|
// Simple melodies using Console.Beep (Windows only)
|
|
|
|
|
var melodies = new (int freq, int dur)[][]
|
|
|
|
|
{
|
|
|
|
|
// "Box Opening Fanfare"
|
|
|
|
|
[(523, 150), (587, 150), (659, 150), (784, 300), (659, 150), (784, 400)],
|
|
|
|
|
// "Loot Drop Jingle"
|
|
|
|
|
[(440, 200), (554, 200), (659, 200), (880, 400)],
|
|
|
|
|
// "Mystery Theme"
|
|
|
|
|
[(330, 300), (294, 300), (330, 300), (392, 300), (330, 600)],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var melody = melodies[Random.Shared.Next(melodies.Length)];
|
|
|
|
|
foreach (var (freq, dur) in melody)
|
|
|
|
|
{
|
|
|
|
|
Console.Beep(freq, dur);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Console.Beep not supported on all platforms
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-10 18:24:01 +01:00
|
|
|
}
|