mirror of
https://github.com/Nighthawk42/YodaStoriesNG.git
synced 2026-08-30 09:02:26 +00:00
Add high scores, bot R2D2 priority, and menu updates
- Add HighScoreManager to save/load scores per game type - Add HighScoreWindow with tabs for Force Factor and Indy Quotient - Add "High Scores" menu item under About - Save high score when completing 15-mission cycle - Bot now prioritizes picking up R2D2/locator at game start - Update About menu text to "Desktop Adventures NG" Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
3e3f0b6823
commit
040652b267
@@ -217,6 +217,22 @@ public class MissionSolver
|
||||
/// </summary>
|
||||
private BotObjective CreateExplorationObjective()
|
||||
{
|
||||
// Priority 0: Get the locator/R2D2 if we don't have it (highest priority after safety)
|
||||
if (!_state.HasLocator)
|
||||
{
|
||||
var locator = FindLocatorItem();
|
||||
if (locator != null)
|
||||
{
|
||||
return new BotObjective
|
||||
{
|
||||
Type = ObjectiveType.PickupItem,
|
||||
Description = "Pick up R2D2 (Locator)",
|
||||
TargetX = locator.X,
|
||||
TargetY = locator.Y
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 1: Kill nearby enemies (safety first!)
|
||||
var enemy = FindNearestEnemy();
|
||||
if (enemy != null)
|
||||
@@ -492,6 +508,29 @@ public class MissionSolver
|
||||
_usedItemsOnNpcs.Add((_state.CurrentZoneId, npc.X * 10000 + itemId, npc.Y));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the locator/R2D2 item in the current zone.
|
||||
/// </summary>
|
||||
private ZoneObject? FindLocatorItem()
|
||||
{
|
||||
if (_state.CurrentZone == null) return null;
|
||||
|
||||
foreach (var obj in _state.CurrentZone.Objects)
|
||||
{
|
||||
if (obj.Type == ZoneObjectType.LocatorItem)
|
||||
{
|
||||
// Skip if already collected
|
||||
if (_collectedItems.Contains((_state.CurrentZoneId, obj.X, obj.Y)))
|
||||
continue;
|
||||
if (_state.IsObjectCollected(_state.CurrentZoneId, obj.X, obj.Y))
|
||||
continue;
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds an item we haven't picked up yet (either zone objects or tile items).
|
||||
/// </summary>
|
||||
|
||||
@@ -30,6 +30,7 @@ public unsafe class GameEngine : IDisposable
|
||||
private ControlsWindow? _controlsWindow;
|
||||
private AboutWindow? _aboutWindow;
|
||||
private ScoreWindow? _scoreWindow;
|
||||
private UI.HighScoreWindow? _highScoreWindow;
|
||||
private TitleScreen? _titleScreen;
|
||||
private MenuBar? _menuBar;
|
||||
|
||||
@@ -210,11 +211,13 @@ public unsafe class GameEngine : IDisposable
|
||||
_menuBar.OnShowControllerControls += ShowControllerControls;
|
||||
_menuBar.OnSelectDataFile += SelectDataFile;
|
||||
_menuBar.OnShowAbout += ShowAboutDialog;
|
||||
_menuBar.OnShowHighScores += ShowHighScores;
|
||||
|
||||
// Initialize controls window, about window, and score window
|
||||
_controlsWindow = new ControlsWindow();
|
||||
_aboutWindow = new AboutWindow();
|
||||
_scoreWindow = new ScoreWindow();
|
||||
_highScoreWindow = new UI.HighScoreWindow();
|
||||
|
||||
// Show title screen
|
||||
_showingTitleScreen = true;
|
||||
@@ -227,6 +230,11 @@ public unsafe class GameEngine : IDisposable
|
||||
_aboutWindow?.Open();
|
||||
}
|
||||
|
||||
private void ShowHighScores()
|
||||
{
|
||||
_highScoreWindow?.Open();
|
||||
}
|
||||
|
||||
private void SetGraphicsScale(int scale)
|
||||
{
|
||||
_graphicsScale = scale;
|
||||
@@ -875,6 +883,12 @@ public unsafe class GameEngine : IDisposable
|
||||
if (_scoreWindow.HandleEvent(&evtCopy))
|
||||
continue;
|
||||
}
|
||||
if (_highScoreWindow != null && _highScoreWindow.IsOpen)
|
||||
{
|
||||
SDLEvent evtCopy = evt;
|
||||
if (_highScoreWindow.HandleEvent(&evtCopy))
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ((SDLEventType)evt.Type)
|
||||
{
|
||||
@@ -2360,6 +2374,12 @@ public unsafe class GameEngine : IDisposable
|
||||
_messages.ShowMessage("CONGRATULATIONS! You've completed all 15 missions!", MessageType.System);
|
||||
_messages.ShowMessage("Press R to start a new 15-mission cycle.", MessageType.Info);
|
||||
|
||||
// Calculate and save high score
|
||||
var (total, _, _, _, _) = _state.CalculateScore();
|
||||
var elapsedTime = DateTime.Now - _state.GameStartTime;
|
||||
string rating = GetScoreRating(total, _gameData!.GameType);
|
||||
HighScoreManager.AddScore(_gameData!.GameType, total, rating, _state.WorldSize, elapsedTime);
|
||||
|
||||
// Show score window
|
||||
_scoreWindow?.Show(_state, _gameData!.GameType);
|
||||
}
|
||||
@@ -3481,6 +3501,7 @@ public unsafe class GameEngine : IDisposable
|
||||
_controlsWindow?.Render();
|
||||
_aboutWindow?.Render();
|
||||
_scoreWindow?.Render();
|
||||
_highScoreWindow?.Render();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -3894,6 +3915,33 @@ public unsafe class GameEngine : IDisposable
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the rating string for a score.
|
||||
/// </summary>
|
||||
private string GetScoreRating(int score, Data.GameType gameType)
|
||||
{
|
||||
if (gameType == Data.GameType.IndianaJones)
|
||||
{
|
||||
if (score >= 450) return "Master Archaeologist";
|
||||
if (score >= 400) return "Professor";
|
||||
if (score >= 350) return "Seasoned Explorer";
|
||||
if (score >= 300) return "Field Researcher";
|
||||
if (score >= 250) return "Curator";
|
||||
if (score >= 200) return "Student";
|
||||
return "Amateur";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (score >= 450) return "Legendary Hero";
|
||||
if (score >= 400) return "Jedi Master";
|
||||
if (score >= 350) return "Jedi Knight";
|
||||
if (score >= 300) return "Padawan";
|
||||
if (score >= 250) return "Force Sensitive";
|
||||
if (score >= 200) return "Adventurer";
|
||||
return "Beginner";
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Clean up controller
|
||||
@@ -3906,6 +3954,7 @@ public unsafe class GameEngine : IDisposable
|
||||
_debugMapWindow?.Dispose();
|
||||
_scriptViewer?.Dispose();
|
||||
_assetViewer?.Dispose();
|
||||
_highScoreWindow?.Dispose();
|
||||
_sounds?.Dispose();
|
||||
_renderer?.Dispose();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
using System.Text.Json;
|
||||
using YodaStoriesNG.Engine.Data;
|
||||
|
||||
namespace YodaStoriesNG.Engine.Game;
|
||||
|
||||
/// <summary>
|
||||
/// Manages high scores for Force Factor (Yoda) and Indy Quotient (Indy).
|
||||
/// </summary>
|
||||
public static class HighScoreManager
|
||||
{
|
||||
private const int MaxScoresPerGame = 10;
|
||||
private static readonly string ScoreFilePath;
|
||||
|
||||
public class HighScore
|
||||
{
|
||||
public int Score { get; set; }
|
||||
public string Rating { get; set; } = "";
|
||||
public DateTime Date { get; set; }
|
||||
public WorldSize WorldSize { get; set; }
|
||||
public TimeSpan Time { get; set; }
|
||||
}
|
||||
|
||||
public class HighScoreData
|
||||
{
|
||||
public List<HighScore> YodaScores { get; set; } = new();
|
||||
public List<HighScore> IndyScores { get; set; } = new();
|
||||
}
|
||||
|
||||
private static HighScoreData _scores = new();
|
||||
|
||||
static HighScoreManager()
|
||||
{
|
||||
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
var gameDir = Path.Combine(appData, "YodaStoriesNG");
|
||||
Directory.CreateDirectory(gameDir);
|
||||
ScoreFilePath = Path.Combine(gameDir, "highscores.json");
|
||||
Load();
|
||||
}
|
||||
|
||||
public static void Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(ScoreFilePath))
|
||||
{
|
||||
var json = File.ReadAllText(ScoreFilePath);
|
||||
_scores = JsonSerializer.Deserialize<HighScoreData>(json) ?? new HighScoreData();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[HighScores] Failed to load: {ex.Message}");
|
||||
_scores = new HighScoreData();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(_scores, new JsonSerializerOptions { WriteIndented = true });
|
||||
File.WriteAllText(ScoreFilePath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"[HighScores] Failed to save: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddScore(GameType gameType, int score, string rating, WorldSize worldSize, TimeSpan time)
|
||||
{
|
||||
var newScore = new HighScore
|
||||
{
|
||||
Score = score,
|
||||
Rating = rating,
|
||||
Date = DateTime.Now,
|
||||
WorldSize = worldSize,
|
||||
Time = time
|
||||
};
|
||||
|
||||
var list = gameType == GameType.IndianaJones ? _scores.IndyScores : _scores.YodaScores;
|
||||
list.Add(newScore);
|
||||
list.Sort((a, b) => b.Score.CompareTo(a.Score)); // Sort descending
|
||||
|
||||
// Keep only top scores
|
||||
while (list.Count > MaxScoresPerGame)
|
||||
list.RemoveAt(list.Count - 1);
|
||||
|
||||
Save();
|
||||
}
|
||||
|
||||
public static List<HighScore> GetScores(GameType gameType)
|
||||
{
|
||||
return gameType == GameType.IndianaJones ? _scores.IndyScores : _scores.YodaScores;
|
||||
}
|
||||
|
||||
public static int GetHighScore(GameType gameType)
|
||||
{
|
||||
var list = gameType == GameType.IndianaJones ? _scores.IndyScores : _scores.YodaScores;
|
||||
return list.Count > 0 ? list[0].Score : 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
using Hexa.NET.SDL2;
|
||||
using YodaStoriesNG.Engine.Data;
|
||||
using YodaStoriesNG.Engine.Game;
|
||||
using YodaStoriesNG.Engine.Rendering;
|
||||
|
||||
namespace YodaStoriesNG.Engine.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Displays high scores for both Force Factor (Yoda) and Indy Quotient (Indy).
|
||||
/// </summary>
|
||||
public unsafe class HighScoreWindow : IDisposable
|
||||
{
|
||||
private SDLWindow* _window;
|
||||
private SDLRenderer* _renderer;
|
||||
private BitmapFont? _font;
|
||||
private bool _isOpen = false;
|
||||
private uint _windowId;
|
||||
private int _selectedTab = 0; // 0 = Yoda, 1 = Indy
|
||||
|
||||
private const int WindowWidth = 450;
|
||||
private const int WindowHeight = 400;
|
||||
|
||||
public bool IsOpen => _isOpen;
|
||||
|
||||
public void Open()
|
||||
{
|
||||
if (_isOpen)
|
||||
{
|
||||
if (_window != null)
|
||||
SDL.RaiseWindow(_window);
|
||||
return;
|
||||
}
|
||||
|
||||
_window = SDL.CreateWindow(
|
||||
"High Scores",
|
||||
200, 150,
|
||||
WindowWidth, WindowHeight,
|
||||
(uint)SDLWindowFlags.Shown);
|
||||
|
||||
if (_window == null)
|
||||
{
|
||||
Console.WriteLine($"Failed to create high score window: {SDL.GetErrorS()}");
|
||||
return;
|
||||
}
|
||||
|
||||
_renderer = SDL.CreateRenderer(_window, -1,
|
||||
(uint)(SDLRendererFlags.Accelerated | SDLRendererFlags.Presentvsync));
|
||||
|
||||
if (_renderer == null)
|
||||
{
|
||||
SDL.DestroyWindow(_window);
|
||||
_window = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_font = new BitmapFont();
|
||||
_font.Initialize(_renderer);
|
||||
|
||||
_windowId = SDL.GetWindowID(_window);
|
||||
_isOpen = true;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (!_isOpen) return;
|
||||
|
||||
_font?.Dispose();
|
||||
_font = null;
|
||||
|
||||
if (_renderer != null)
|
||||
{
|
||||
SDL.DestroyRenderer(_renderer);
|
||||
_renderer = null;
|
||||
}
|
||||
|
||||
if (_window != null)
|
||||
{
|
||||
SDL.DestroyWindow(_window);
|
||||
_window = null;
|
||||
}
|
||||
|
||||
_isOpen = false;
|
||||
}
|
||||
|
||||
public bool HandleEvent(SDLEvent* evt)
|
||||
{
|
||||
if (!_isOpen) return false;
|
||||
|
||||
if (evt->Type == (uint)SDLEventType.Windowevent && evt->Window.WindowID == _windowId)
|
||||
{
|
||||
if (evt->Window.Event == (byte)SDLWindowEventID.Close)
|
||||
{
|
||||
Close();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (evt->Type == (uint)SDLEventType.Keydown && evt->Key.WindowID == _windowId)
|
||||
{
|
||||
var keyCode = (int)evt->Key.Keysym.Sym;
|
||||
if (keyCode == (int)SDLKeyCode.Escape)
|
||||
{
|
||||
Close();
|
||||
return true;
|
||||
}
|
||||
if (keyCode == (int)SDLKeyCode.Tab || keyCode == (int)SDLKeyCode.Left || keyCode == (int)SDLKeyCode.Right)
|
||||
{
|
||||
_selectedTab = _selectedTab == 0 ? 1 : 0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (evt->Type == (uint)SDLEventType.Mousebuttondown && evt->Button.WindowID == _windowId)
|
||||
{
|
||||
int mx = evt->Button.X;
|
||||
int my = evt->Button.Y;
|
||||
|
||||
// Check tab clicks
|
||||
if (my >= 50 && my <= 80)
|
||||
{
|
||||
if (mx < WindowWidth / 2)
|
||||
_selectedTab = 0;
|
||||
else
|
||||
_selectedTab = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check close button
|
||||
if (my >= WindowHeight - 50 && my <= WindowHeight - 20 &&
|
||||
mx >= WindowWidth / 2 - 50 && mx <= WindowWidth / 2 + 50)
|
||||
{
|
||||
Close();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Render()
|
||||
{
|
||||
if (!_isOpen || _renderer == null || _font == null) return;
|
||||
|
||||
// Background
|
||||
SDL.SetRenderDrawColor(_renderer, 25, 30, 40, 255);
|
||||
SDL.RenderClear(_renderer);
|
||||
|
||||
// Header
|
||||
SDL.SetRenderDrawColor(_renderer, 40, 45, 60, 255);
|
||||
var headerRect = new SDLRect { X = 0, Y = 0, W = WindowWidth, H = 45 };
|
||||
SDL.RenderFillRect(_renderer, &headerRect);
|
||||
|
||||
RenderTextCentered("HIGH SCORES", WindowWidth / 2, 15, 2, 255, 215, 0);
|
||||
|
||||
// Tabs
|
||||
int tabWidth = WindowWidth / 2;
|
||||
|
||||
// Yoda tab
|
||||
SDL.SetRenderDrawColor(_renderer, _selectedTab == 0 ? (byte)60 : (byte)40,
|
||||
_selectedTab == 0 ? (byte)80 : (byte)45,
|
||||
_selectedTab == 0 ? (byte)60 : (byte)55, 255);
|
||||
var yodaTab = new SDLRect { X = 0, Y = 50, W = tabWidth, H = 30 };
|
||||
SDL.RenderFillRect(_renderer, &yodaTab);
|
||||
RenderTextCentered("Force Factor", tabWidth / 2, 57, 1,
|
||||
_selectedTab == 0 ? (byte)100 : (byte)150,
|
||||
_selectedTab == 0 ? (byte)255 : (byte)150,
|
||||
_selectedTab == 0 ? (byte)100 : (byte)150);
|
||||
|
||||
// Indy tab
|
||||
SDL.SetRenderDrawColor(_renderer, _selectedTab == 1 ? (byte)80 : (byte)40,
|
||||
_selectedTab == 1 ? (byte)60 : (byte)45,
|
||||
_selectedTab == 1 ? (byte)40 : (byte)55, 255);
|
||||
var indyTab = new SDLRect { X = tabWidth, Y = 50, W = tabWidth, H = 30 };
|
||||
SDL.RenderFillRect(_renderer, &indyTab);
|
||||
RenderTextCentered("Indy Quotient", tabWidth + tabWidth / 2, 57, 1,
|
||||
_selectedTab == 1 ? (byte)255 : (byte)150,
|
||||
_selectedTab == 1 ? (byte)200 : (byte)150,
|
||||
_selectedTab == 1 ? (byte)100 : (byte)150);
|
||||
|
||||
// Score list
|
||||
var gameType = _selectedTab == 0 ? GameType.YodaStories : GameType.IndianaJones;
|
||||
var scores = HighScoreManager.GetScores(gameType);
|
||||
|
||||
int y = 95;
|
||||
int rank = 1;
|
||||
|
||||
// Column headers
|
||||
_font.RenderText(_renderer, "#", 20, y, 1, 120, 120, 150, 255);
|
||||
_font.RenderText(_renderer, "Score", 50, y, 1, 120, 120, 150, 255);
|
||||
_font.RenderText(_renderer, "Rating", 120, y, 1, 120, 120, 150, 255);
|
||||
_font.RenderText(_renderer, "Size", 280, y, 1, 120, 120, 150, 255);
|
||||
_font.RenderText(_renderer, "Time", 340, y, 1, 120, 120, 150, 255);
|
||||
y += 25;
|
||||
|
||||
if (scores.Count == 0)
|
||||
{
|
||||
RenderTextCentered("No scores yet!", WindowWidth / 2, y + 50, 1, 150, 150, 150);
|
||||
RenderTextCentered("Complete a 15-mission cycle", WindowWidth / 2, y + 75, 1, 120, 120, 140);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var score in scores.Take(10))
|
||||
{
|
||||
byte r = rank == 1 ? (byte)255 : rank == 2 ? (byte)200 : rank == 3 ? (byte)180 : (byte)160;
|
||||
byte g = rank == 1 ? (byte)215 : rank == 2 ? (byte)200 : rank == 3 ? (byte)150 : (byte)160;
|
||||
byte b = rank == 1 ? (byte)0 : rank == 2 ? (byte)200 : rank == 3 ? (byte)100 : (byte)170;
|
||||
|
||||
_font.RenderText(_renderer, $"{rank}", 20, y, 1, r, g, b, 255);
|
||||
_font.RenderText(_renderer, $"{score.Score}", 50, y, 1, 200, 200, 200, 255);
|
||||
_font.RenderText(_renderer, score.Rating, 120, y, 1, 180, 180, 200, 255);
|
||||
_font.RenderText(_renderer, score.WorldSize.ToString()[0].ToString(), 280, y, 1, 150, 150, 180, 255);
|
||||
_font.RenderText(_renderer, $"{(int)score.Time.TotalMinutes}:{score.Time.Seconds:D2}", 340, y, 1, 150, 150, 180, 255);
|
||||
|
||||
y += 22;
|
||||
rank++;
|
||||
}
|
||||
}
|
||||
|
||||
// Close button
|
||||
int btnX = WindowWidth / 2 - 50;
|
||||
int btnY = WindowHeight - 50;
|
||||
SDL.SetRenderDrawColor(_renderer, 60, 80, 100, 255);
|
||||
var btnRect = new SDLRect { X = btnX, Y = btnY, W = 100, H = 30 };
|
||||
SDL.RenderFillRect(_renderer, &btnRect);
|
||||
SDL.SetRenderDrawColor(_renderer, 80, 100, 130, 255);
|
||||
SDL.RenderDrawRect(_renderer, &btnRect);
|
||||
RenderTextCentered("Close", WindowWidth / 2, btnY + 8, 1, 200, 200, 200);
|
||||
|
||||
// Footer hint
|
||||
_font.RenderText(_renderer, "Tab/Arrow: Switch game | ESC: Close", 90, WindowHeight - 18, 1, 100, 100, 120, 255);
|
||||
|
||||
SDL.RenderPresent(_renderer);
|
||||
}
|
||||
|
||||
private void RenderTextCentered(string text, int centerX, int y, int scale, byte r, byte g, byte b)
|
||||
{
|
||||
int width = _font!.GetTextWidth(text) * scale;
|
||||
_font.RenderText(_renderer, text, centerX - width / 2, y, scale, r, g, b, 255);
|
||||
}
|
||||
|
||||
public void Dispose() => Close();
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public unsafe class MenuBar
|
||||
new[] { "New Game: Small", "New Game: Medium", "New Game: Large", "New Game: X-tra Large", "-", "Save Game", "Save As...", "Load Game", "-", "Exit" },
|
||||
new[] { "Asset Viewer (F2)", "Script Editor (F3)", "Map Viewer (F4)", "-", "Enable Bot", "Disable Bot" },
|
||||
new[] { "Graphics: 1x Scale", "Graphics: 2x Scale", "Graphics: 4x Scale", "-", "Keyboard Controls", "Controller Controls", "-", "Select Data File..." },
|
||||
new[] { "About Yoda Stories NG" }
|
||||
new[] { "About Desktop Adventures NG", "High Scores" }
|
||||
};
|
||||
|
||||
// Menu positions (consistent 5px gap between menus)
|
||||
@@ -48,6 +48,7 @@ public unsafe class MenuBar
|
||||
public event Action? OnShowControllerControls;
|
||||
public event Action? OnSelectDataFile;
|
||||
public event Action? OnShowAbout;
|
||||
public event Action? OnShowHighScores;
|
||||
|
||||
public bool IsMenuOpen => _openMenu >= 0;
|
||||
|
||||
@@ -243,6 +244,7 @@ public unsafe class MenuBar
|
||||
switch (item)
|
||||
{
|
||||
case 0: OnShowAbout?.Invoke(); break;
|
||||
case 1: OnShowHighScores?.Invoke(); break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user