- .NET 10 console app with Spectre.Console and Loreline integration - Black Box Sim architecture (simulation separated from presentation) - Progressive CLI rendering (9 phases from basic to full layout) - 25+ box definitions with weighted loot tables - 100+ item definitions (meta, cosmetics, materials, adventure tokens) - 9 Loreline adventures (Space, Medieval, Pirate, etc.) - Bilingual content (EN/FR) - Save/load system - Game Design Document
422 lines
14 KiB
C#
422 lines
14 KiB
C#
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);
|
|
|
|
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<string>
|
|
{
|
|
_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);
|
|
}
|
|
|
|
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;
|
|
|
|
_renderer = RendererFactory.Create(_renderContext);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
var boxNames = boxes.Select(b =>
|
|
_loc.Get(_registry.GetItem(b.DefinitionId)?.NameKey ?? b.DefinitionId)).ToList();
|
|
boxNames.Add(_loc.Get("menu.back"));
|
|
|
|
int choice = _renderer.ShowSelection(_loc.Get("prompt.choose_box"), boxNames);
|
|
if (choice >= boxes.Count) return;
|
|
|
|
var boxInstance = boxes[choice];
|
|
|
|
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)
|
|
{
|
|
foreach (var evt in events)
|
|
{
|
|
switch (evt)
|
|
{
|
|
case BoxOpenedEvent boxEvt:
|
|
var boxDef = _registry.GetBox(boxEvt.BoxId);
|
|
_renderer.ShowBoxOpening(
|
|
_loc.Get(boxDef?.NameKey ?? boxEvt.BoxId),
|
|
boxDef?.Rarity.ToString() ?? "Common");
|
|
break;
|
|
|
|
case ItemReceivedEvent itemEvt:
|
|
_state.AddItem(itemEvt.Item);
|
|
var itemDef = _registry.GetItem(itemEvt.Item.DefinitionId);
|
|
_renderer.ShowLootReveal(
|
|
[
|
|
(
|
|
_loc.Get(itemDef?.NameKey ?? itemEvt.Item.DefinitionId),
|
|
(itemDef?.Rarity ?? ItemRarity.Common).ToString(),
|
|
(itemDef?.Category ?? ItemCategory.Box).ToString()
|
|
)
|
|
]);
|
|
break;
|
|
|
|
case UIFeatureUnlockedEvent uiEvt:
|
|
_renderContext.Unlock(uiEvt.Feature);
|
|
_renderer = RendererFactory.Create(_renderContext);
|
|
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;
|
|
}
|
|
}
|
|
|
|
_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);
|
|
return (
|
|
name: _loc.Get(def?.NameKey ?? g.Key),
|
|
rarity: (def?.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}] {_loc.Get(def?.NameKey ?? 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"));
|
|
}
|
|
}
|