Browse Source

Fix shader and map loading by adding QRC prefix and complete shader list

djeada 1 month ago
parent
commit
85e0f5814a

+ 33 - 2
CMakeLists.txt

@@ -135,9 +135,41 @@ if(QT_VERSION_MAJOR EQUAL 6)
             ui/qml/BattleSummary.qml
             ui/qml/GameView.qml
         RESOURCES
-            assets/shaders/basic.vert
+            assets/shaders/archer.frag
+            assets/shaders/archer.vert
             assets/shaders/basic.frag
+            assets/shaders/basic.vert
+            assets/shaders/bridge.frag
+            assets/shaders/bridge.vert
+            assets/shaders/cylinder_instanced.frag
+            assets/shaders/cylinder_instanced.vert
+            assets/shaders/firecamp.frag
+            assets/shaders/firecamp.vert
+            assets/shaders/fog_instanced.frag
+            assets/shaders/fog_instanced.vert
+            assets/shaders/grass_instanced.frag
+            assets/shaders/grass_instanced.vert
             assets/shaders/grid.frag
+            assets/shaders/ground_plane.frag
+            assets/shaders/ground_plane.vert
+            assets/shaders/knight.frag
+            assets/shaders/knight.vert
+            assets/shaders/mounted_knight.frag
+            assets/shaders/mounted_knight.vert
+            assets/shaders/pine_instanced.frag
+            assets/shaders/pine_instanced.vert
+            assets/shaders/plant_instanced.frag
+            assets/shaders/plant_instanced.vert
+            assets/shaders/river.frag
+            assets/shaders/river.vert
+            assets/shaders/riverbank.frag
+            assets/shaders/riverbank.vert
+            assets/shaders/spearman.frag
+            assets/shaders/spearman.vert
+            assets/shaders/stone_instanced.frag
+            assets/shaders/stone_instanced.vert
+            assets/shaders/terrain_chunk.frag
+            assets/shaders/terrain_chunk.vert
             assets/maps/map_forest.json
             assets/maps/map_rivers.json
             assets/maps/map_mountain.json
@@ -222,4 +254,3 @@ if (CLANG_FORMAT_EXE)
       COMMAND "${CMAKE_COMMAND}" -E echo "No C/C++ files found to check.")
   endif()
 endif()
-

+ 1 - 1
app/controllers/command_controller.cpp

@@ -116,7 +116,7 @@ CommandResult CommandController::onHoldCommand() {
 
     auto *unit = entity->getComponent<Engine::Core::UnitComponent>();
 
-    if (!unit || (unit->spawnType != Game::Units::SpawnType::Archer && 
+    if (!unit || (unit->spawnType != Game::Units::SpawnType::Archer &&
                   unit->spawnType != Game::Units::SpawnType::Spearman))
       continue;
 

+ 21 - 13
app/core/game_engine.cpp

@@ -1351,10 +1351,9 @@ void GameEngine::rebuildBuildingCollisions() {
     if (!transform || !unit)
       continue;
 
-    registry.registerBuilding(entity->getId(), 
-                              Game::Units::spawnTypeToString(unit->spawnType),
-                              transform->position.x, transform->position.z,
-                              unit->ownerId);
+    registry.registerBuilding(
+        entity->getId(), Game::Units::spawnTypeToString(unit->spawnType),
+        transform->position.x, transform->position.z, unit->ownerId);
   }
 }
 
@@ -1672,7 +1671,8 @@ void GameEngine::loadAudioResources() {
   QDir audioDir(basePath);
   if (!audioDir.exists()) {
     qWarning() << "Audio assets directory does not exist:" << basePath;
-    qWarning() << "Application directory:" << QCoreApplication::applicationDirPath();
+    qWarning() << "Application directory:"
+               << QCoreApplication::applicationDirPath();
     return;
   }
 
@@ -1681,7 +1681,8 @@ void GameEngine::loadAudioResources() {
                          AudioCategory::VOICE)) {
     qInfo() << "Loaded archer voice";
   } else {
-    qWarning() << "Failed to load archer voice from:" << (basePath + "voices/archer_voice.wav");
+    qWarning() << "Failed to load archer voice from:"
+               << (basePath + "voices/archer_voice.wav");
   }
 
   if (audioSys.loadSound("knight_voice",
@@ -1689,7 +1690,8 @@ void GameEngine::loadAudioResources() {
                          AudioCategory::VOICE)) {
     qInfo() << "Loaded knight voice";
   } else {
-    qWarning() << "Failed to load knight voice from:" << (basePath + "voices/knight_voice.wav");
+    qWarning() << "Failed to load knight voice from:"
+               << (basePath + "voices/knight_voice.wav");
   }
 
   if (audioSys.loadSound("spearman_voice",
@@ -1697,42 +1699,48 @@ void GameEngine::loadAudioResources() {
                          AudioCategory::VOICE)) {
     qInfo() << "Loaded spearman voice";
   } else {
-    qWarning() << "Failed to load spearman voice from:" << (basePath + "voices/spearman_voice.wav");
+    qWarning() << "Failed to load spearman voice from:"
+               << (basePath + "voices/spearman_voice.wav");
   }
 
   if (audioSys.loadMusic("music_peaceful",
                          (basePath + "music/peaceful.wav").toStdString())) {
     qInfo() << "Loaded peaceful music";
   } else {
-    qWarning() << "Failed to load peaceful music from:" << (basePath + "music/peaceful.wav");
+    qWarning() << "Failed to load peaceful music from:"
+               << (basePath + "music/peaceful.wav");
   }
 
   if (audioSys.loadMusic("music_tense",
                          (basePath + "music/tense.wav").toStdString())) {
     qInfo() << "Loaded tense music";
   } else {
-    qWarning() << "Failed to load tense music from:" << (basePath + "music/tense.wav");
+    qWarning() << "Failed to load tense music from:"
+               << (basePath + "music/tense.wav");
   }
 
   if (audioSys.loadMusic("music_combat",
                          (basePath + "music/combat.wav").toStdString())) {
     qInfo() << "Loaded combat music";
   } else {
-    qWarning() << "Failed to load combat music from:" << (basePath + "music/combat.wav");
+    qWarning() << "Failed to load combat music from:"
+               << (basePath + "music/combat.wav");
   }
 
   if (audioSys.loadMusic("music_victory",
                          (basePath + "music/victory.wav").toStdString())) {
     qInfo() << "Loaded victory music";
   } else {
-    qWarning() << "Failed to load victory music from:" << (basePath + "music/victory.wav");
+    qWarning() << "Failed to load victory music from:"
+               << (basePath + "music/victory.wav");
   }
 
   if (audioSys.loadMusic("music_defeat",
                          (basePath + "music/defeat.wav").toStdString())) {
     qInfo() << "Loaded defeat music";
   } else {
-    qWarning() << "Failed to load defeat music from:" << (basePath + "music/defeat.wav");
+    qWarning() << "Failed to load defeat music from:"
+               << (basePath + "music/defeat.wav");
   }
 
   qInfo() << "Audio resources loading complete";

+ 37 - 2
assets.qrc

@@ -1,13 +1,48 @@
 <RCC>
     <qresource prefix="/">
-        <file>assets/shaders/basic.vert</file>
+        <!-- Shader files -->
+        <file>assets/shaders/archer.frag</file>
+        <file>assets/shaders/archer.vert</file>
         <file>assets/shaders/basic.frag</file>
-        <file>assets/shaders/bridge.vert</file>
+        <file>assets/shaders/basic.vert</file>
         <file>assets/shaders/bridge.frag</file>
+        <file>assets/shaders/bridge.vert</file>
+        <file>assets/shaders/cylinder_instanced.frag</file>
+        <file>assets/shaders/cylinder_instanced.vert</file>
+        <file>assets/shaders/firecamp.frag</file>
+        <file>assets/shaders/firecamp.vert</file>
+        <file>assets/shaders/fog_instanced.frag</file>
+        <file>assets/shaders/fog_instanced.vert</file>
+        <file>assets/shaders/grass_instanced.frag</file>
+        <file>assets/shaders/grass_instanced.vert</file>
         <file>assets/shaders/grid.frag</file>
+        <file>assets/shaders/ground_plane.frag</file>
+        <file>assets/shaders/ground_plane.vert</file>
+        <file>assets/shaders/knight.frag</file>
+        <file>assets/shaders/knight.vert</file>
+        <file>assets/shaders/mounted_knight.frag</file>
+        <file>assets/shaders/mounted_knight.vert</file>
+        <file>assets/shaders/pine_instanced.frag</file>
+        <file>assets/shaders/pine_instanced.vert</file>
+        <file>assets/shaders/plant_instanced.frag</file>
+        <file>assets/shaders/plant_instanced.vert</file>
+        <file>assets/shaders/river.frag</file>
+        <file>assets/shaders/river.vert</file>
+        <file>assets/shaders/riverbank.frag</file>
+        <file>assets/shaders/riverbank.vert</file>
+        <file>assets/shaders/spearman.frag</file>
+        <file>assets/shaders/spearman.vert</file>
+        <file>assets/shaders/stone_instanced.frag</file>
+        <file>assets/shaders/stone_instanced.vert</file>
+        <file>assets/shaders/terrain_chunk.frag</file>
+        <file>assets/shaders/terrain_chunk.vert</file>
+        
+        <!-- Map files -->
         <file>assets/maps/map_forest.json</file>
         <file>assets/maps/map_rivers.json</file>
         <file>assets/maps/map_mountain.json</file>
+        
+        <!-- Visual config -->
         <file>assets/visuals/unit_visuals.json</file>
     </qresource>
 </RCC>

+ 2 - 1
game/audio/AudioEventHandler.cpp

@@ -91,7 +91,8 @@ void AudioEventHandler::onUnitSelected(
     return;
   }
 
-  std::string unitTypeStr = Game::Units::spawnTypeToString(unitComponent->spawnType);
+  std::string unitTypeStr =
+      Game::Units::spawnTypeToString(unitComponent->spawnType);
   auto it = m_unitVoiceMap.find(unitTypeStr);
   if (it != m_unitVoiceMap.end()) {
     auto now = std::chrono::steady_clock::now();

+ 2 - 1
game/core/event_manager.h

@@ -191,7 +191,8 @@ public:
 
 class UnitSpawnedEvent : public Event {
 public:
-  UnitSpawnedEvent(EntityID unitId, int ownerId, Game::Units::SpawnType spawnType)
+  UnitSpawnedEvent(EntityID unitId, int ownerId,
+                   Game::Units::SpawnType spawnType)
       : unitId(unitId), ownerId(ownerId), spawnType(spawnType) {}
   EntityID unitId;
   int ownerId;

+ 3 - 3
game/core/serialization.cpp

@@ -97,7 +97,7 @@ QJsonObject Serialization::serializeEntity(const Entity *entity) {
     unitObj["maxHealth"] = unit->maxHealth;
     unitObj["speed"] = unit->speed;
     unitObj["visionRange"] = unit->visionRange;
-    unitObj["unitType"] = 
+    unitObj["unitType"] =
         QString::fromStdString(Game::Units::spawnTypeToString(unit->spawnType));
     unitObj["ownerId"] = unit->ownerId;
     entityObj["unit"] = unitObj;
@@ -261,7 +261,7 @@ void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
     unit->speed = static_cast<float>(unitObj["speed"].toDouble());
     unit->visionRange =
         static_cast<float>(unitObj["visionRange"].toDouble(12.0));
-    
+
     QString unitTypeStr = unitObj["unitType"].toString();
     Game::Units::SpawnType spawnType;
     if (Game::Units::tryParseSpawnType(unitTypeStr, spawnType)) {
@@ -271,7 +271,7 @@ void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
                  << "- defaulting to Archer";
       unit->spawnType = Game::Units::SpawnType::Archer;
     }
-    
+
     unit->ownerId = unitObj["ownerId"].toInt(0);
   }
 

+ 10 - 4
game/map/level_loader.cpp

@@ -11,6 +11,7 @@
 #include "map_loader.h"
 #include "map_transformer.h"
 #include "terrain_service.h"
+#include "utils/resource_utils.h"
 #include <QDebug>
 
 namespace Game {
@@ -26,8 +27,9 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
 
   Game::Visuals::VisualCatalog visualCatalog;
   QString visualsErr;
-  if (!visualCatalog.loadFromJsonFile("assets/visuals/unit_visuals.json",
-                                      &visualsErr)) {
+  const QString visualsPath = Utils::Resources::resolveResourcePath(
+      QStringLiteral(":/assets/visuals/unit_visuals.json"));
+  if (!visualCatalog.loadFromJsonFile(visualsPath, &visualsErr)) {
     res.ok = false;
     res.errorMessage =
         QString("Failed to load visual catalog: %1").arg(visualsErr);
@@ -39,9 +41,12 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
   Game::Units::registerBuiltInUnits(*unitReg);
   Game::Map::MapTransformer::setFactoryRegistry(unitReg);
 
+  const QString resolvedMapPath =
+      Utils::Resources::resolveResourcePath(mapPath);
+
   Game::Map::MapDefinition def;
   QString err;
-  if (Game::Map::MapLoader::loadFromJsonFile(mapPath, def, &err)) {
+  if (Game::Map::MapLoader::loadFromJsonFile(resolvedMapPath, def, &err)) {
     res.ok = true;
     res.mapName = def.name;
 
@@ -104,7 +109,8 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
     res.ok = false;
     res.errorMessage = QString("Map load failed: %1").arg(err);
     qWarning() << "LevelLoader: Map load failed:" << err
-               << " - applying default environment";
+               << "(path:" << resolvedMapPath << ')'
+               << "- applying default environment";
     Game::Map::Environment::applyDefault(renderer, camera);
     res.ok = false;
     res.camFov = camera.getFOV();

+ 28 - 14
game/map/map_catalog.cpp

@@ -1,4 +1,5 @@
 #include "map_catalog.h"
+#include "utils/resource_utils.h"
 #include <QDir>
 #include <QFile>
 #include <QFileInfo>
@@ -18,14 +19,16 @@ MapCatalog::MapCatalog(QObject *parent) : QObject(parent) {}
 
 QVariantList MapCatalog::availableMaps() {
   QVariantList list;
-  QDir mapsDir(QStringLiteral("assets/maps"));
+  const QString mapsRoot =
+      Utils::Resources::resolveResourcePath(QStringLiteral(":/assets/maps"));
+  QDir mapsDir(mapsRoot);
   if (!mapsDir.exists())
     return list;
 
   QStringList files =
       mapsDir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
   for (const QString &f : files) {
-    QString path = mapsDir.filePath(f);
+    QString path = Utils::Resources::resolveResourcePath(mapsDir.filePath(f));
     QFile file(path);
     QString name = f;
     QString desc;
@@ -87,9 +90,12 @@ QVariantList MapCatalog::availableMaps() {
 
     if (thumbnail.isEmpty()) {
       QString baseName = QFileInfo(f).baseName();
-      thumbnail = QString("assets/maps/%1_thumb.png").arg(baseName);
+      QString thumbCandidate = Utils::Resources::resolveResourcePath(
+          QString(":/assets/maps/%1_thumb.png").arg(baseName));
 
-      if (!QFileInfo::exists(thumbnail)) {
+      if (QFileInfo::exists(thumbCandidate)) {
+        thumbnail = thumbCandidate;
+      } else {
         thumbnail = "";
       }
     }
@@ -109,7 +115,9 @@ void MapCatalog::loadMapsAsync() {
   m_loading = true;
   emit loadingChanged(true);
 
-  QDir mapsDir(QStringLiteral("assets/maps"));
+  const QString mapsRoot =
+      Utils::Resources::resolveResourcePath(QStringLiteral(":/assets/maps"));
+  QDir mapsDir(mapsRoot);
   if (!mapsDir.exists()) {
     m_loading = false;
     emit loadingChanged(false);
@@ -139,8 +147,11 @@ void MapCatalog::loadNextMap() {
   }
 
   QString fileName = m_pendingFiles.takeFirst();
-  QDir mapsDir(QStringLiteral("assets/maps"));
-  QString path = mapsDir.filePath(fileName);
+  const QString mapsRoot =
+      Utils::Resources::resolveResourcePath(QStringLiteral(":/assets/maps"));
+  QDir mapsDir(mapsRoot);
+  QString path =
+      Utils::Resources::resolveResourcePath(mapsDir.filePath(fileName));
 
   QVariantMap entry = loadSingleMap(path);
   if (!entry.isEmpty()) {
@@ -158,8 +169,9 @@ void MapCatalog::loadNextMap() {
 }
 
 QVariantMap MapCatalog::loadSingleMap(const QString &path) {
-  QFile file(path);
-  QString name = QFileInfo(path).fileName();
+  const QString resolvedPath = Utils::Resources::resolveResourcePath(path);
+  QFile file(resolvedPath);
+  QString name = QFileInfo(resolvedPath).fileName();
   QString desc;
   QSet<int> playerIds;
 
@@ -195,7 +207,7 @@ QVariantMap MapCatalog::loadSingleMap(const QString &path) {
   QVariantMap entry;
   entry["name"] = name;
   entry["description"] = desc;
-  entry["path"] = path;
+  entry["path"] = resolvedPath;
   entry["playerCount"] = playerIds.size();
 
   QVariantList playerIdList;
@@ -221,12 +233,14 @@ QVariantMap MapCatalog::loadSingleMap(const QString &path) {
   }
 
   if (thumbnail.isEmpty()) {
-    QString baseName = QFileInfo(path).baseName();
-    thumbnail = QString("assets/maps/%1_thumb.png").arg(baseName);
+    QString baseName = QFileInfo(resolvedPath).baseName();
+    QString thumbCandidate = Utils::Resources::resolveResourcePath(
+        QString(":/assets/maps/%1_thumb.png").arg(baseName));
 
-    if (!QFileInfo::exists(thumbnail)) {
+    if (QFileInfo::exists(thumbCandidate))
+      thumbnail = thumbCandidate;
+    else
       thumbnail = "";
-    }
   }
   entry["thumbnail"] = thumbnail;
 

+ 1 - 1
game/map/map_loader.cpp

@@ -234,7 +234,7 @@ static void readTerrain(const QJsonArray &arr, std::vector<TerrainFeature> &out,
 
     const QString typeStr = o.value("type").toString("flat");
     if (!tryParseTerrainType(typeStr, feature.type)) {
-      qWarning() << "MapLoader: unknown terrain type" << typeStr 
+      qWarning() << "MapLoader: unknown terrain type" << typeStr
                  << "- defaulting to flat";
       feature.type = TerrainType::Flat;
     }

+ 5 - 7
game/map/skirmish_loader.cpp

@@ -167,15 +167,14 @@ SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
     }
   }
 
-  // Validate that we have at least 2 distinct teams for multiplayer matches (2+ players).
-  // Single-player practice mode is exempt from team validation.
   std::set<int> uniqueTeams;
   for (const auto &[playerId, teamId] : teamOverrides) {
     uniqueTeams.insert(teamId);
   }
-  
+
   if (teamOverrides.size() >= 2 && uniqueTeams.size() < 2) {
-    result.errorMessage = "Invalid team configuration: At least two teams must be selected to start a match.";
+    result.errorMessage = "Invalid team configuration: At least two teams must "
+                          "be selected to start a match.";
     m_renderer.unlockWorldForModification();
     m_renderer.resume();
     qWarning() << "SkirmishLoader: " << result.errorMessage;
@@ -366,9 +365,8 @@ SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
     auto *u = e->getComponent<Engine::Core::UnitComponent>();
     if (!u)
       continue;
-    if (u->spawnType == Game::Units::SpawnType::Barracks && 
-        u->ownerId == playerOwnerId &&
-        u->health > 0) {
+    if (u->spawnType == Game::Units::SpawnType::Barracks &&
+        u->ownerId == playerOwnerId && u->health > 0) {
       focusEntity = e;
       break;
     }

+ 3 - 5
game/map/terrain.h

@@ -1,7 +1,7 @@
 #pragma once
 
-#include <QVector3D>
 #include <QString>
+#include <QVector3D>
 #include <cstdint>
 #include <memory>
 #include <optional>
@@ -12,7 +12,6 @@ namespace Game::Map {
 
 enum class TerrainType { Flat, Hill, Mountain, River };
 
-// String conversion utilities for TerrainType
 inline QString terrainTypeToQString(TerrainType type) {
   switch (type) {
   case TerrainType::Flat:
@@ -31,7 +30,6 @@ inline std::string terrainTypeToString(TerrainType type) {
   return terrainTypeToQString(type).toStdString();
 }
 
-// Case-insensitive parsing with validation
 inline bool tryParseTerrainType(const QString &value, TerrainType &out) {
   const QString lowered = value.trimmed().toLower();
   if (lowered == QStringLiteral("flat")) {
@@ -53,8 +51,8 @@ inline bool tryParseTerrainType(const QString &value, TerrainType &out) {
   return false;
 }
 
-// For std::string compatibility
-inline std::optional<TerrainType> terrainTypeFromString(const std::string &str) {
+inline std::optional<TerrainType>
+terrainTypeFromString(const std::string &str) {
   TerrainType result;
   if (tryParseTerrainType(QString::fromStdString(str), result)) {
     return result;

+ 1 - 1
game/systems/ai_system/ai_reasoner.cpp

@@ -66,7 +66,7 @@ void AIReasoner::updateContext(const AISnapshot &snapshot, AIContext &ctx) {
     if (entity.isBuilding) {
       ctx.buildings.push_back(entity.id);
 
-      if (entity.spawnType == Game::Units::SpawnType::Barracks && 
+      if (entity.spawnType == Game::Units::SpawnType::Barracks &&
           ctx.primaryBarracks == 0) {
         ctx.primaryBarracks = entity.id;
         ctx.rallyX = entity.posX - 5.0f;

+ 1 - 1
game/systems/ai_system/behaviors/production_behavior.cpp

@@ -52,7 +52,7 @@ void ProductionBehavior::execute(const AISnapshot &snapshot, AIContext &context,
   }
 
   for (const auto &entity : snapshot.friendlies) {
-    if (!entity.isBuilding || 
+    if (!entity.isBuilding ||
         entity.spawnType != Game::Units::SpawnType::Barracks)
       continue;
 

+ 2 - 3
game/systems/camera_service.cpp

@@ -90,9 +90,8 @@ void CameraService::resetCamera(Render::GL::Camera &camera,
     auto *u = e->getComponent<Engine::Core::UnitComponent>();
     if (!u)
       continue;
-    if (u->spawnType == Game::Units::SpawnType::Barracks && 
-        u->ownerId == localOwnerId &&
-        u->health > 0) {
+    if (u->spawnType == Game::Units::SpawnType::Barracks &&
+        u->ownerId == localOwnerId && u->health > 0) {
       focusEntity = e;
       break;
     }

+ 2 - 1
game/systems/combat_system.cpp

@@ -125,7 +125,8 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
 
           range *= 1.5f;
           damage = static_cast<int>(damage * 1.3f);
-        } else if (attackerUnit->spawnType == Game::Units::SpawnType::Spearman) {
+        } else if (attackerUnit->spawnType ==
+                   Game::Units::SpawnType::Spearman) {
 
           damage = static_cast<int>(damage * 1.4f);
         }

+ 2 - 1
game/systems/selection_system.cpp

@@ -173,7 +173,8 @@ bool SelectionController::hasSelectedType(const QString &type) const {
   for (auto id : sel) {
     if (auto *e = m_world->getEntity(id)) {
       if (auto *u = e->getComponent<Engine::Core::UnitComponent>()) {
-        if (QString::fromStdString(Game::Units::spawnTypeToString(u->spawnType)) == type)
+        if (QString::fromStdString(
+                Game::Units::spawnTypeToString(u->spawnType)) == type)
           return true;
       }
     }

+ 8 - 8
game/systems/victory_service.cpp

@@ -33,7 +33,7 @@ void VictoryService::reset() {
 void VictoryService::configure(const Game::Map::VictoryConfig &config,
                                int localOwnerId) {
   reset();
-  
+
   m_localOwnerId = localOwnerId;
 
   if (config.victoryType == "elimination") {
@@ -200,10 +200,10 @@ bool VictoryService::checkElimination(Engine::Core::World &world) {
     if (m_ownerRegistry.areAllies(m_localOwnerId, unit->ownerId))
       continue;
 
-    QString unitTypeStr = 
+    QString unitTypeStr =
         QString::fromStdString(Game::Units::spawnTypeToString(unit->spawnType));
-    if (std::find(m_keyStructures.begin(), m_keyStructures.end(), unitTypeStr) !=
-        m_keyStructures.end()) {
+    if (std::find(m_keyStructures.begin(), m_keyStructures.end(),
+                  unitTypeStr) != m_keyStructures.end()) {
       enemyKeyStructuresAlive = true;
       break;
     }
@@ -241,10 +241,10 @@ bool VictoryService::checkNoKeyStructures(Engine::Core::World &world) {
       continue;
 
     if (unit->ownerId == m_localOwnerId) {
-      QString unitTypeStr = 
-          QString::fromStdString(Game::Units::spawnTypeToString(unit->spawnType));
-      if (std::find(m_keyStructures.begin(), m_keyStructures.end(), unitTypeStr) !=
-          m_keyStructures.end()) {
+      QString unitTypeStr = QString::fromStdString(
+          Game::Units::spawnTypeToString(unit->spawnType));
+      if (std::find(m_keyStructures.begin(), m_keyStructures.end(),
+                    unitTypeStr) != m_keyStructures.end()) {
         return false;
       }
     }

+ 4 - 3
game/units/spawn_type.h

@@ -54,8 +54,7 @@ inline bool tryParseSpawnType(const QString &value, SpawnType &out) {
   return false;
 }
 
-inline std::optional<SpawnType> 
-spawnTypeFromString(const std::string &str) {
+inline std::optional<SpawnType> spawnTypeFromString(const std::string &str) {
   if (str == "archer")
     return SpawnType::Archer;
   if (str == "knight")
@@ -71,7 +70,9 @@ spawnTypeFromString(const std::string &str) {
 
 inline bool isTroopSpawn(SpawnType type) { return type != SpawnType::Barracks; }
 
-inline bool isBuildingSpawn(SpawnType type) { return type == SpawnType::Barracks; }
+inline bool isBuildingSpawn(SpawnType type) {
+  return type == SpawnType::Barracks;
+}
 
 inline std::optional<TroopType> spawnTypeToTroopType(SpawnType type) {
   switch (type) {

+ 3 - 5
game/units/troop_type.h

@@ -29,7 +29,6 @@ inline std::string troopTypeToString(TroopType type) {
   return troopTypeToQString(type).toStdString();
 }
 
-// Case-insensitive parsing with validation (preferred)
 inline bool tryParseTroopType(const QString &value, TroopType &out) {
   const QString lowered = value.trimmed().toLower();
   if (lowered == QStringLiteral("archer")) {
@@ -44,23 +43,22 @@ inline bool tryParseTroopType(const QString &value, TroopType &out) {
     out = TroopType::Spearman;
     return true;
   }
-  if (lowered == QStringLiteral("mounted_knight") || lowered == QStringLiteral("mountedknight")) {
+  if (lowered == QStringLiteral("mounted_knight") ||
+      lowered == QStringLiteral("mountedknight")) {
     out = TroopType::MountedKnight;
     return true;
   }
   return false;
 }
 
-// Deprecated: use tryParseTroopType(QString, TroopType&) instead
 inline TroopType troopTypeFromString(const std::string &str) {
   TroopType result;
   if (tryParseTroopType(QString::fromStdString(str), result)) {
     return result;
   }
-  return TroopType::Archer; // fallback for backward compatibility
+  return TroopType::Archer;
 }
 
-// For std::string compatibility
 inline std::optional<TroopType> tryParseTroopType(const std::string &str) {
   TroopType result;
   if (tryParseTroopType(QString::fromStdString(str), result)) {

+ 21 - 6
render/gl/shader.cpp

@@ -1,4 +1,5 @@
 #include "shader.h"
+#include "utils/resource_utils.h"
 #include <QByteArray>
 #include <QDebug>
 #include <QFile>
@@ -16,12 +17,26 @@ Shader::~Shader() {
 
 bool Shader::loadFromFiles(const QString &vertexPath,
                            const QString &fragmentPath) {
-  QFile vertexFile(vertexPath);
-  QFile fragmentFile(fragmentPath);
+  const QString resolvedVert =
+      Utils::Resources::resolveResourcePath(vertexPath);
+  const QString resolvedFrag =
+      Utils::Resources::resolveResourcePath(fragmentPath);
+
+  QFile vertexFile(resolvedVert);
+  QFile fragmentFile(resolvedFrag);
+
+  if (!vertexFile.open(QIODevice::ReadOnly)) {
+    qWarning() << "Failed to open vertex shader file:" << resolvedVert;
+    if (resolvedVert != vertexPath)
+      qWarning() << "  Requested path:" << vertexPath;
+    return false;
+  }
 
-  if (!vertexFile.open(QIODevice::ReadOnly) ||
-      !fragmentFile.open(QIODevice::ReadOnly)) {
-    qWarning() << "Failed to open shader files";
+  if (!fragmentFile.open(QIODevice::ReadOnly)) {
+    qWarning() << "Failed to open fragment shader file:" << resolvedFrag;
+    if (resolvedFrag != fragmentPath)
+      qWarning() << "  Requested path:" << fragmentPath;
+    vertexFile.close();
     return false;
   }
 
@@ -214,4 +229,4 @@ bool Shader::linkProgram(GLuint vertexShader, GLuint fragmentShader) {
   return true;
 }
 
-} // namespace Render::GL
+} // namespace Render::GL

+ 74 - 39
render/gl/shader_cache.h

@@ -1,6 +1,8 @@
 #pragma once
 
 #include "shader.h"
+#include "utils/resource_utils.h"
+#include <QDebug>
 #include <QString>
 #include <memory>
 #include <unordered_map>
@@ -14,9 +16,15 @@ public:
     auto it = m_named.find(name);
     if (it != m_named.end())
       return it->second.get();
+    const QString resolvedVert =
+        Utils::Resources::resolveResourcePath(vertPath);
+    const QString resolvedFrag =
+        Utils::Resources::resolveResourcePath(fragPath);
     auto sh = std::make_unique<Shader>();
-    if (!sh->loadFromFiles(vertPath, fragPath))
+    if (!sh->loadFromFiles(resolvedVert, resolvedFrag)) {
+      qWarning() << "ShaderCache: Failed to load shader" << name;
       return nullptr;
+    }
     Shader *raw = sh.get();
     m_named.emplace(name, std::move(sh));
     return raw;
@@ -28,104 +36,131 @@ public:
   }
 
   Shader *getOrLoad(const QString &vertPath, const QString &fragPath) {
-    auto key = vertPath + "|" + fragPath;
+    const QString resolvedVert =
+        Utils::Resources::resolveResourcePath(vertPath);
+    const QString resolvedFrag =
+        Utils::Resources::resolveResourcePath(fragPath);
+    auto key = resolvedVert + "|" + resolvedFrag;
     auto it = m_byPath.find(key);
     if (it != m_byPath.end())
       return it->second.get();
     auto sh = std::make_unique<Shader>();
-    if (!sh->loadFromFiles(vertPath, fragPath))
+    if (!sh->loadFromFiles(resolvedVert, resolvedFrag)) {
+      qWarning() << "ShaderCache: Failed to load shader from paths:"
+                 << resolvedVert << "," << resolvedFrag;
       return nullptr;
+    }
     Shader *raw = sh.get();
     m_byPath.emplace(std::move(key), std::move(sh));
     return raw;
   }
 
   void initializeDefaults() {
-    static const QString kShaderBase = QStringLiteral("assets/shaders/");
-    const QString basicVert = kShaderBase + QStringLiteral("basic.vert");
-    const QString basicFrag = kShaderBase + QStringLiteral("basic.frag");
-    const QString gridFrag = kShaderBase + QStringLiteral("grid.frag");
+    static const QString kShaderBase = QStringLiteral(":/assets/shaders/");
+    auto resolve = [](const QString &path) {
+      return Utils::Resources::resolveResourcePath(path);
+    };
+
+    const QString basicVert =
+        resolve(kShaderBase + QStringLiteral("basic.vert"));
+    const QString basicFrag =
+        resolve(kShaderBase + QStringLiteral("basic.frag"));
+    const QString gridFrag = resolve(kShaderBase + QStringLiteral("grid.frag"));
     load(QStringLiteral("basic"), basicVert, basicFrag);
     load(QStringLiteral("grid"), basicVert, gridFrag);
     const QString cylVert =
-        kShaderBase + QStringLiteral("cylinder_instanced.vert");
+        resolve(kShaderBase + QStringLiteral("cylinder_instanced.vert"));
     const QString cylFrag =
-        kShaderBase + QStringLiteral("cylinder_instanced.frag");
+        resolve(kShaderBase + QStringLiteral("cylinder_instanced.frag"));
     load(QStringLiteral("cylinder_instanced"), cylVert, cylFrag);
-    const QString fogVert = kShaderBase + QStringLiteral("fog_instanced.vert");
-    const QString fogFrag = kShaderBase + QStringLiteral("fog_instanced.frag");
+    const QString fogVert =
+        resolve(kShaderBase + QStringLiteral("fog_instanced.vert"));
+    const QString fogFrag =
+        resolve(kShaderBase + QStringLiteral("fog_instanced.frag"));
     load(QStringLiteral("fog_instanced"), fogVert, fogFrag);
     const QString grassVert =
-        kShaderBase + QStringLiteral("grass_instanced.vert");
+        resolve(kShaderBase + QStringLiteral("grass_instanced.vert"));
     const QString grassFrag =
-        kShaderBase + QStringLiteral("grass_instanced.frag");
+        resolve(kShaderBase + QStringLiteral("grass_instanced.frag"));
     load(QStringLiteral("grass_instanced"), grassVert, grassFrag);
 
     const QString stoneVert =
-        kShaderBase + QStringLiteral("stone_instanced.vert");
+        resolve(kShaderBase + QStringLiteral("stone_instanced.vert"));
     const QString stoneFrag =
-        kShaderBase + QStringLiteral("stone_instanced.frag");
+        resolve(kShaderBase + QStringLiteral("stone_instanced.frag"));
     load(QStringLiteral("stone_instanced"), stoneVert, stoneFrag);
 
     const QString plantVert =
-        kShaderBase + QStringLiteral("plant_instanced.vert");
+        resolve(kShaderBase + QStringLiteral("plant_instanced.vert"));
     const QString plantFrag =
-        kShaderBase + QStringLiteral("plant_instanced.frag");
+        resolve(kShaderBase + QStringLiteral("plant_instanced.frag"));
     load(QStringLiteral("plant_instanced"), plantVert, plantFrag);
 
     const QString pineVert =
-        kShaderBase + QStringLiteral("pine_instanced.vert");
+        resolve(kShaderBase + QStringLiteral("pine_instanced.vert"));
     const QString pineFrag =
-        kShaderBase + QStringLiteral("pine_instanced.frag");
+        resolve(kShaderBase + QStringLiteral("pine_instanced.frag"));
     load(QStringLiteral("pine_instanced"), pineVert, pineFrag);
 
-    const QString firecampVert = kShaderBase + QStringLiteral("firecamp.vert");
-    const QString firecampFrag = kShaderBase + QStringLiteral("firecamp.frag");
+    const QString firecampVert =
+        resolve(kShaderBase + QStringLiteral("firecamp.vert"));
+    const QString firecampFrag =
+        resolve(kShaderBase + QStringLiteral("firecamp.frag"));
     load(QStringLiteral("firecamp"), firecampVert, firecampFrag);
 
     const QString groundVert =
-        kShaderBase + QStringLiteral("ground_plane.vert");
+        resolve(kShaderBase + QStringLiteral("ground_plane.vert"));
     const QString groundFrag =
-        kShaderBase + QStringLiteral("ground_plane.frag");
+        resolve(kShaderBase + QStringLiteral("ground_plane.frag"));
     load(QStringLiteral("ground_plane"), groundVert, groundFrag);
 
     const QString terrainVert =
-        kShaderBase + QStringLiteral("terrain_chunk.vert");
+        resolve(kShaderBase + QStringLiteral("terrain_chunk.vert"));
     const QString terrainFrag =
-        kShaderBase + QStringLiteral("terrain_chunk.frag");
+        resolve(kShaderBase + QStringLiteral("terrain_chunk.frag"));
     load(QStringLiteral("terrain_chunk"), terrainVert, terrainFrag);
 
-    const QString riverVert = kShaderBase + QStringLiteral("river.vert");
-    const QString riverFrag = kShaderBase + QStringLiteral("river.frag");
+    const QString riverVert =
+        resolve(kShaderBase + QStringLiteral("river.vert"));
+    const QString riverFrag =
+        resolve(kShaderBase + QStringLiteral("river.frag"));
     load(QStringLiteral("river"), riverVert, riverFrag);
 
     const QString riverbankVert =
-        kShaderBase + QStringLiteral("riverbank.vert");
+        resolve(kShaderBase + QStringLiteral("riverbank.vert"));
     const QString riverbankFrag =
-        kShaderBase + QStringLiteral("riverbank.frag");
+        resolve(kShaderBase + QStringLiteral("riverbank.frag"));
     load(QStringLiteral("riverbank"), riverbankVert, riverbankFrag);
 
-    const QString bridgeVert = kShaderBase + QStringLiteral("bridge.vert");
-    const QString bridgeFrag = kShaderBase + QStringLiteral("bridge.frag");
+    const QString bridgeVert =
+        resolve(kShaderBase + QStringLiteral("bridge.vert"));
+    const QString bridgeFrag =
+        resolve(kShaderBase + QStringLiteral("bridge.frag"));
     load(QStringLiteral("bridge"), bridgeVert, bridgeFrag);
 
-    const QString archerVert = kShaderBase + QStringLiteral("archer.vert");
-    const QString archerFrag = kShaderBase + QStringLiteral("archer.frag");
+    const QString archerVert =
+        resolve(kShaderBase + QStringLiteral("archer.vert"));
+    const QString archerFrag =
+        resolve(kShaderBase + QStringLiteral("archer.frag"));
     load(QStringLiteral("archer"), archerVert, archerFrag);
 
-    const QString knightVert = kShaderBase + QStringLiteral("knight.vert");
-    const QString knightFrag = kShaderBase + QStringLiteral("knight.frag");
+    const QString knightVert =
+        resolve(kShaderBase + QStringLiteral("knight.vert"));
+    const QString knightFrag =
+        resolve(kShaderBase + QStringLiteral("knight.frag"));
     load(QStringLiteral("knight"), knightVert, knightFrag);
 
     const QString mountedKnightVert =
-        kShaderBase + QStringLiteral("mounted_knight.vert");
+        resolve(kShaderBase + QStringLiteral("mounted_knight.vert"));
     const QString mountedKnightFrag =
-        kShaderBase + QStringLiteral("mounted_knight.frag");
+        resolve(kShaderBase + QStringLiteral("mounted_knight.frag"));
     load(QStringLiteral("mounted_knight"), mountedKnightVert,
          mountedKnightFrag);
 
-    const QString spearmanVert = kShaderBase + QStringLiteral("spearman.vert");
-    const QString spearmanFrag = kShaderBase + QStringLiteral("spearman.frag");
+    const QString spearmanVert =
+        resolve(kShaderBase + QStringLiteral("spearman.vert"));
+    const QString spearmanFrag =
+        resolve(kShaderBase + QStringLiteral("spearman.frag"));
     load(QStringLiteral("spearman"), spearmanVert, spearmanFrag);
   }
 

+ 3 - 2
render/scene_renderer.cpp

@@ -311,7 +311,7 @@ void Renderer::renderWorld(Engine::Core::World *world) {
     if (m_camera && unitComp) {
 
       float cullRadius = 3.0f;
-      
+
       if (unitComp->spawnType == Game::Units::SpawnType::MountedKnight) {
         cullRadius = 4.0f;
       } else if (unitComp->spawnType == Game::Units::SpawnType::Spearman ||
@@ -350,7 +350,8 @@ void Renderer::renderWorld(Engine::Core::World *world) {
 
     bool drawnByRegistry = false;
     if (unitComp && m_entityRegistry) {
-      std::string unitTypeStr = Game::Units::spawnTypeToString(unitComp->spawnType);
+      std::string unitTypeStr =
+          Game::Units::spawnTypeToString(unitComp->spawnType);
       auto fn = m_entityRegistry->get(unitTypeStr);
       if (fn) {
         DrawContext ctx{resources(), entity, world, modelMatrix};

+ 9 - 14
ui/qml/MapSelect.qml

@@ -19,6 +19,7 @@ Item {
     function hasMinimumDistinctTeams() {
         if (playersModel.count < 2)
             return false;
+
         let teams = new Set();
         for (let i = 0; i < playersModel.count; i++) {
             teams.add(playersModel.get(i).teamId);
@@ -27,13 +28,12 @@ Item {
     }
 
     function updateValidationError() {
-        if (playersModel.count < 2) {
+        if (playersModel.count < 2)
             validationError = "Need at least 2 players to start";
-        } else if (!hasMinimumDistinctTeams()) {
+        else if (!hasMinimumDistinctTeams())
             validationError = "At least two teams must be selected to start a match";
-        } else {
+        else
             validationError = "";
-        }
     }
 
     function field(obj, key) {
@@ -74,7 +74,7 @@ Item {
         });
         if (cpuId !== undefined)
             addCPU();
-        
+
         updateValidationError();
     }
 
@@ -106,10 +106,7 @@ Item {
                 break;
             }
         }
-        
-        // Set CPU to a different team (team 1) by default to ensure valid match setup
         let defaultTeamId = playersModel.count > 0 ? 1 : 0;
-        
         playersModel.append({
             "playerId": nextId,
             "playerName": "CPU " + nextId,
@@ -122,7 +119,6 @@ Item {
             "factionName": Theme.factions[0].name,
             "isHuman": false
         });
-        
         updateValidationError();
     }
 
@@ -202,14 +198,11 @@ Item {
             updateValidationError();
             return ;
         }
-        
-        // Check for at least 2 distinct teams
         if (!hasMinimumDistinctTeams()) {
             console.log("MapSelect: Need at least 2 different teams to start");
             updateValidationError();
             return ;
         }
-        
         validationError = "";
         let configs = getPlayerConfigs();
         console.log("MapSelect: Starting game with", playersModel.count, "players");
@@ -1362,7 +1355,7 @@ Item {
 
             Text {
                 id: validationErrorText
-                
+
                 text: validationError
                 visible: validationError !== ""
                 color: Theme.removeColor
@@ -1370,7 +1363,7 @@ Item {
                 font.bold: true
                 wrapMode: Text.WordWrap
                 horizontalAlignment: Text.AlignHCenter
-                
+
                 anchors {
                     left: parent.left
                     right: parent.right
@@ -1378,6 +1371,7 @@ Item {
                     leftMargin: 140
                     rightMargin: 140
                 }
+
             }
 
             Button {
@@ -1471,6 +1465,7 @@ Item {
                 ToolTip.text: {
                     if (validationError !== "")
                         return validationError;
+
                     return qsTr("Start game (Enter)");
                 }
 

+ 47 - 0
utils/resource_utils.h

@@ -0,0 +1,47 @@
+#pragma once
+
+#include <QDir>
+#include <QFileInfo>
+#include <QString>
+#include <QStringList>
+
+namespace Utils::Resources {
+
+// Resolve resources that may have been relocated under a Qt QML module prefix.
+inline QString resolveResourcePath(const QString &path) {
+  if (path.isEmpty())
+    return path;
+
+  auto exists = [](const QString &candidate) {
+    QFileInfo info(candidate);
+    if (info.exists())
+      return true;
+    QDir dir(candidate);
+    return dir.exists();
+  };
+
+  if (exists(path))
+    return path;
+
+  if (!path.startsWith(QStringLiteral(":/")))
+    return path;
+
+  static const QStringList kAlternateRoots = {
+      QStringLiteral(":/StandardOfIron"),
+      QStringLiteral(":/qt/qml/StandardOfIron"),
+      QStringLiteral(":/qt/qml/default")};
+
+  const QString relative = path.mid(2); // strip ":/"
+  for (const auto &root : kAlternateRoots) {
+    QString candidate = root;
+    if (!candidate.endsWith('/'))
+      candidate.append('/');
+    candidate.append(relative);
+    if (exists(candidate))
+      return candidate;
+  }
+
+  return path;
+}
+
+} // namespace Utils::Resources