Jelajahi Sumber

Merge branch 'main' into copilot/refine-audio-system-performance

Adam Djellouli 1 bulan lalu
induk
melakukan
314c721a88

+ 3 - 2
app/core/game_engine.cpp

@@ -7,6 +7,7 @@
 #include "../models/hover_tracker.h"
 #include "../utils/json_vec_utils.h"
 #include "game/audio/AudioSystem.h"
+#include "game/units/troop_type.h"
 #include <QBuffer>
 #include <QCoreApplication>
 #include <QCursor>
@@ -791,7 +792,7 @@ QVariantMap GameEngine::getSelectedProductionState() const {
       st);
   m["hasBarracks"] = st.hasBarracks;
   m["inProgress"] = st.inProgress;
-  m["productType"] = QString::fromStdString(st.productType);
+  m["productType"] = QString::fromStdString(Game::Units::troopTypeToString(st.productType));
   m["timeRemaining"] = st.timeRemaining;
   m["buildTime"] = st.buildTime;
   m["producedCount"] = st.producedCount;
@@ -801,7 +802,7 @@ QVariantMap GameEngine::getSelectedProductionState() const {
 
   QVariantList queueList;
   for (const auto &unitType : st.productionQueue) {
-    queueList.append(QString::fromStdString(unitType));
+    queueList.append(QString::fromStdString(Game::Units::troopTypeToString(unitType)));
   }
   m["productionQueue"] = queueList;
 

+ 173 - 0
docs/TROOP_TYPE_ENUM.md

@@ -0,0 +1,173 @@
+# TroopType Enum Usage Guide
+
+## Overview
+
+The game now uses a strongly-typed `TroopType` enum instead of hardcoded string literals for troop identifiers. This provides compile-time type safety and prevents typos.
+
+## Quick Start
+
+### Include the Header
+
+```cpp
+#include "game/units/troop_type.h"
+```
+
+### Using the Enum
+
+```cpp
+// Creating troops
+Game::Units::TroopType myTroop = Game::Units::TroopType::Archer;
+
+// Comparing troop types
+if (myTroop == Game::Units::TroopType::Archer) {
+    // Handle archer logic
+}
+
+// Convert to string (for display, serialization)
+std::string name = Game::Units::troopTypeToString(myTroop);
+// Returns "archer"
+
+// Convert from string (for loading, UI input)
+Game::Units::TroopType parsed = Game::Units::troopTypeFromString("knight");
+// Returns TroopType::Knight
+```
+
+## Available Troop Types
+
+- `TroopType::Archer`
+- `TroopType::Knight`
+- `TroopType::Spearman`
+- `TroopType::MountedKnight`
+
+## When to Use Enum vs String
+
+### Use Enum For:
+- Internal game logic
+- Production systems
+- AI decisions
+- Nation/troop type definitions
+- Factory registration (for troops)
+- Configuration lookups
+
+### Use String For:
+- UnitComponent (for rendering/audio compatibility)
+- UI display
+- Audio asset mapping
+- Shader names
+- External data (maps, configs)
+- Building types
+
+## Common Patterns
+
+### Spawning Units
+
+```cpp
+Game::Units::SpawnParams sp;
+sp.unitType = Game::Units::TroopType::Archer;
+sp.position = QVector3D(10.0f, 0.0f, 10.0f);
+sp.playerId = 1;
+
+auto unit = factory->create(Game::Units::TroopType::Archer, world, sp);
+```
+
+### Production
+
+```cpp
+// Set production type
+productionComponent->productType = Game::Units::TroopType::Knight;
+
+// Start production from UI (string input)
+ProductionService::startProductionForFirstSelectedBarracks(
+    world, selectedUnits, playerId, "spearman");
+```
+
+### Configuration
+
+```cpp
+// Enum-based lookup (preferred)
+int individuals = TroopConfig::instance().getIndividualsPerUnit(
+    Game::Units::TroopType::Spearman);
+
+// String-based lookup (for compatibility)
+int individualsStr = TroopConfig::instance().getIndividualsPerUnit("spearman");
+```
+
+### AI Systems
+
+```cpp
+AICommand cmd;
+cmd.type = AICommandType::StartProduction;
+cmd.productType = Game::Units::TroopType::Archer;
+```
+
+## Serialization
+
+The serialization system automatically converts between enum and string:
+
+```cpp
+// Save (enum -> string)
+json["productType"] = troopTypeToString(production->productType);
+
+// Load (string -> enum)
+production->productType = troopTypeFromString(json["productType"].toString());
+```
+
+## Migration Notes
+
+If you're updating existing code:
+
+1. Replace string literals with enum values:
+   ```cpp
+   // Old
+   if (unitType == "archer") { ... }
+   
+   // New
+   if (unitType == TroopType::Archer) { ... }
+   ```
+
+2. Use conversion functions at boundaries:
+   ```cpp
+   // UI to internal
+   TroopType type = troopTypeFromString(qmlString.toStdString());
+   
+   // Internal to UI
+   QString displayName = QString::fromStdString(troopTypeToString(type));
+   ```
+
+3. UnitComponent still uses strings - no changes needed there
+
+## Performance Benefits
+
+- Enum comparisons are faster than string comparisons
+- No string allocation overhead
+- Better CPU cache utilization
+- Compiler can optimize switch statements on enums
+
+## Type Safety Benefits
+
+- Typos caught at compile time
+- IDE autocomplete for troop types
+- Refactoring tools can find all usages
+- Impossible to use invalid troop types
+
+## Adding New Troop Types
+
+To add a new troop type:
+
+1. Add enum value to `TroopType` in `troop_type.h`
+2. Update `troopTypeToString()` function
+3. Update `troopTypeFromString()` function
+4. Add configuration in `TroopConfig` constructor
+5. Register in factory system
+6. Update UI icons/audio mappings as needed
+
+Example:
+```cpp
+enum class TroopType { 
+    Archer, 
+    Knight, 
+    Spearman, 
+    MountedKnight,
+    Crossbowman  // New type
+};
+```

+ 4 - 3
game/core/component.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "../units/troop_type.h"
 #include "entity.h"
 #include <cstdint>
 #include <string>
@@ -163,7 +164,7 @@ class ProductionComponent : public Component {
 public:
   ProductionComponent()
       : inProgress(false), buildTime(4.0f), timeRemaining(0.0f),
-        producedCount(0), maxUnits(5), productType("archer"), rallyX(0.0f),
+        producedCount(0), maxUnits(5), productType(Game::Units::TroopType::Archer), rallyX(0.0f),
         rallyZ(0.0f), rallySet(false), villagerCost(1) {}
 
   bool inProgress;
@@ -171,11 +172,11 @@ public:
   float timeRemaining;
   int producedCount;
   int maxUnits;
-  std::string productType;
+  Game::Units::TroopType productType;
   float rallyX, rallyZ;
   bool rallySet;
   int villagerCost;
-  std::vector<std::string> productionQueue;
+  std::vector<Game::Units::TroopType> productionQueue;
 };
 
 class AIControlledComponent : public Component {

+ 5 - 4
game/core/serialization.cpp

@@ -1,4 +1,5 @@
 #include "serialization.h"
+#include "../units/troop_type.h"
 #include "component.h"
 #include "entity.h"
 #include "world.h"
@@ -185,7 +186,7 @@ QJsonObject Serialization::serializeEntity(const Entity *entity) {
     productionObj["producedCount"] = production->producedCount;
     productionObj["maxUnits"] = production->maxUnits;
     productionObj["productType"] =
-        QString::fromStdString(production->productType);
+        QString::fromStdString(Game::Units::troopTypeToString(production->productType));
     productionObj["rallyX"] = production->rallyX;
     productionObj["rallyZ"] = production->rallyZ;
     productionObj["rallySet"] = production->rallySet;
@@ -193,7 +194,7 @@ QJsonObject Serialization::serializeEntity(const Entity *entity) {
 
     QJsonArray queueArray;
     for (const auto &queued : production->productionQueue) {
-      queueArray.append(QString::fromStdString(queued));
+      queueArray.append(QString::fromStdString(Game::Units::troopTypeToString(queued)));
     }
     productionObj["queue"] = queueArray;
     entityObj["production"] = productionObj;
@@ -361,7 +362,7 @@ void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
     production->producedCount = productionObj["producedCount"].toInt(0);
     production->maxUnits = productionObj["maxUnits"].toInt(0);
     production->productType =
-        productionObj["productType"].toString().toStdString();
+        Game::Units::troopTypeFromString(productionObj["productType"].toString().toStdString());
     production->rallyX = static_cast<float>(productionObj["rallyX"].toDouble());
     production->rallyZ = static_cast<float>(productionObj["rallyZ"].toDouble());
     production->rallySet = productionObj["rallySet"].toBool(false);
@@ -371,7 +372,7 @@ void Serialization::deserializeEntity(Entity *entity, const QJsonObject &json) {
     const auto queueArray = productionObj["queue"].toArray();
     production->productionQueue.reserve(queueArray.size());
     for (const auto &value : queueArray) {
-      production->productionQueue.push_back(value.toString().toStdString());
+      production->productionQueue.push_back(Game::Units::troopTypeFromString(value.toString().toStdString()));
     }
   }
 

+ 2 - 2
game/map/level_loader.cpp

@@ -70,7 +70,7 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
         sp.playerId = 0;
         sp.unitType = "archer";
         sp.aiControlled = !owners.isPlayer(sp.playerId);
-        if (auto unit = reg->create("archer", world, sp)) {
+        if (auto unit = reg->create(Game::Units::TroopType::Archer, world, sp)) {
           res.playerUnitId = unit->id();
         } else {
           qWarning() << "LevelLoader: Fallback archer spawn failed";
@@ -119,7 +119,7 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
       sp.playerId = 0;
       sp.unitType = "archer";
       sp.aiControlled = !owners.isPlayer(sp.playerId);
-      if (auto unit = reg->create("archer", world, sp)) {
+      if (auto unit = reg->create(Game::Units::TroopType::Archer, world, sp)) {
         res.playerUnitId = unit->id();
       }
     }

+ 2 - 5
game/systems/ai_system/ai_command_applier.cpp

@@ -116,9 +116,7 @@ void AICommandApplier::apply(Engine::Core::World &world, int aiOwnerId,
 
       int currentTroops = world.countTroopsForPlayer(aiOwnerId);
       int maxTroops = Game::GameConfig::instance().getMaxTroopsPerPlayer();
-      std::string productType = command.productType.empty()
-                                    ? production->productType
-                                    : command.productType;
+      Game::Units::TroopType productType = production->productType;
       int individualsPerUnit =
           Game::Units::TroopConfig::instance().getIndividualsPerUnit(
               productType);
@@ -126,8 +124,7 @@ void AICommandApplier::apply(Engine::Core::World &world, int aiOwnerId,
         break;
       }
 
-      if (!command.productType.empty())
-        production->productType = command.productType;
+      production->productType = command.productType;
 
       production->timeRemaining = production->buildTime;
       production->inProgress = true;

+ 4 - 2
game/systems/ai_system/ai_reasoner.cpp

@@ -1,5 +1,6 @@
 #include "ai_reasoner.h"
 #include "../../game_config.h"
+#include "../../units/troop_type.h"
 #include "../nation_registry.h"
 #include "ai_utils.h"
 #include <algorithm>
@@ -80,9 +81,10 @@ void AIReasoner::updateContext(const AISnapshot &snapshot, AIContext &ctx) {
     ctx.totalUnits++;
 
     if (ctx.nation) {
-      if (ctx.nation->isRangedUnit(entity.unitType)) {
+      auto troopType = Game::Units::troopTypeFromString(entity.unitType);
+      if (ctx.nation->isRangedUnit(troopType)) {
         ctx.rangedCount++;
-      } else if (ctx.nation->isMeleeUnit(entity.unitType)) {
+      } else if (ctx.nation->isMeleeUnit(troopType)) {
         ctx.meleeCount++;
       }
     }

+ 4 - 2
game/systems/ai_system/ai_tactical.cpp

@@ -1,4 +1,5 @@
 #include "ai_tactical.h"
+#include "../../units/troop_type.h"
 #include "../nation_registry.h"
 #include "ai_utils.h"
 #include <algorithm>
@@ -201,10 +202,11 @@ float TacticalUtils::getUnitTypePriority(const std::string &unitType,
                                          const Game::Systems::Nation *nation) {
 
   if (nation) {
-    if (nation->isRangedUnit(unitType)) {
+    auto troopType = Game::Units::troopTypeFromString(unitType);
+    if (nation->isRangedUnit(troopType)) {
       return 3.0f;
     }
-    if (nation->isMeleeUnit(unitType)) {
+    if (nation->isMeleeUnit(troopType)) {
       return 2.0f;
     }
   }

+ 3 - 2
game/systems/ai_system/ai_types.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "../../units/troop_type.h"
 #include <string>
 #include <unordered_map>
 #include <vector>
@@ -45,7 +46,7 @@ struct ProductionSnapshot {
   float timeRemaining = 0.0f;
   int producedCount = 0;
   int maxUnits = 0;
-  std::string productType;
+  Game::Units::TroopType productType = Game::Units::TroopType::Archer;
   bool rallySet = false;
   float rallyX = 0.0f;
   float rallyZ = 0.0f;
@@ -148,7 +149,7 @@ struct AICommand {
   Engine::Core::EntityID targetId = 0;
   bool shouldChase = false;
   Engine::Core::EntityID buildingId = 0;
-  std::string productType;
+  Game::Units::TroopType productType = Game::Units::TroopType::Archer;
 };
 
 struct AIResult {

+ 1 - 1
game/systems/capture_system.cpp

@@ -74,7 +74,7 @@ void CaptureSystem::transferBarrackOwnership(Engine::Core::World *world,
   if (!Game::Core::isNeutralOwner(newOwnerId) && !prod) {
     prod = barrack->addComponent<Engine::Core::ProductionComponent>();
     if (prod) {
-      prod->productType = "archer";
+      prod->productType = Game::Units::TroopType::Archer;
       prod->buildTime = 10.0f;
       prod->maxUnits = 150;
       prod->inProgress = false;

+ 23 - 5
game/systems/nation_registry.cpp

@@ -24,7 +24,7 @@ std::vector<const TroopType *> Nation::getRangedTroops() const {
   return result;
 }
 
-const TroopType *Nation::getTroop(const std::string &unitType) const {
+const TroopType *Nation::getTroop(Game::Units::TroopType unitType) const {
   for (const auto &troop : availableTroops) {
     if (troop.unitType == unitType) {
       return &troop;
@@ -59,12 +59,12 @@ const TroopType *Nation::getBestRangedTroop() const {
   return *it;
 }
 
-bool Nation::isMeleeUnit(const std::string &unitType) const {
+bool Nation::isMeleeUnit(Game::Units::TroopType unitType) const {
   const auto *troop = getTroop(unitType);
   return troop != nullptr && troop->isMelee;
 }
 
-bool Nation::isRangedUnit(const std::string &unitType) const {
+bool Nation::isRangedUnit(Game::Units::TroopType unitType) const {
   const auto *troop = getTroop(unitType);
   return troop != nullptr && !troop->isMelee;
 }
@@ -125,7 +125,7 @@ void NationRegistry::initializeDefaults() {
   kingdomOfIron.formationType = FormationType::Roman;
 
   TroopType archer;
-  archer.unitType = "archer";
+  archer.unitType = Game::Units::TroopType::Archer;
   archer.displayName = "Archer";
   archer.isMelee = false;
   archer.cost = 50;
@@ -134,7 +134,7 @@ void NationRegistry::initializeDefaults() {
   kingdomOfIron.availableTroops.push_back(archer);
 
   TroopType knight;
-  knight.unitType = "knight";
+  knight.unitType = Game::Units::TroopType::Knight;
   knight.displayName = "Knight";
   knight.isMelee = true;
   knight.cost = 100;
@@ -142,6 +142,24 @@ void NationRegistry::initializeDefaults() {
   knight.priority = 10;
   kingdomOfIron.availableTroops.push_back(knight);
 
+  TroopType spearman;
+  spearman.unitType = Game::Units::TroopType::Spearman;
+  spearman.displayName = "Spearman";
+  spearman.isMelee = true;
+  spearman.cost = 75;
+  spearman.buildTime = 6.0f;
+  spearman.priority = 5;
+  kingdomOfIron.availableTroops.push_back(spearman);
+
+  TroopType mountedKnight;
+  mountedKnight.unitType = Game::Units::TroopType::MountedKnight;
+  mountedKnight.displayName = "Mounted Knight";
+  mountedKnight.isMelee = true;
+  mountedKnight.cost = 150;
+  mountedKnight.buildTime = 10.0f;
+  mountedKnight.priority = 15;
+  kingdomOfIron.availableTroops.push_back(mountedKnight);
+
   registerNation(std::move(kingdomOfIron));
 
   m_defaultNation = "kingdom_of_iron";

+ 5 - 4
game/systems/nation_registry.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "../units/troop_type.h"
 #include "formation_system.h"
 #include <memory>
 #include <string>
@@ -9,7 +10,7 @@
 namespace Game::Systems {
 
 struct TroopType {
-  std::string unitType;
+  Game::Units::TroopType unitType;
   std::string displayName;
   bool isMelee = false;
   int cost = 100;
@@ -28,13 +29,13 @@ struct Nation {
 
   std::vector<const TroopType *> getRangedTroops() const;
 
-  const TroopType *getTroop(const std::string &unitType) const;
+  const TroopType *getTroop(Game::Units::TroopType unitType) const;
 
   const TroopType *getBestMeleeTroop() const;
   const TroopType *getBestRangedTroop() const;
 
-  bool isMeleeUnit(const std::string &unitType) const;
-  bool isRangedUnit(const std::string &unitType) const;
+  bool isMeleeUnit(Game::Units::TroopType unitType) const;
+  bool isRangedUnit(Game::Units::TroopType unitType) const;
 };
 
 class NationRegistry {

+ 1 - 1
game/systems/production_service.cpp

@@ -26,7 +26,7 @@ findFirstSelectedBarracks(Engine::Core::World &world,
 ProductionResult ProductionService::startProductionForFirstSelectedBarracks(
     Engine::Core::World &world,
     const std::vector<Engine::Core::EntityID> &selected, int ownerId,
-    const std::string &unitType) {
+    Game::Units::TroopType unitType) {
   auto *e = findFirstSelectedBarracks(world, selected, ownerId);
   if (!e)
     return ProductionResult::NoBarracks;

+ 12 - 3
game/systems/production_service.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "../units/troop_type.h"
 #include <string>
 #include <vector>
 
@@ -25,14 +26,14 @@ enum class ProductionResult {
 struct ProductionState {
   bool hasBarracks = false;
   bool inProgress = false;
-  std::string productType = "";
+  Game::Units::TroopType productType = Game::Units::TroopType::Archer;
   float timeRemaining = 0.0f;
   float buildTime = 0.0f;
   int producedCount = 0;
   int maxUnits = 0;
   int villagerCost = 1;
   int queueSize = 0;
-  std::vector<std::string> productionQueue;
+  std::vector<Game::Units::TroopType> productionQueue;
 };
 
 class ProductionService {
@@ -40,7 +41,15 @@ public:
   static ProductionResult startProductionForFirstSelectedBarracks(
       Engine::Core::World &world,
       const std::vector<Engine::Core::EntityID> &selected, int ownerId,
-      const std::string &unitType);
+      Game::Units::TroopType unitType);
+
+  static ProductionResult startProductionForFirstSelectedBarracks(
+      Engine::Core::World &world,
+      const std::vector<Engine::Core::EntityID> &selected, int ownerId,
+      const std::string &unitType) {
+    return startProductionForFirstSelectedBarracks(
+        world, selected, ownerId, Game::Units::troopTypeFromString(unitType));
+  }
 
   static bool setRallyForFirstSelectedBarracks(
       Engine::Core::World &world,

+ 1 - 1
game/systems/production_system.cpp

@@ -62,7 +62,7 @@ void ProductionSystem::update(Engine::Core::World *world, float deltaTime) {
           Game::Units::SpawnParams sp;
           sp.position = exitPos;
           sp.playerId = u->ownerId;
-          sp.unitType = prod->productType;
+          sp.unitType = Game::Units::troopTypeToString(prod->productType);
           sp.aiControlled =
               e->hasComponent<Engine::Core::AIControlledComponent>();
           auto unit = reg->create(prod->productType, *world, sp);

+ 2 - 2
game/units/archer.cpp

@@ -22,7 +22,7 @@ static inline QVector3D teamColor(int ownerId) {
 namespace Game {
 namespace Units {
 
-Archer::Archer(Engine::Core::World &world) : Unit(world, "archer") {}
+Archer::Archer(Engine::Core::World &world) : Unit(world, TroopType::Archer) {}
 
 std::unique_ptr<Archer> Archer::Create(Engine::Core::World &world,
                                        const SpawnParams &params) {
@@ -45,7 +45,7 @@ void Archer::init(const SpawnParams &params) {
   m_r->visible = true;
 
   m_u = e->addComponent<Engine::Core::UnitComponent>();
-  m_u->unitType = m_type;
+  m_u->unitType = m_typeString;
   m_u->health = 80;
   m_u->maxHealth = 80;
   m_u->speed = 3.0f;

+ 3 - 3
game/units/barracks.cpp

@@ -34,7 +34,7 @@ void Barracks::init(const SpawnParams &params) {
   m_r->mesh = Engine::Core::RenderableComponent::MeshKind::Cube;
 
   m_u = e->addComponent<Engine::Core::UnitComponent>();
-  m_u->unitType = m_type;
+  m_u->unitType = m_typeString;
   m_u->health = 2000;
   m_u->maxHealth = 2000;
   m_u->speed = 0.0f;
@@ -54,11 +54,11 @@ void Barracks::init(const SpawnParams &params) {
   e->addComponent<Engine::Core::BuildingComponent>();
 
   Game::Systems::BuildingCollisionRegistry::instance().registerBuilding(
-      m_id, m_type, m_t->position.x, m_t->position.z, m_u->ownerId);
+      m_id, m_typeString, m_t->position.x, m_t->position.z, m_u->ownerId);
 
   if (!Game::Core::isNeutralOwner(m_u->ownerId)) {
     if (auto *prod = e->addComponent<Engine::Core::ProductionComponent>()) {
-      prod->productType = "archer";
+      prod->productType = TroopType::Archer;
       prod->buildTime = 10.0f;
       prod->maxUnits = params.maxPopulation;
       prod->inProgress = false;

+ 18 - 18
game/units/factory.cpp

@@ -9,26 +9,26 @@ namespace Game {
 namespace Units {
 
 void registerBuiltInUnits(UnitFactoryRegistry &reg) {
-  reg.registerFactory(
-      "archer", [](Engine::Core::World &world, const SpawnParams &params) {
-        return Archer::Create(world, params);
-      });
-  reg.registerFactory(
-      "knight", [](Engine::Core::World &world, const SpawnParams &params) {
-        return Knight::Create(world, params);
-      });
-  reg.registerFactory("mounted_knight", [](Engine::Core::World &world,
-                                           const SpawnParams &params) {
+  reg.registerFactory(TroopType::Archer, [](Engine::Core::World &world,
+                                            const SpawnParams &params) {
+    return Archer::Create(world, params);
+  });
+  reg.registerFactory(TroopType::Knight, [](Engine::Core::World &world,
+                                            const SpawnParams &params) {
+    return Knight::Create(world, params);
+  });
+  reg.registerFactory(TroopType::MountedKnight, [](Engine::Core::World &world,
+                                                   const SpawnParams &params) {
     return MountedKnight::Create(world, params);
   });
-  reg.registerFactory(
-      "spearman", [](Engine::Core::World &world, const SpawnParams &params) {
-        return Spearman::Create(world, params);
-      });
-  reg.registerFactory(
-      "barracks", [](Engine::Core::World &world, const SpawnParams &params) {
-        return Barracks::Create(world, params);
-      });
+  reg.registerFactory(TroopType::Spearman, [](Engine::Core::World &world,
+                                              const SpawnParams &params) {
+    return Spearman::Create(world, params);
+  });
+  reg.registerFactory("barracks", [](Engine::Core::World &world,
+                                     const SpawnParams &params) {
+    return Barracks::Create(world, params);
+  });
 }
 
 } // namespace Units

+ 19 - 4
game/units/factory.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "../units/troop_type.h"
 #include "unit.h"
 #include <functional>
 #include <memory>
@@ -14,20 +15,34 @@ public:
   using Factory = std::function<std::unique_ptr<Unit>(Engine::Core::World &,
                                                       const SpawnParams &)>;
 
+  void registerFactory(TroopType type, Factory f) {
+    m_enumMap[type] = std::move(f);
+  }
+
   void registerFactory(const std::string &type, Factory f) {
-    m_map[type] = std::move(f);
+    m_stringMap[type] = std::move(f);
+  }
+
+  std::unique_ptr<Unit> create(TroopType type, Engine::Core::World &world,
+                               const SpawnParams &params) const {
+    auto it = m_enumMap.find(type);
+    if (it == m_enumMap.end())
+      return nullptr;
+    return it->second(world, params);
   }
+
   std::unique_ptr<Unit> create(const std::string &type,
                                Engine::Core::World &world,
                                const SpawnParams &params) const {
-    auto it = m_map.find(type);
-    if (it == m_map.end())
+    auto it = m_stringMap.find(type);
+    if (it == m_stringMap.end())
       return nullptr;
     return it->second(world, params);
   }
 
 private:
-  std::unordered_map<std::string, Factory> m_map;
+  std::unordered_map<TroopType, Factory> m_enumMap;
+  std::unordered_map<std::string, Factory> m_stringMap;
 };
 
 void registerBuiltInUnits(UnitFactoryRegistry &reg);

+ 2 - 2
game/units/knight.cpp

@@ -22,7 +22,7 @@ static inline QVector3D teamColor(int ownerId) {
 namespace Game {
 namespace Units {
 
-Knight::Knight(Engine::Core::World &world) : Unit(world, "knight") {}
+Knight::Knight(Engine::Core::World &world) : Unit(world, TroopType::Knight) {}
 
 std::unique_ptr<Knight> Knight::Create(Engine::Core::World &world,
                                        const SpawnParams &params) {
@@ -45,7 +45,7 @@ void Knight::init(const SpawnParams &params) {
   m_r->visible = true;
 
   m_u = e->addComponent<Engine::Core::UnitComponent>();
-  m_u->unitType = m_type;
+  m_u->unitType = m_typeString;
   m_u->health = 150;
   m_u->maxHealth = 150;
   m_u->speed = 2.0f;

+ 2 - 2
game/units/mounted_knight.cpp

@@ -23,7 +23,7 @@ namespace Game {
 namespace Units {
 
 MountedKnight::MountedKnight(Engine::Core::World &world)
-    : Unit(world, "mounted_knight") {}
+    : Unit(world, TroopType::MountedKnight) {}
 
 std::unique_ptr<MountedKnight>
 MountedKnight::Create(Engine::Core::World &world, const SpawnParams &params) {
@@ -46,7 +46,7 @@ void MountedKnight::init(const SpawnParams &params) {
   m_r->visible = true;
 
   m_u = e->addComponent<Engine::Core::UnitComponent>();
-  m_u->unitType = m_type;
+  m_u->unitType = m_typeString;
   m_u->health = 200;
   m_u->maxHealth = 200;
   m_u->speed = 4.0f;

+ 2 - 2
game/units/spearman.cpp

@@ -22,7 +22,7 @@ static inline QVector3D teamColor(int ownerId) {
 namespace Game {
 namespace Units {
 
-Spearman::Spearman(Engine::Core::World &world) : Unit(world, "spearman") {}
+Spearman::Spearman(Engine::Core::World &world) : Unit(world, TroopType::Spearman) {}
 
 std::unique_ptr<Spearman> Spearman::Create(Engine::Core::World &world,
                                            const SpawnParams &params) {
@@ -45,7 +45,7 @@ void Spearman::init(const SpawnParams &params) {
   m_r->visible = true;
 
   m_u = e->addComponent<Engine::Core::UnitComponent>();
-  m_u->unitType = m_type;
+  m_u->unitType = m_typeString;
   m_u->health = 120;
   m_u->maxHealth = 120;
   m_u->speed = 2.5f;

+ 34 - 22
game/units/troop_config.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "troop_type.h"
 #include <string>
 #include <unordered_map>
 
@@ -13,7 +14,7 @@ public:
     return inst;
   }
 
-  int getIndividualsPerUnit(const std::string &unitType) const {
+  int getIndividualsPerUnit(TroopType unitType) const {
     auto it = m_individualsPerUnit.find(unitType);
     if (it != m_individualsPerUnit.end()) {
       return it->second;
@@ -21,7 +22,7 @@ public:
     return 1;
   }
 
-  int getMaxUnitsPerRow(const std::string &unitType) const {
+  int getMaxUnitsPerRow(TroopType unitType) const {
     auto it = m_maxUnitsPerRow.find(unitType);
     if (it != m_maxUnitsPerRow.end()) {
       return it->second;
@@ -29,7 +30,7 @@ public:
     return 10;
   }
 
-  float getSelectionRingSize(const std::string &unitType) const {
+  float getSelectionRingSize(TroopType unitType) const {
     auto it = m_selectionRingSize.find(unitType);
     if (it != m_selectionRingSize.end()) {
       return it->second;
@@ -37,41 +38,52 @@ public:
     return 0.5f;
   }
 
-  void registerTroopType(const std::string &unitType, int individualsPerUnit) {
+  int getIndividualsPerUnit(const std::string &unitType) const {
+    return getIndividualsPerUnit(troopTypeFromString(unitType));
+  }
+
+  int getMaxUnitsPerRow(const std::string &unitType) const {
+    return getMaxUnitsPerRow(troopTypeFromString(unitType));
+  }
+
+  float getSelectionRingSize(const std::string &unitType) const {
+    return getSelectionRingSize(troopTypeFromString(unitType));
+  }
+
+  void registerTroopType(TroopType unitType, int individualsPerUnit) {
     m_individualsPerUnit[unitType] = individualsPerUnit;
   }
 
-  void registerMaxUnitsPerRow(const std::string &unitType, int maxUnitsPerRow) {
+  void registerMaxUnitsPerRow(TroopType unitType, int maxUnitsPerRow) {
     m_maxUnitsPerRow[unitType] = maxUnitsPerRow;
   }
 
-  void registerSelectionRingSize(const std::string &unitType,
-                                 float selectionRingSize) {
+  void registerSelectionRingSize(TroopType unitType, float selectionRingSize) {
     m_selectionRingSize[unitType] = selectionRingSize;
   }
 
 private:
   TroopConfig() {
-    m_individualsPerUnit["archer"] = 20;
-    m_maxUnitsPerRow["archer"] = 5;
-    m_selectionRingSize["archer"] = 1.2f;
+    m_individualsPerUnit[TroopType::Archer] = 20;
+    m_maxUnitsPerRow[TroopType::Archer] = 5;
+    m_selectionRingSize[TroopType::Archer] = 1.2f;
 
-    m_individualsPerUnit["knight"] = 15;
-    m_maxUnitsPerRow["knight"] = 5;
-    m_selectionRingSize["knight"] = 1.1f;
+    m_individualsPerUnit[TroopType::Knight] = 15;
+    m_maxUnitsPerRow[TroopType::Knight] = 5;
+    m_selectionRingSize[TroopType::Knight] = 1.1f;
 
-    m_individualsPerUnit["spearman"] = 24;
-    m_maxUnitsPerRow["spearman"] = 6;
-    m_selectionRingSize["spearman"] = 1.4f;
+    m_individualsPerUnit[TroopType::Spearman] = 24;
+    m_maxUnitsPerRow[TroopType::Spearman] = 6;
+    m_selectionRingSize[TroopType::Spearman] = 1.4f;
 
-    m_individualsPerUnit["mounted_knight"] = 10;
-    m_maxUnitsPerRow["mounted_knight"] = 5;
-    m_selectionRingSize["mounted_knight"] = 1.0f;
+    m_individualsPerUnit[TroopType::MountedKnight] = 10;
+    m_maxUnitsPerRow[TroopType::MountedKnight] = 5;
+    m_selectionRingSize[TroopType::MountedKnight] = 1.0f;
   }
 
-  std::unordered_map<std::string, int> m_individualsPerUnit;
-  std::unordered_map<std::string, int> m_maxUnitsPerRow;
-  std::unordered_map<std::string, float> m_selectionRingSize;
+  std::unordered_map<TroopType, int> m_individualsPerUnit;
+  std::unordered_map<TroopType, int> m_maxUnitsPerRow;
+  std::unordered_map<TroopType, float> m_selectionRingSize;
 };
 
 } // namespace Units

+ 45 - 0
game/units/troop_type.h

@@ -0,0 +1,45 @@
+#pragma once
+
+#include <string>
+
+namespace Game {
+namespace Units {
+
+enum class TroopType { Archer, Knight, Spearman, MountedKnight };
+
+inline std::string troopTypeToString(TroopType type) {
+  switch (type) {
+  case TroopType::Archer:
+    return "archer";
+  case TroopType::Knight:
+    return "knight";
+  case TroopType::Spearman:
+    return "spearman";
+  case TroopType::MountedKnight:
+    return "mounted_knight";
+  }
+  return "";
+}
+
+inline TroopType troopTypeFromString(const std::string &str) {
+  if (str == "archer")
+    return TroopType::Archer;
+  if (str == "knight")
+    return TroopType::Knight;
+  if (str == "spearman")
+    return TroopType::Spearman;
+  if (str == "mounted_knight")
+    return TroopType::MountedKnight;
+  return TroopType::Archer;
+}
+
+} // namespace Units
+} // namespace Game
+
+namespace std {
+template <> struct hash<Game::Units::TroopType> {
+  size_t operator()(Game::Units::TroopType type) const {
+    return hash<int>()(static_cast<int>(type));
+  }
+};
+} // namespace std

+ 4 - 1
game/units/unit.cpp

@@ -5,8 +5,11 @@
 namespace Game {
 namespace Units {
 
+Unit::Unit(Engine::Core::World &world, TroopType type)
+    : m_world(&world), m_typeString(troopTypeToString(type)) {}
+
 Unit::Unit(Engine::Core::World &world, const std::string &type)
-    : m_world(&world), m_type(type) {}
+    : m_world(&world), m_typeString(type) {}
 
 Engine::Core::Entity *Unit::entity() const {
   return m_world ? m_world->getEntity(m_id) : nullptr;

+ 5 - 3
game/units/unit.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "troop_type.h"
 #include <QVector3D>
 #include <memory>
 #include <string>
@@ -25,7 +26,7 @@ struct SpawnParams {
 
   QVector3D position{0, 0, 0};
   int playerId = 0;
-  std::string unitType;
+  std::string unitType = "archer";
   bool aiControlled = false;
   int maxPopulation = 100;
 };
@@ -35,7 +36,7 @@ public:
   virtual ~Unit() = default;
 
   Engine::Core::EntityID id() const { return m_id; }
-  const std::string &type() const { return m_type; }
+  std::string typeString() const { return m_typeString; }
 
   void moveTo(float x, float z);
   bool isAlive() const;
@@ -45,6 +46,7 @@ public:
   bool isInHoldMode() const;
 
 protected:
+  Unit(Engine::Core::World &world, TroopType type);
   Unit(Engine::Core::World &world, const std::string &type);
   Engine::Core::Entity *entity() const;
 
@@ -52,7 +54,7 @@ protected:
 
   Engine::Core::World *m_world = nullptr;
   Engine::Core::EntityID m_id = 0;
-  std::string m_type;
+  std::string m_typeString;
 
   Engine::Core::TransformComponent *m_t = nullptr;
   Engine::Core::RenderableComponent *m_r = nullptr;

+ 1 - 2
render/entity/archer_renderer.cpp

@@ -17,6 +17,7 @@
 #include "../scene_renderer.h"
 #include "../submitter.h"
 #include "registry.h"
+#include "renderer_constants.h"
 
 #include <QMatrix4x4>
 #include <QString>
@@ -34,8 +35,6 @@ using Render::Geom::coneFromTo;
 using Render::Geom::cylinderBetween;
 using Render::Geom::sphereAt;
 
-static constexpr std::size_t MAX_EXTRAS_CACHE_SIZE = 10000;
-
 struct ArcherExtras {
   QVector3D stringCol;
   QVector3D fletch;

+ 1 - 4
render/entity/barracks_renderer.cpp

@@ -17,6 +17,7 @@ namespace {
 using Render::Geom::clamp01;
 using Render::Geom::clampVec01;
 using Render::Geom::cylinderBetween;
+using Render::Geom::lerp;
 using Render::Geom::sphereAt;
 
 struct BuildingProportions {
@@ -73,10 +74,6 @@ static inline BarracksPalette makePalette(const QVector3D &team) {
   return p;
 }
 
-static inline QVector3D lerp(const QVector3D &a, const QVector3D &b, float t) {
-  return a * (1.0f - t) + b * t;
-}
-
 static inline void drawCylinder(ISubmitter &out, const QMatrix4x4 &model,
                                 const QVector3D &a, const QVector3D &b,
                                 float radius, const QVector3D &color,

+ 14 - 21
render/entity/horse_renderer.cpp

@@ -20,6 +20,8 @@ namespace Render::GL {
 using Render::Geom::clamp01;
 using Render::Geom::coneFromTo;
 using Render::Geom::cylinderBetween;
+using Render::Geom::lerp;
+using Render::Geom::smoothstep;
 
 namespace {
 
@@ -39,17 +41,8 @@ inline float randBetween(uint32_t seed, uint32_t salt, float minV, float maxV) {
   return minV + (maxV - minV) * t;
 }
 
-inline QVector3D lerpVec(const QVector3D &a, const QVector3D &b, float t) {
-  return a * (1.0f - t) + b * t;
-}
-
 inline float saturate(float x) { return std::min(1.0f, std::max(0.0f, x)); }
 
-inline float smoothstep(float e0, float e1, float x) {
-  float t = saturate((x - e0) / (e1 - e0));
-  return t * t * (3.0f - 2.0f * t);
-}
-
 inline QVector3D rotateAroundY(const QVector3D &v, float angle) {
   float s = std::sin(angle), c = std::cos(angle);
   return QVector3D(v.x() * c + v.z() * s, v.y(), -v.x() * s + v.z() * c);
@@ -133,17 +126,17 @@ HorseVariant makeHorseVariant(uint32_t seed, const QVector3D &leatherBase,
 
   float blazeChance = hash01(seed ^ 0x1122u);
   if (blazeChance > 0.82f) {
-    v.coatColor = lerpVec(v.coatColor, QVector3D(0.92f, 0.92f, 0.90f), 0.25f);
+    v.coatColor = lerp(v.coatColor, QVector3D(0.92f, 0.92f, 0.90f), 0.25f);
   }
 
-  v.maneColor = lerpVec(v.coatColor, QVector3D(0.10f, 0.09f, 0.08f),
-                        randBetween(seed, 0x3344u, 0.55f, 0.85f));
-  v.tailColor = lerpVec(v.maneColor, v.coatColor, 0.35f);
+  v.maneColor = lerp(v.coatColor, QVector3D(0.10f, 0.09f, 0.08f),
+                     randBetween(seed, 0x3344u, 0.55f, 0.85f));
+  v.tailColor = lerp(v.maneColor, v.coatColor, 0.35f);
 
-  v.muzzleColor = lerpVec(v.coatColor, QVector3D(0.18f, 0.14f, 0.12f), 0.65f);
+  v.muzzleColor = lerp(v.coatColor, QVector3D(0.18f, 0.14f, 0.12f), 0.65f);
   v.hoofColor =
-      lerpVec(QVector3D(0.16f, 0.14f, 0.12f), QVector3D(0.40f, 0.35f, 0.32f),
-              randBetween(seed, 0x5566u, 0.15f, 0.65f));
+      lerp(QVector3D(0.16f, 0.14f, 0.12f), QVector3D(0.40f, 0.35f, 0.32f),
+           randBetween(seed, 0x5566u, 0.15f, 0.65f));
 
   float leatherTone = randBetween(seed, 0x7788u, 0.78f, 0.96f);
   float tackTone = randBetween(seed, 0x88AAu, 0.58f, 0.78f);
@@ -151,7 +144,7 @@ HorseVariant makeHorseVariant(uint32_t seed, const QVector3D &leatherBase,
   QVector3D tackTint = leatherBase * tackTone;
   if (blazeChance > 0.90f) {
 
-    tackTint = lerpVec(tackTint, QVector3D(0.18f, 0.19f, 0.22f), 0.25f);
+    tackTint = lerp(tackTint, QVector3D(0.18f, 0.19f, 0.22f), 0.25f);
   }
   v.saddleColor = leatherTint;
   v.tackColor = tackTint;
@@ -272,7 +265,7 @@ void HorseRenderer::render(const DrawContext &ctx, const AnimationInputs &anim,
   float neckRadius = d.bodyWidth * 0.42f;
 
   QVector3D neckMid =
-      lerpVec(neckBase, neckTop, 0.55f) +
+      lerp(neckBase, neckTop, 0.55f) +
       QVector3D(0.0f, d.bodyHeight * 0.02f, d.bodyLength * 0.02f);
   out.mesh(getUnitCylinder(),
            cylinderBetween(ctx.model, neckBase, neckMid, neckRadius * 1.00f),
@@ -413,7 +406,7 @@ void HorseRenderer::render(const DrawContext &ctx, const AnimationInputs &anim,
       neckTop + QVector3D(0.0f, d.headHeight * 0.20f, -d.headLength * 0.20f);
   for (int i = 0; i < 12; ++i) {
     float t = i / 11.0f;
-    QVector3D segStart = lerpVec(maneRoot, neckBase, t);
+    QVector3D segStart = lerp(maneRoot, neckBase, t);
     segStart.setY(segStart.y() + (0.07f - t * 0.05f));
     float sway =
         (anim.isMoving ? std::sin((phase + t * 0.15f) * 2.0f * kPi) * 0.04f
@@ -571,7 +564,7 @@ void HorseRenderer::render(const DrawContext &ctx, const AnimationInputs &anim,
 
     out.mesh(getUnitCylinder(),
              cylinderBetween(ctx.model, cannon, fetlock, pasternR * 1.05f),
-             lerpVec(v.coatColor * 0.94f, distalColor, tSock * 0.8f), nullptr,
+             lerp(v.coatColor * 0.94f, distalColor, tSock * 0.8f), nullptr,
              1.0f);
 
     QVector3D hoofColor = v.hoofColor;
@@ -719,7 +712,7 @@ void HorseRenderer::render(const DrawContext &ctx, const AnimationInputs &anim,
                                                  -d.saddleThickness * 0.3f,
                                                  d.seatForwardOffset * 0.15f);
 
-    QVector3D mid = lerpVec(reinStart, reinEnd, 0.5f) +
+    QVector3D mid = lerp(reinStart, reinEnd, 0.5f) +
                     QVector3D(0.0f, -d.bodyHeight * 0.10f, 0.0f);
     out.mesh(getUnitCylinder(),
              cylinderBetween(ctx.model, reinStart, mid, d.bodyWidth * 0.02f),

+ 5 - 24
render/entity/knight_renderer.cpp

@@ -17,6 +17,7 @@
 #include "../scene_renderer.h"
 #include "../submitter.h"
 #include "registry.h"
+#include "renderer_constants.h"
 #include <unordered_map>
 
 #include <QMatrix4x4>
@@ -32,32 +33,12 @@ using Render::Geom::clamp01;
 using Render::Geom::clampf;
 using Render::Geom::coneFromTo;
 using Render::Geom::cylinderBetween;
+using Render::Geom::easeInOutCubic;
+using Render::Geom::lerp;
+using Render::Geom::nlerp;
+using Render::Geom::smoothstep;
 using Render::Geom::sphereAt;
 
-static constexpr std::size_t MAX_EXTRAS_CACHE_SIZE = 10000;
-
-static inline float easeInOutCubic(float t) {
-  t = clamp01(t);
-  return t < 0.5f ? 4.0f * t * t * t
-                  : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) / 2.0f;
-}
-
-static inline float smoothstep(float a, float b, float x) {
-  x = clamp01((x - a) / (b - a));
-  return x * x * (3.0f - 2.0f * x);
-}
-
-static inline float lerp(float a, float b, float t) {
-  return a * (1.0f - t) + b * t;
-}
-
-static inline QVector3D nlerp(const QVector3D &a, const QVector3D &b, float t) {
-  QVector3D v = a * (1.0f - t) + b * t;
-  if (v.lengthSquared() > 1e-6f)
-    v.normalize();
-  return v;
-}
-
 struct KnightExtras {
   QVector3D metalColor;
   QVector3D shieldColor;

+ 5 - 0
render/entity/mounted_knight_renderer.cpp

@@ -18,6 +18,7 @@
 #include "../submitter.h"
 #include "horse_renderer.h"
 #include "registry.h"
+#include "renderer_constants.h"
 #include <unordered_map>
 
 #include <QMatrix4x4>
@@ -33,6 +34,10 @@ using Render::Geom::clamp01;
 using Render::Geom::clampf;
 using Render::Geom::coneFromTo;
 using Render::Geom::cylinderBetween;
+using Render::Geom::easeInOutCubic;
+using Render::Geom::lerp;
+using Render::Geom::nlerp;
+using Render::Geom::smoothstep;
 using Render::Geom::sphereAt;
 
 static constexpr std::size_t MAX_EXTRAS_CACHE_SIZE = 10000;

+ 9 - 0
render/entity/renderer_constants.h

@@ -0,0 +1,9 @@
+#pragma once
+
+#include <cstddef>
+
+namespace Render::GL {
+
+static constexpr std::size_t MAX_EXTRAS_CACHE_SIZE = 10000;
+
+}

+ 4 - 0
render/entity/spearman_renderer.cpp

@@ -17,6 +17,7 @@
 #include "../scene_renderer.h"
 #include "../submitter.h"
 #include "registry.h"
+#include "renderer_constants.h"
 
 #include <QMatrix4x4>
 #include <QString>
@@ -32,6 +33,9 @@ using Render::Geom::clamp01;
 using Render::Geom::clampf;
 using Render::Geom::coneFromTo;
 using Render::Geom::cylinderBetween;
+using Render::Geom::easeInOutCubic;
+using Render::Geom::lerp;
+using Render::Geom::smoothstep;
 using Render::Geom::sphereAt;
 
 static constexpr std::size_t MAX_EXTRAS_CACHE_SIZE = 10000;

+ 25 - 0
render/geom/math_utils.h

@@ -2,6 +2,7 @@
 
 #include <QVector3D>
 #include <algorithm>
+#include <cmath>
 
 namespace Render::Geom {
 
@@ -20,4 +21,28 @@ inline QVector3D clampVec(const QVector3D &c, float minVal, float maxVal) {
                    clampf(c.z(), minVal, maxVal));
 }
 
+inline float lerp(float a, float b, float t) { return a * (1.0f - t) + b * t; }
+
+inline QVector3D lerp(const QVector3D &a, const QVector3D &b, float t) {
+  return a * (1.0f - t) + b * t;
+}
+
+inline float easeInOutCubic(float t) {
+  t = clamp01(t);
+  return t < 0.5f ? 4.0f * t * t * t
+                  : 1.0f - std::pow(-2.0f * t + 2.0f, 3.0f) / 2.0f;
+}
+
+inline float smoothstep(float a, float b, float x) {
+  x = clamp01((x - a) / (b - a));
+  return x * x * (3.0f - 2.0f * x);
+}
+
+inline QVector3D nlerp(const QVector3D &a, const QVector3D &b, float t) {
+  QVector3D v = a * (1.0f - t) + b * t;
+  if (v.lengthSquared() > 1e-6f)
+    v.normalize();
+  return v;
+}
+
 } // namespace Render::Geom

+ 79 - 5
ui/qml/ProductionPanel.qml

@@ -94,8 +94,8 @@ Rectangle {
                                             else
                                                 unitType = "archer";
                                         }
-                                        if (typeof Theme !== 'undefined' && Theme.unitIcons)
-                                            return Theme.unitIcons[unitType] || Theme.unitIcons["default"] || "👤";
+                                        if (typeof StyleGuide !== 'undefined' && StyleGuide.unitIcons)
+                                            return StyleGuide.unitIcons[unitType] || StyleGuide.unitIcons["default"] || "👤";
 
                                         return "🏹";
                                     }
@@ -270,7 +270,7 @@ Rectangle {
 
                                 Text {
                                     anchors.horizontalCenter: parent.horizontalCenter
-                                    text: (typeof Theme !== 'undefined' && Theme.unitIcons) ? Theme.unitIcons["archer"] || "🏹" : "🏹"
+                                    text: (typeof StyleGuide !== 'undefined' && StyleGuide.unitIcons) ? StyleGuide.unitIcons["archer"] || "🏹" : "🏹"
                                     color: parent.parent.parent.isEnabled ? "#ecf0f1" : "#5a5a5a"
                                     font.pointSize: 24
                                 }
@@ -344,7 +344,7 @@ Rectangle {
 
                                 Text {
                                     anchors.horizontalCenter: parent.horizontalCenter
-                                    text: (typeof Theme !== 'undefined' && Theme.unitIcons) ? Theme.unitIcons["knight"] || "⚔️" : "⚔️"
+                                    text: (typeof StyleGuide !== 'undefined' && StyleGuide.unitIcons) ? StyleGuide.unitIcons["knight"] || "⚔️" : "⚔️"
                                     color: parent.parent.parent.isEnabled ? "#ecf0f1" : "#5a5a5a"
                                     font.pointSize: 24
                                 }
@@ -418,7 +418,7 @@ Rectangle {
 
                                 Text {
                                     anchors.horizontalCenter: parent.horizontalCenter
-                                    text: (typeof Theme !== 'undefined' && Theme.unitIcons) ? Theme.unitIcons["spearman"] || "🛡️" : "🛡️"
+                                    text: (typeof StyleGuide !== 'undefined' && StyleGuide.unitIcons) ? StyleGuide.unitIcons["spearman"] || "🛡️" : "🛡️"
                                     color: parent.parent.parent.isEnabled ? "#ecf0f1" : "#5a5a5a"
                                     font.pointSize: 24
                                 }
@@ -474,6 +474,80 @@ Rectangle {
 
                         }
 
+                        Rectangle {
+                            property int queueTotal: (unitGridContent.prod.inProgress ? 1 : 0) + (unitGridContent.prod.queueSize || 0)
+                            property bool isEnabled: unitGridContent.prod.hasBarracks && unitGridContent.prod.producedCount < unitGridContent.prod.maxUnits && queueTotal < 5
+
+                            width: 110
+                            height: 80
+                            radius: 6
+                            color: isEnabled ? (mountedKnightMouseArea.containsMouse ? "#34495e" : "#2c3e50") : "#1a1a1a"
+                            border.color: isEnabled ? "#4a6572" : "#2a2a2a"
+                            border.width: 2
+                            opacity: isEnabled ? 1 : 0.5
+
+                            Column {
+                                anchors.centerIn: parent
+                                spacing: 4
+
+                                Text {
+                                    anchors.horizontalCenter: parent.horizontalCenter
+                                    text: (typeof StyleGuide !== 'undefined' && StyleGuide.unitIcons) ? StyleGuide.unitIcons["mounted_knight"] || "🐴" : "🐴"
+                                    color: parent.parent.parent.isEnabled ? "#ecf0f1" : "#5a5a5a"
+                                    font.pointSize: 24
+                                }
+
+                                Text {
+                                    anchors.horizontalCenter: parent.horizontalCenter
+                                    text: qsTr("Mounted Knight")
+                                    color: parent.parent.parent.isEnabled ? "#ecf0f1" : "#5a5a5a"
+                                    font.pointSize: 10
+                                    font.bold: true
+                                }
+
+                                Row {
+                                    anchors.horizontalCenter: parent.horizontalCenter
+                                    spacing: 4
+
+                                    Text {
+                                        text: "👥"
+                                        color: parent.parent.parent.parent.isEnabled ? "#f39c12" : "#5a5a5a"
+                                        font.pointSize: 9
+                                    }
+
+                                    Text {
+                                        text: unitGridContent.prod.villagerCost || 1
+                                        color: parent.parent.parent.parent.isEnabled ? "#f39c12" : "#5a5a5a"
+                                        font.pointSize: 9
+                                        font.bold: true
+                                    }
+
+                                }
+
+                            }
+
+                            MouseArea {
+                                id: mountedKnightMouseArea
+
+                                anchors.fill: parent
+                                hoverEnabled: true
+                                enabled: parent.isEnabled
+                                onClicked: productionPanel.recruitUnit("mounted_knight")
+                                cursorShape: parent.isEnabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
+                                ToolTip.visible: containsMouse
+                                ToolTip.text: parent.isEnabled ? qsTr("Recruit Mounted Knight\nCost: %1 villagers\nBuild time: %2s").arg(unitGridContent.prod.villagerCost || 1).arg((unitGridContent.prod.buildTime || 0).toFixed(0)) : (parent.queueTotal >= 5 ? qsTr("Queue is full (5/5)") : (unitGridContent.prod.producedCount >= unitGridContent.prod.maxUnits ? qsTr("Unit cap reached") : qsTr("Cannot recruit")))
+                                ToolTip.delay: 300
+                            }
+
+                            Rectangle {
+                                anchors.fill: parent
+                                color: "#ffffff"
+                                opacity: mountedKnightMouseArea.pressed ? 0.2 : 0
+                                radius: parent.radius
+                            }
+
+                        }
+
                     }
 
                 }

+ 7 - 0
ui/qml/StyleGuide.qml

@@ -123,4 +123,11 @@ QtObject {
         "normal": 160,
         "slow": 200
     })
+    readonly property var unitIcons: ({
+        "archer": "🏹",
+        "knight": "⚔️",
+        "spearman": "🛡️",
+        "mounted_knight": "🐴",
+        "default": "👤"
+    })
 }