using OpenTheBox.Core; 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!; private static bool _running = true; public static async Task Main(string[] args) { _saveManager = new SaveManager(); _loc = new LocalizationManager(Locale.EN); _renderContext = new RenderContext(); _renderer = RendererFactory.Create(_renderContext, _loc); await MainMenuLoop(); } private static async Task MainMenuLoop() { while (_running) { _renderer.Clear(); _renderer.ShowMessage("========================================"); _renderer.ShowMessage(" OPEN THE BOX"); _renderer.ShowMessage("========================================"); _renderer.ShowMessage(""); _renderer.ShowMessage(_loc.Get("game.subtitle")); _renderer.ShowMessage(""); var options = new List { _loc.Get("menu.new_game"), _loc.Get("menu.load_game"), _loc.Get("menu.language"), _loc.Get("menu.quit") }; int choice = _renderer.ShowSelection("", options); switch (choice) { case 0: await NewGame(); break; case 1: await LoadGame(); break; case 2: ChangeLanguage(); break; case 3: _running = false; break; } } } 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(""); _renderer.ShowMessage($"Welcome, {name}!"); _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", "content/data/interactions.json" ); _simulation = new GameSimulation(_registry); _renderContext = RenderContext.FromGameState(_state); _renderer = RendererFactory.Create(_renderContext, _loc); } private static void ChangeLanguage() { var options = new List { "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; _renderer = RendererFactory.Create(_renderContext, _loc); } private static async Task GameLoop() { while (_running) { _renderer.Clear(); _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")); actions.Add((_loc.Get("action.save"), "save")); 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; 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; } // 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(); boxNames.Add(_loc.Get("menu.back")); int choice = _renderer.ShowSelection(_loc.Get("prompt.choose_box"), boxNames); if (choice >= grouped.Count) return; var boxInstance = grouped[choice].First(); var openAction = new OpenBoxAction(boxInstance.Id) { BoxDefinitionId = boxInstance.DefinitionId }; var events = _simulation.ProcessAction(openAction, _state); await RenderEvents(events); } private static async Task RenderEvents(List events) { // 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().Select(e => e.InstanceId).ToHashSet(); foreach (var evt in events) { switch (evt) { case BoxOpenedEvent boxEvt: var boxDef = _registry.GetBox(boxEvt.BoxId); var boxName = _loc.Get(boxDef?.NameKey ?? boxEvt.BoxId); if (boxEvt.IsAutoOpen) { _renderer.ShowMessage(_loc.Get("box.auto_open", boxName)); } else { _renderer.ShowBoxOpening(boxName, boxDef?.Rarity.ToString() ?? "Common"); } break; case ItemReceivedEvent itemEvt: // Skip loot reveal for auto-consumed items (auto-opening boxes) if (autoConsumedIds.Contains(itemEvt.Item.Id)) break; _state.AddItem(itemEvt.Item); var itemDef = _registry.GetItem(itemEvt.Item.DefinitionId); var itemBoxDef = itemDef is null ? _registry.GetBox(itemEvt.Item.DefinitionId) : null; _renderer.ShowLootReveal( [ ( GetLocalizedName(itemEvt.Item.DefinitionId), (itemDef?.Rarity ?? itemBoxDef?.Rarity ?? ItemRarity.Common).ToString(), (itemDef?.Category ?? ItemCategory.Box).ToString() ) ]); break; case UIFeatureUnlockedEvent uiEvt: _renderContext.Unlock(uiEvt.Feature); _renderer = RendererFactory.Create(_renderContext, _loc); var featureKey = $"meta.{uiEvt.Feature.ToString().ToLower()}"; _renderer.ShowUIFeatureUnlocked( _loc.Get("meta.unlocked", _loc.Get(featureKey))); _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; case AdventureStartedEvent advEvt: await RunAdventure(advEvt.Theme); break; 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; } } _renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); } private static void ShowInventory() { _renderer.Clear(); if (_state.Inventory.Count == 0) { _renderer.ShowMessage("Your inventory is empty. Open more boxes!"); _renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); return; } var grouped = _state.Inventory .GroupBy(i => i.DefinitionId) .Select(g => { var def = _registry.GetItem(g.Key); var bDef = def is null ? _registry.GetBox(g.Key) : null; var baseName = GetLocalizedName(g.Key); return ( name: g.Count() > 1 ? $"{baseName} (x{g.Count()})" : baseName, rarity: (def?.Rarity ?? bDef?.Rarity ?? ItemRarity.Common).ToString(), 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) { _renderer.ShowMessage("No adventures available yet. Keep opening boxes!"); _renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); return; } var options = available.Select(a => { bool completed = _state.CompletedAdventures.Contains(a.ToString()); return (completed ? "[Done] " : "") + a.ToString(); }).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) { _renderer.ShowMessage($"Adventure '{theme}' is coming soon! The boxes are still being assembled."); } catch (Exception ex) { _renderer.ShowError($"Adventure error: {ex.Message}"); } _renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); } private static void ChangeAppearance() { var cosmeticItems = _state.Inventory .Where(i => { var def = _registry.GetItem(i.DefinitionId); return def?.Category == ItemCategory.Cosmetic && def.CosmeticSlot.HasValue; }) .ToList(); if (cosmeticItems.Count == 0) { _renderer.ShowMessage("No cosmetics available yet. Open Style Boxes!"); _renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); return; } var options = cosmeticItems.Select(i => { var def = _registry.GetItem(i.DefinitionId); return $"[{def?.CosmeticSlot}] {GetLocalizedName(i.DefinitionId)}"; }).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) _renderer.ShowMessage($"Equipped {cosEvt.Slot}: {cosEvt.NewValue}"); else if (evt is MessageEvent msgEvt) _renderer.ShowMessage(_loc.Get(msgEvt.MessageKey, msgEvt.Args ?? [])); } _renderer.WaitForKeyPress(_loc.Get("prompt.press_key")); } 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")); } /// /// Resolves the localized display name for any definition ID (item or box). /// 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); } [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 } } }