Browse Source

Separate Save and Load menus with save slot management UI

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 2 months ago
parent
commit
8659891f86

+ 67 - 0
app/core/game_engine.cpp

@@ -947,6 +947,73 @@ void GameEngine::saveGame(const QString &filename) {
   }
   }
 }
 }
 
 
+void GameEngine::saveGameToSlot(const QString &slotName) {
+  if (!m_saveLoadService || !m_world) {
+    qWarning() << "Cannot save game: service or world not initialized";
+    return;
+  }
+
+  bool success = m_saveLoadService->saveGameToSlot(*m_world, slotName, m_level.mapName);
+  
+  if (success) {
+    qInfo() << "Game saved successfully to slot:" << slotName;
+  } else {
+    QString error = m_saveLoadService->getLastError();
+    qWarning() << "Failed to save game:" << error;
+    setError(error);
+  }
+}
+
+void GameEngine::loadGameFromSlot(const QString &slotName) {
+  if (!m_saveLoadService || !m_world) {
+    qWarning() << "Cannot load game: service or world not initialized";
+    return;
+  }
+
+  bool success = m_saveLoadService->loadGameFromSlot(*m_world, slotName);
+  
+  if (success) {
+    qInfo() << "Game loaded successfully from slot:" << slotName;
+    // Rebuild caches and update UI after loading
+    rebuildEntityCache();
+    if (m_victoryService) {
+      m_victoryService->configure(Game::Map::VictoryConfig(), m_runtime.localOwnerId);
+    }
+    emit selectedUnitsChanged();
+    emit ownerInfoChanged();
+  } else {
+    QString error = m_saveLoadService->getLastError();
+    qWarning() << "Failed to load game:" << error;
+    setError(error);
+  }
+}
+
+QVariantList GameEngine::getSaveSlots() const {
+  if (!m_saveLoadService) {
+    qWarning() << "Cannot get save slots: service not initialized";
+    return QVariantList();
+  }
+
+  return m_saveLoadService->getSaveSlots();
+}
+
+bool GameEngine::deleteSaveSlot(const QString &slotName) {
+  if (!m_saveLoadService) {
+    qWarning() << "Cannot delete save slot: service not initialized";
+    return false;
+  }
+
+  bool success = m_saveLoadService->deleteSaveSlot(slotName);
+  
+  if (!success) {
+    QString error = m_saveLoadService->getLastError();
+    qWarning() << "Failed to delete save slot:" << error;
+    setError(error);
+  }
+  
+  return success;
+}
+
 void GameEngine::exitGame() {
 void GameEngine::exitGame() {
   if (m_saveLoadService) {
   if (m_saveLoadService) {
     m_saveLoadService->exitGame();
     m_saveLoadService->exitGame();

+ 4 - 0
app/core/game_engine.h

@@ -163,6 +163,10 @@ public:
   Q_INVOKABLE void openSettings();
   Q_INVOKABLE void openSettings();
   Q_INVOKABLE void loadSave();
   Q_INVOKABLE void loadSave();
   Q_INVOKABLE void saveGame(const QString &filename = "savegame.json");
   Q_INVOKABLE void saveGame(const QString &filename = "savegame.json");
+  Q_INVOKABLE void saveGameToSlot(const QString &slotName);
+  Q_INVOKABLE void loadGameFromSlot(const QString &slotName);
+  Q_INVOKABLE QVariantList getSaveSlots() const;
+  Q_INVOKABLE bool deleteSaveSlot(const QString &slotName);
   Q_INVOKABLE void exitGame();
   Q_INVOKABLE void exitGame();
   Q_INVOKABLE QVariantList getOwnerInfo() const;
   Q_INVOKABLE QVariantList getOwnerInfo() const;
 
 

+ 181 - 1
game/systems/save_load_service.cpp

@@ -2,7 +2,16 @@
 #include "game/core/serialization.h"
 #include "game/core/serialization.h"
 #include "game/core/world.h"
 #include "game/core/world.h"
 #include <QCoreApplication>
 #include <QCoreApplication>
+#include <QDateTime>
 #include <QDebug>
 #include <QDebug>
+#include <QDir>
+#include <QFile>
+#include <QFileInfo>
+#include <QJsonArray>
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QRegularExpression>
+#include <QStandardPaths>
 
 
 namespace Game {
 namespace Game {
 namespace Systems {
 namespace Systems {
@@ -10,6 +19,27 @@ namespace Systems {
 SaveLoadService::SaveLoadService() = default;
 SaveLoadService::SaveLoadService() = default;
 SaveLoadService::~SaveLoadService() = default;
 SaveLoadService::~SaveLoadService() = default;
 
 
+QString SaveLoadService::getSavesDirectory() const {
+  QString savesPath = QStandardPaths::writableLocation(
+      QStandardPaths::AppDataLocation);
+  return savesPath + "/saves";
+}
+
+void SaveLoadService::ensureSavesDirectoryExists() const {
+  QString savesDir = getSavesDirectory();
+  QDir dir;
+  if (!dir.exists(savesDir)) {
+    dir.mkpath(savesDir);
+  }
+}
+
+QString SaveLoadService::getSlotFilePath(const QString &slotName) const {
+  QString sanitized = slotName;
+  QRegularExpression regex("[^a-zA-Z0-9_-]");
+  sanitized.replace(regex, "_");
+  return getSavesDirectory() + "/" + sanitized + ".json";
+}
+
 bool SaveLoadService::saveGame(Engine::Core::World &world,
 bool SaveLoadService::saveGame(Engine::Core::World &world,
                                 const QString &filename) {
                                 const QString &filename) {
   qInfo() << "Saving game to:" << filename;
   qInfo() << "Saving game to:" << filename;
@@ -46,7 +76,8 @@ bool SaveLoadService::loadGame(Engine::Core::World &world,
     QJsonDocument doc = Engine::Core::Serialization::loadFromFile(filename);
     QJsonDocument doc = Engine::Core::Serialization::loadFromFile(filename);
 
 
     if (doc.isNull() || doc.isEmpty()) {
     if (doc.isNull() || doc.isEmpty()) {
-      m_lastError = "Failed to load game from file or file is empty: " + filename;
+      m_lastError =
+          "Failed to load game from file or file is empty: " + filename;
       qWarning() << m_lastError;
       qWarning() << m_lastError;
       return false;
       return false;
     }
     }
@@ -67,6 +98,155 @@ bool SaveLoadService::loadGame(Engine::Core::World &world,
   }
   }
 }
 }
 
 
+bool SaveLoadService::saveGameToSlot(Engine::Core::World &world,
+                                     const QString &slotName,
+                                     const QString &mapName) {
+  qInfo() << "Saving game to slot:" << slotName;
+
+  try {
+    ensureSavesDirectoryExists();
+
+    // Serialize the world
+    QJsonDocument worldDoc =
+        Engine::Core::Serialization::serializeWorld(&world);
+    QJsonObject worldObj = worldDoc.object();
+
+    // Add metadata
+    QJsonObject metadata;
+    metadata["slotName"] = slotName;
+    metadata["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);
+    metadata["mapName"] = mapName.isEmpty() ? "Unknown Map" : mapName;
+    metadata["version"] = "1.0";
+
+    worldObj["metadata"] = metadata;
+
+    QJsonDocument finalDoc(worldObj);
+
+    // Save to slot file
+    QString filePath = getSlotFilePath(slotName);
+    bool success = Engine::Core::Serialization::saveToFile(filePath, finalDoc);
+
+    if (success) {
+      qInfo() << "Game saved successfully to slot:" << slotName << "at"
+              << filePath;
+      m_lastError.clear();
+      return true;
+    } else {
+      m_lastError = "Failed to save game to slot: " + slotName;
+      qWarning() << m_lastError;
+      return false;
+    }
+  } catch (const std::exception &e) {
+    m_lastError = QString("Exception while saving game to slot: %1").arg(e.what());
+    qWarning() << m_lastError;
+    return false;
+  }
+}
+
+bool SaveLoadService::loadGameFromSlot(Engine::Core::World &world,
+                                       const QString &slotName) {
+  qInfo() << "Loading game from slot:" << slotName;
+
+  try {
+    QString filePath = getSlotFilePath(slotName);
+
+    // Check if file exists
+    if (!QFile::exists(filePath)) {
+      m_lastError = "Save slot not found: " + slotName;
+      qWarning() << m_lastError;
+      return false;
+    }
+
+    // Load JSON document
+    QJsonDocument doc = Engine::Core::Serialization::loadFromFile(filePath);
+
+    if (doc.isNull() || doc.isEmpty()) {
+      m_lastError = "Failed to load game from slot or file is empty: " + slotName;
+      qWarning() << m_lastError;
+      return false;
+    }
+
+    // Clear current world state before loading
+    world.clear();
+
+    // Deserialize the world (metadata will be ignored during deserialization)
+    Engine::Core::Serialization::deserializeWorld(&world, doc);
+
+    qInfo() << "Game loaded successfully from slot:" << slotName;
+    m_lastError.clear();
+    return true;
+  } catch (const std::exception &e) {
+    m_lastError =
+        QString("Exception while loading game from slot: %1").arg(e.what());
+    qWarning() << m_lastError;
+    return false;
+  }
+}
+
+QVariantList SaveLoadService::getSaveSlots() const {
+  QVariantList slots;
+
+  QString savesDir = getSavesDirectory();
+  QDir dir(savesDir);
+
+  if (!dir.exists()) {
+    return slots;
+  }
+
+  QStringList filters;
+  filters << "*.json";
+  QFileInfoList files =
+      dir.entryInfoList(filters, QDir::Files, QDir::Time | QDir::Reversed);
+
+  for (const QFileInfo &fileInfo : files) {
+    try {
+      QJsonDocument doc =
+          Engine::Core::Serialization::loadFromFile(fileInfo.absoluteFilePath());
+
+      if (!doc.isNull() && doc.isObject()) {
+        QJsonObject obj = doc.object();
+        QJsonObject metadata = obj["metadata"].toObject();
+
+        QVariantMap slot;
+        slot["name"] = metadata["slotName"].toString(fileInfo.baseName());
+        slot["timestamp"] = metadata["timestamp"].toString(
+            fileInfo.lastModified().toString(Qt::ISODate));
+        slot["mapName"] = metadata["mapName"].toString("Unknown Map");
+        slot["filePath"] = fileInfo.absoluteFilePath();
+
+        slots.append(slot);
+      }
+    } catch (...) {
+      // Skip corrupted save files
+      continue;
+    }
+  }
+
+  return slots;
+}
+
+bool SaveLoadService::deleteSaveSlot(const QString &slotName) {
+  qInfo() << "Deleting save slot:" << slotName;
+
+  QString filePath = getSlotFilePath(slotName);
+
+  if (!QFile::exists(filePath)) {
+    m_lastError = "Save slot not found: " + slotName;
+    qWarning() << m_lastError;
+    return false;
+  }
+
+  if (QFile::remove(filePath)) {
+    qInfo() << "Save slot deleted successfully:" << slotName;
+    m_lastError.clear();
+    return true;
+  } else {
+    m_lastError = "Failed to delete save slot: " + slotName;
+    qWarning() << m_lastError;
+    return false;
+  }
+}
+
 void SaveLoadService::openSettings() {
 void SaveLoadService::openSettings() {
   qInfo() << "Open settings requested";
   qInfo() << "Open settings requested";
   // TODO: Implement settings dialog/menu integration
   // TODO: Implement settings dialog/menu integration

+ 19 - 0
game/systems/save_load_service.h

@@ -1,6 +1,8 @@
 #pragma once
 #pragma once
 
 
 #include <QString>
 #include <QString>
+#include <QVariantList>
+#include <QVariantMap>
 #include <functional>
 #include <functional>
 
 
 namespace Engine {
 namespace Engine {
@@ -23,6 +25,19 @@ public:
   // Load game state from file
   // Load game state from file
   bool loadGame(Engine::Core::World &world, const QString &filename);
   bool loadGame(Engine::Core::World &world, const QString &filename);
 
 
+  // Save game to named slot with metadata
+  bool saveGameToSlot(Engine::Core::World &world, const QString &slotName,
+                      const QString &mapName = QString());
+
+  // Load game from named slot
+  bool loadGameFromSlot(Engine::Core::World &world, const QString &slotName);
+
+  // Get list of all save slots with metadata
+  QVariantList getSaveSlots() const;
+
+  // Delete a save slot
+  bool deleteSaveSlot(const QString &slotName);
+
   // Get the last error message
   // Get the last error message
   QString getLastError() const { return m_lastError; }
   QString getLastError() const { return m_lastError; }
 
 
@@ -36,6 +51,10 @@ public:
   void exitGame();
   void exitGame();
 
 
 private:
 private:
+  QString getSlotFilePath(const QString &slotName) const;
+  QString getSavesDirectory() const;
+  void ensureSavesDirectoryExists() const;
+
   QString m_lastError;
   QString m_lastError;
 };
 };
 
 

+ 2 - 0
qml_resources.qrc

@@ -6,6 +6,8 @@
         <file>ui/qml/MapListPanel.qml</file>
         <file>ui/qml/MapListPanel.qml</file>
         <file>ui/qml/PlayerListItem.qml</file>
         <file>ui/qml/PlayerListItem.qml</file>
         <file>ui/qml/PlayerConfigPanel.qml</file>
         <file>ui/qml/PlayerConfigPanel.qml</file>
+        <file>ui/qml/SaveGamePanel.qml</file>
+        <file>ui/qml/LoadGamePanel.qml</file>
         <file>ui/qml/Constants.qml</file>
         <file>ui/qml/Constants.qml</file>
         <file>ui/qml/StyleGuide.qml</file>
         <file>ui/qml/StyleGuide.qml</file>
         <file>ui/qml/StyledButton.qml</file>
         <file>ui/qml/StyledButton.qml</file>

+ 327 - 0
ui/qml/LoadGamePanel.qml

@@ -0,0 +1,327 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+import StandardOfIron.UI 1.0
+
+Item {
+    id: root
+
+    signal cancelled()
+    signal loadRequested(string slotName)
+
+    anchors.fill: parent
+    z: 25
+
+    Rectangle {
+        anchors.fill: parent
+        color: Theme.dim
+    }
+
+    Rectangle {
+        id: container
+
+        width: Math.min(parent.width * 0.7, 900)
+        height: Math.min(parent.height * 0.8, 600)
+        anchors.centerIn: parent
+        radius: Theme.radiusPanel
+        color: Theme.panelBase
+        border.color: Theme.panelBr
+        border.width: 1
+        opacity: 0.98
+
+        ColumnLayout {
+            anchors.fill: parent
+            anchors.margins: Theme.spacingXLarge
+            spacing: Theme.spacingLarge
+
+            // Header
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: Theme.spacingMedium
+
+                Label {
+                    text: "Load Game"
+                    color: Theme.textMain
+                    font.pointSize: Theme.fontSizeHero
+                    font.bold: true
+                    Layout.fillWidth: true
+                }
+
+                Button {
+                    text: "Cancel"
+                    onClicked: root.cancelled()
+                }
+            }
+
+            Rectangle {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 1
+                color: Theme.border
+            }
+
+            // Save slots list
+            Rectangle {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                color: Theme.cardBase
+                border.color: Theme.border
+                border.width: 1
+                radius: Theme.radiusLarge
+
+                ScrollView {
+                    anchors.fill: parent
+                    anchors.margins: Theme.spacingSmall
+                    clip: true
+
+                    ListView {
+                        id: loadListView
+
+                        property int selectedIndex: -1
+
+                        model: ListModel {
+                            id: loadListModel
+
+                            Component.onCompleted: {
+                                // Populate with existing saves
+                                if (typeof game !== 'undefined' && game.getSaveSlots) {
+                                    var slots = game.getSaveSlots()
+                                    for (var i = 0; i < slots.length; i++) {
+                                        append({
+                                            slotName: slots[i].name,
+                                            timestamp: slots[i].timestamp,
+                                            mapName: slots[i].mapName || "Unknown Map",
+                                            playTime: slots[i].playTime || "Unknown"
+                                        })
+                                    }
+                                }
+                                
+                                if (count === 0) {
+                                    append({
+                                        slotName: "No saves found",
+                                        timestamp: 0,
+                                        mapName: "",
+                                        playTime: "",
+                                        isEmpty: true
+                                    })
+                                }
+                            }
+                        }
+
+                        spacing: Theme.spacingSmall
+                        delegate: Rectangle {
+                            width: loadListView.width
+                            height: model.isEmpty ? 100 : 120
+                            color: loadListView.selectedIndex === index ? Theme.selectedBg : 
+                                   mouseArea.containsMouse ? Theme.hoverBg : Qt.rgba(0, 0, 0, 0)
+                            radius: Theme.radiusMedium
+                            border.color: loadListView.selectedIndex === index ? Theme.selectedBr : Theme.cardBorder
+                            border.width: 1
+                            visible: !model.isEmpty || loadListModel.count === 1
+
+                            RowLayout {
+                                anchors.fill: parent
+                                anchors.margins: Theme.spacingMedium
+                                spacing: Theme.spacingMedium
+
+                                ColumnLayout {
+                                    Layout.fillWidth: true
+                                    spacing: Theme.spacingTiny
+                                    visible: !model.isEmpty
+
+                                    Label {
+                                        text: model.slotName
+                                        color: Theme.textMain
+                                        font.pointSize: Theme.fontSizeLarge
+                                        font.bold: true
+                                        Layout.fillWidth: true
+                                        elide: Label.ElideRight
+                                    }
+
+                                    Label {
+                                        text: model.mapName
+                                        color: Theme.textSub
+                                        font.pointSize: Theme.fontSizeMedium
+                                        Layout.fillWidth: true
+                                        elide: Label.ElideRight
+                                    }
+
+                                    RowLayout {
+                                        Layout.fillWidth: true
+                                        spacing: Theme.spacingLarge
+
+                                        Label {
+                                            text: "Last saved: " + Qt.formatDateTime(new Date(model.timestamp), "yyyy-MM-dd hh:mm:ss")
+                                            color: Theme.textHint
+                                            font.pointSize: Theme.fontSizeSmall
+                                            Layout.fillWidth: true
+                                            elide: Label.ElideRight
+                                        }
+
+                                        Label {
+                                            text: model.playTime !== "" ? "Play time: " + model.playTime : ""
+                                            color: Theme.textHint
+                                            font.pointSize: Theme.fontSizeSmall
+                                            visible: model.playTime !== ""
+                                        }
+                                    }
+                                }
+
+                                Label {
+                                    text: model.slotName
+                                    color: Theme.textDim
+                                    font.pointSize: Theme.fontSizeLarge
+                                    Layout.fillWidth: true
+                                    horizontalAlignment: Text.AlignHCenter
+                                    visible: model.isEmpty
+                                }
+
+                                Button {
+                                    text: "Load"
+                                    highlighted: true
+                                    visible: !model.isEmpty
+                                    onClicked: {
+                                        root.loadRequested(model.slotName)
+                                    }
+                                }
+
+                                Button {
+                                    text: "Delete"
+                                    visible: !model.isEmpty
+                                    onClicked: {
+                                        confirmDeleteDialog.slotName = model.slotName
+                                        confirmDeleteDialog.slotIndex = index
+                                        confirmDeleteDialog.open()
+                                    }
+                                }
+                            }
+
+                            MouseArea {
+                                id: mouseArea
+                                anchors.fill: parent
+                                hoverEnabled: true
+                                enabled: !model.isEmpty
+                                onClicked: {
+                                    loadListView.selectedIndex = index
+                                }
+                                onDoubleClicked: {
+                                    if (!model.isEmpty) {
+                                        root.loadRequested(model.slotName)
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            // Load button at bottom
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: Theme.spacingMedium
+
+                Item {
+                    Layout.fillWidth: true
+                }
+
+                Label {
+                    text: loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty ? 
+                          "Selected: " + loadListModel.get(loadListView.selectedIndex).slotName : 
+                          "Select a save to load"
+                    color: Theme.textSub
+                    font.pointSize: Theme.fontSizeMedium
+                }
+
+                Button {
+                    text: "Load Selected"
+                    enabled: loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty
+                    highlighted: true
+                    onClicked: {
+                        if (loadListView.selectedIndex >= 0) {
+                            root.loadRequested(loadListModel.get(loadListView.selectedIndex).slotName)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    Dialog {
+        id: confirmDeleteDialog
+
+        property string slotName: ""
+        property int slotIndex: -1
+
+        anchors.centerIn: parent
+        width: Math.min(parent.width * 0.5, 400)
+        title: "Confirm Delete"
+        modal: true
+        standardButtons: Dialog.Yes | Dialog.No
+
+        onAccepted: {
+            if (typeof game !== 'undefined' && game.deleteSaveSlot) {
+                game.deleteSaveSlot(slotName)
+                loadListModel.remove(slotIndex)
+                
+                // Add empty message if no saves left
+                if (loadListModel.count === 0) {
+                    loadListModel.append({
+                        slotName: "No saves found",
+                        timestamp: 0,
+                        mapName: "",
+                        playTime: "",
+                        isEmpty: true
+                    })
+                }
+            }
+        }
+
+        contentItem: Rectangle {
+            color: Theme.cardBase
+            implicitHeight: warningText.implicitHeight + 40
+
+            ColumnLayout {
+                anchors.fill: parent
+                anchors.margins: Theme.spacingMedium
+                spacing: Theme.spacingMedium
+
+                Label {
+                    id: warningText
+                    text: "Are you sure you want to delete the save:\n\"" + confirmDeleteDialog.slotName + "\"?\n\nThis action cannot be undone."
+                    color: Theme.textMain
+                    wrapMode: Text.WordWrap
+                    Layout.fillWidth: true
+                    font.pointSize: Theme.fontSizeMedium
+                }
+            }
+        }
+    }
+
+    Keys.onPressed: function(event) {
+        if (event.key === Qt.Key_Escape) {
+            root.cancelled()
+            event.accepted = true
+        } else if (event.key === Qt.Key_Down) {
+            if (loadListView.selectedIndex < loadListModel.count - 1) {
+                loadListView.selectedIndex++
+            }
+            event.accepted = true
+        } else if (event.key === Qt.Key_Up) {
+            if (loadListView.selectedIndex > 0) {
+                loadListView.selectedIndex--
+            }
+            event.accepted = true
+        } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+            if (loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty) {
+                root.loadRequested(loadListModel.get(loadListView.selectedIndex).slotName)
+            }
+            event.accepted = true
+        }
+    }
+
+    Component.onCompleted: {
+        forceActiveFocus()
+        if (loadListModel.count > 0 && !loadListModel.get(0).isEmpty) {
+            loadListView.selectedIndex = 0
+        }
+    }
+}

+ 65 - 5
ui/qml/Main.qml

@@ -132,15 +132,20 @@ ApplicationWindow {
             mapSelect.visible = true;
             mapSelect.visible = true;
             mainWindow.menuVisible = false;
             mainWindow.menuVisible = false;
         }
         }
+        onSaveGame: function() {
+            if (mainWindow.gameStarted) {
+                saveGamePanel.visible = true;
+                mainWindow.menuVisible = false;
+            }
+        }
+        onLoadSave: function() {
+            loadGamePanel.visible = true;
+            mainWindow.menuVisible = false;
+        }
         onOpenSettings: function() {
         onOpenSettings: function() {
             if (typeof game !== 'undefined' && game.openSettings)
             if (typeof game !== 'undefined' && game.openSettings)
                 game.openSettings();
                 game.openSettings();
 
 
-        }
-        onLoadSave: function() {
-            if (typeof game !== 'undefined' && game.loadSave)
-                game.loadSave();
-
         }
         }
         onExitRequested: function() {
         onExitRequested: function() {
             if (typeof game !== 'undefined' && game.exitGame)
             if (typeof game !== 'undefined' && game.exitGame)
@@ -178,6 +183,61 @@ ApplicationWindow {
         }
         }
     }
     }
 
 
+    SaveGamePanel {
+        id: saveGamePanel
+
+        anchors.fill: parent
+        z: 22
+        visible: false
+        onVisibleChanged: {
+            if (visible) {
+                saveGamePanel.forceActiveFocus();
+                gameViewItem.focus = false;
+            }
+        }
+        onSaveRequested: function(slotName) {
+            console.log("Main: Save requested for slot:", slotName);
+            if (typeof game !== 'undefined' && game.saveGameToSlot) {
+                game.saveGameToSlot(slotName);
+            }
+            saveGamePanel.visible = false;
+            mainWindow.menuVisible = true;
+        }
+        onCancelled: function() {
+            saveGamePanel.visible = false;
+            mainWindow.menuVisible = true;
+        }
+    }
+
+    LoadGamePanel {
+        id: loadGamePanel
+
+        anchors.fill: parent
+        z: 22
+        visible: false
+        onVisibleChanged: {
+            if (visible) {
+                loadGamePanel.forceActiveFocus();
+                gameViewItem.focus = false;
+            }
+        }
+        onLoadRequested: function(slotName) {
+            console.log("Main: Load requested for slot:", slotName);
+            if (typeof game !== 'undefined' && game.loadGameFromSlot) {
+                game.loadGameFromSlot(slotName);
+                loadGamePanel.visible = false;
+                mainWindow.menuVisible = false;
+                mainWindow.gameStarted = true;
+                mainWindow.gamePaused = false;
+                gameViewItem.forceActiveFocus();
+            }
+        }
+        onCancelled: function() {
+            loadGamePanel.visible = false;
+            mainWindow.menuVisible = true;
+        }
+    }
+
     Item {
     Item {
         id: edgeScrollOverlay
         id: edgeScrollOverlay
 
 

+ 12 - 1
ui/qml/MainMenu.qml

@@ -10,6 +10,7 @@ Item {
     signal openSkirmish()
     signal openSkirmish()
     signal openSettings()
     signal openSettings()
     signal loadSave()
     signal loadSave()
+    signal saveGame()
     signal exitRequested()
     signal exitRequested()
 
 
     anchors.fill: parent
     anchors.fill: parent
@@ -26,6 +27,8 @@ Item {
             var m = menuModel.get(container.selectedIndex);
             var m = menuModel.get(container.selectedIndex);
             if (m.idStr === "skirmish")
             if (m.idStr === "skirmish")
                 root.openSkirmish();
                 root.openSkirmish();
+            else if (m.idStr === "save")
+                root.saveGame();
             else if (m.idStr === "load")
             else if (m.idStr === "load")
                 root.loadSave();
                 root.loadSave();
             else if (m.idStr === "settings")
             else if (m.idStr === "settings")
@@ -107,9 +110,15 @@ Item {
                         subtitle: "Select a map and start"
                         subtitle: "Select a map and start"
                     }
                     }
 
 
+                    ListElement {
+                        idStr: "save"
+                        title: "Save Game"
+                        subtitle: "Save your current progress"
+                    }
+
                     ListElement {
                     ListElement {
                         idStr: "load"
                         idStr: "load"
-                        title: "Load Save"
+                        title: "Load Game"
                         subtitle: "Resume a previous game"
                         subtitle: "Resume a previous game"
                     }
                     }
 
 
@@ -214,6 +223,8 @@ Item {
                             onClicked: {
                             onClicked: {
                                 if (model.idStr === "skirmish")
                                 if (model.idStr === "skirmish")
                                     root.openSkirmish();
                                     root.openSkirmish();
+                                else if (model.idStr === "save")
+                                    root.saveGame();
                                 else if (model.idStr === "load")
                                 else if (model.idStr === "load")
                                     root.loadSave();
                                     root.loadSave();
                                 else if (model.idStr === "settings")
                                 else if (model.idStr === "settings")

+ 270 - 0
ui/qml/SaveGamePanel.qml

@@ -0,0 +1,270 @@
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.3
+import StandardOfIron.UI 1.0
+
+Item {
+    id: root
+
+    signal cancelled()
+    signal saveRequested(string slotName)
+
+    anchors.fill: parent
+    z: 25
+
+    Rectangle {
+        anchors.fill: parent
+        color: Theme.dim
+    }
+
+    Rectangle {
+        id: container
+
+        width: Math.min(parent.width * 0.7, 900)
+        height: Math.min(parent.height * 0.8, 600)
+        anchors.centerIn: parent
+        radius: Theme.radiusPanel
+        color: Theme.panelBase
+        border.color: Theme.panelBr
+        border.width: 1
+        opacity: 0.98
+
+        ColumnLayout {
+            anchors.fill: parent
+            anchors.margins: Theme.spacingXLarge
+            spacing: Theme.spacingLarge
+
+            // Header
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: Theme.spacingMedium
+
+                Label {
+                    text: "Save Game"
+                    color: Theme.textMain
+                    font.pointSize: Theme.fontSizeHero
+                    font.bold: true
+                    Layout.fillWidth: true
+                }
+
+                Button {
+                    text: "Cancel"
+                    onClicked: root.cancelled()
+                }
+            }
+
+            Rectangle {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 1
+                color: Theme.border
+            }
+
+            // Save slot name input
+            RowLayout {
+                Layout.fillWidth: true
+                spacing: Theme.spacingMedium
+
+                Label {
+                    text: "Save Name:"
+                    color: Theme.textSub
+                    font.pointSize: Theme.fontSizeMedium
+                }
+
+                TextField {
+                    id: saveNameField
+                    
+                    Layout.fillWidth: true
+                    placeholderText: "Enter save name..."
+                    text: "Save_" + Qt.formatDateTime(new Date(), "yyyy-MM-dd_HH-mm")
+                    font.pointSize: Theme.fontSizeMedium
+                    
+                    background: Rectangle {
+                        color: Theme.cardBase
+                        border.color: saveNameField.activeFocus ? Theme.accent : Theme.border
+                        border.width: 1
+                        radius: Theme.radiusMedium
+                    }
+                    
+                    color: Theme.textMain
+                }
+
+                Button {
+                    text: "Save"
+                    enabled: saveNameField.text.length > 0
+                    highlighted: true
+                    onClicked: {
+                        if (saveListModel.slotExists(saveNameField.text)) {
+                            confirmOverwriteDialog.slotName = saveNameField.text
+                            confirmOverwriteDialog.open()
+                        } else {
+                            root.saveRequested(saveNameField.text)
+                        }
+                    }
+                }
+            }
+
+            // Existing saves list
+            Label {
+                text: "Existing Saves"
+                color: Theme.textSub
+                font.pointSize: Theme.fontSizeMedium
+            }
+
+            Rectangle {
+                Layout.fillWidth: true
+                Layout.fillHeight: true
+                color: Theme.cardBase
+                border.color: Theme.border
+                border.width: 1
+                radius: Theme.radiusLarge
+
+                ScrollView {
+                    anchors.fill: parent
+                    anchors.margins: Theme.spacingSmall
+                    clip: true
+
+                    ListView {
+                        id: saveListView
+
+                        model: ListModel {
+                            id: saveListModel
+
+                            function slotExists(name) {
+                                for (var i = 0; i < count; i++) {
+                                    if (get(i).slotName === name) {
+                                        return true
+                                    }
+                                }
+                                return false
+                            }
+
+                            Component.onCompleted: {
+                                // Populate with existing saves
+                                if (typeof game !== 'undefined' && game.getSaveSlots) {
+                                    var slots = game.getSaveSlots()
+                                    for (var i = 0; i < slots.length; i++) {
+                                        append({
+                                            slotName: slots[i].name,
+                                            timestamp: slots[i].timestamp,
+                                            mapName: slots[i].mapName || "Unknown Map"
+                                        })
+                                    }
+                                }
+                            }
+                        }
+
+                        spacing: Theme.spacingSmall
+                        delegate: Rectangle {
+                            width: saveListView.width
+                            height: 80
+                            color: mouseArea.containsMouse ? Theme.hoverBg : Qt.rgba(0, 0, 0, 0)
+                            radius: Theme.radiusMedium
+                            border.color: Theme.cardBorder
+                            border.width: 1
+
+                            RowLayout {
+                                anchors.fill: parent
+                                anchors.margins: Theme.spacingMedium
+                                spacing: Theme.spacingMedium
+
+                                ColumnLayout {
+                                    Layout.fillWidth: true
+                                    spacing: Theme.spacingTiny
+
+                                    Label {
+                                        text: model.slotName
+                                        color: Theme.textMain
+                                        font.pointSize: Theme.fontSizeLarge
+                                        font.bold: true
+                                        Layout.fillWidth: true
+                                        elide: Label.ElideRight
+                                    }
+
+                                    Label {
+                                        text: model.mapName
+                                        color: Theme.textSub
+                                        font.pointSize: Theme.fontSizeSmall
+                                        Layout.fillWidth: true
+                                        elide: Label.ElideRight
+                                    }
+
+                                    Label {
+                                        text: "Last saved: " + Qt.formatDateTime(new Date(model.timestamp), "yyyy-MM-dd hh:mm:ss")
+                                        color: Theme.textHint
+                                        font.pointSize: Theme.fontSizeSmall
+                                        Layout.fillWidth: true
+                                        elide: Label.ElideRight
+                                    }
+                                }
+
+                                Button {
+                                    text: "Overwrite"
+                                    onClicked: {
+                                        confirmOverwriteDialog.slotName = model.slotName
+                                        confirmOverwriteDialog.open()
+                                    }
+                                }
+                            }
+
+                            MouseArea {
+                                id: mouseArea
+                                anchors.fill: parent
+                                hoverEnabled: true
+                                onClicked: {
+                                    saveNameField.text = model.slotName
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    Dialog {
+        id: confirmOverwriteDialog
+
+        property string slotName: ""
+
+        anchors.centerIn: parent
+        width: Math.min(parent.width * 0.5, 400)
+        title: "Confirm Overwrite"
+        modal: true
+        standardButtons: Dialog.Yes | Dialog.No
+
+        onAccepted: {
+            root.saveRequested(slotName)
+        }
+
+        contentItem: Rectangle {
+            color: Theme.cardBase
+            implicitHeight: warningText.implicitHeight + 40
+
+            ColumnLayout {
+                anchors.fill: parent
+                anchors.margins: Theme.spacingMedium
+                spacing: Theme.spacingMedium
+
+                Label {
+                    id: warningText
+                    text: "Are you sure you want to overwrite the save:\n\"" + confirmOverwriteDialog.slotName + "\"?"
+                    color: Theme.textMain
+                    wrapMode: Text.WordWrap
+                    Layout.fillWidth: true
+                    font.pointSize: Theme.fontSizeMedium
+                }
+            }
+        }
+    }
+
+    Keys.onPressed: function(event) {
+        if (event.key === Qt.Key_Escape) {
+            root.cancelled()
+            event.accepted = true
+        }
+    }
+
+    Component.onCompleted: {
+        forceActiveFocus()
+    }
+}