瀏覽代碼

Remove backward compatibility, use enums everywhere except external boundaries

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] 1 月之前
父節點
當前提交
4a1598dde8

+ 4 - 3
game/audio/AudioEventHandler.cpp

@@ -91,7 +91,8 @@ void AudioEventHandler::onUnitSelected(
     return;
   }
 
-  auto it = m_unitVoiceMap.find(unitComponent->unitType);
+  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();
     auto timeSinceLastSound =
@@ -100,7 +101,7 @@ void AudioEventHandler::onUnitSelected(
             .count();
 
     bool shouldPlay = (timeSinceLastSound >= SELECTION_SOUND_COOLDOWN_MS) ||
-                      (unitComponent->unitType != m_lastSelectionUnitType);
+                      (unitTypeStr != m_lastSelectionUnitType);
 
     if (shouldPlay) {
       AudioCategory category =
@@ -108,7 +109,7 @@ void AudioEventHandler::onUnitSelected(
       AudioSystem::getInstance().playSound(it->second, 1.0f, false, 5,
                                            category);
       m_lastSelectionSoundTime = now;
-      m_lastSelectionUnitType = unitComponent->unitType;
+      m_lastSelectionUnitType = unitTypeStr;
     }
   }
 }

+ 2 - 3
game/core/component.h

@@ -53,13 +53,12 @@ public:
   UnitComponent(int health = 100, int maxHealth = 100, float speed = 1.0f,
                 float vision = 12.0f)
       : health(health), maxHealth(maxHealth), speed(speed), ownerId(0),
-        visionRange(vision) {}
+        visionRange(vision), spawnType(Game::Units::SpawnType::Archer) {}
 
   int health;
   int maxHealth;
   float speed;
-  std::string unitType;
-  std::optional<Game::Units::SpawnType> spawnTypeEnum;
+  Game::Units::SpawnType spawnType;
   int ownerId;
   float visionRange;
 };

+ 9 - 8
game/core/event_manager.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "../units/spawn_type.h"
 #include "entity.h"
 #include <algorithm>
 #include <functional>
@@ -177,30 +178,30 @@ public:
 
 class UnitDiedEvent : public Event {
 public:
-  UnitDiedEvent(EntityID unitId, int ownerId, const std::string &unitType,
+  UnitDiedEvent(EntityID unitId, int ownerId, Game::Units::SpawnType spawnType,
                 EntityID killerId = 0, int killerOwnerId = 0)
-      : unitId(unitId), ownerId(ownerId), unitType(unitType),
+      : unitId(unitId), ownerId(ownerId), spawnType(spawnType),
         killerId(killerId), killerOwnerId(killerOwnerId) {}
   EntityID unitId;
   int ownerId;
-  std::string unitType;
+  Game::Units::SpawnType spawnType;
   EntityID killerId;
   int killerOwnerId;
 };
 
 class UnitSpawnedEvent : public Event {
 public:
-  UnitSpawnedEvent(EntityID unitId, int ownerId, const std::string &unitType)
-      : unitId(unitId), ownerId(ownerId), unitType(unitType) {}
+  UnitSpawnedEvent(EntityID unitId, int ownerId, Game::Units::SpawnType spawnType)
+      : unitId(unitId), ownerId(ownerId), spawnType(spawnType) {}
   EntityID unitId;
   int ownerId;
-  std::string unitType;
+  Game::Units::SpawnType spawnType;
 };
 
 class BuildingAttackedEvent : public Event {
 public:
   BuildingAttackedEvent(EntityID buildingId, int ownerId,
-                        const std::string &buildingType,
+                        Game::Units::SpawnType buildingType,
                         EntityID attackerId = 0, int attackerOwnerId = 0,
                         int damage = 0)
       : buildingId(buildingId), ownerId(ownerId), buildingType(buildingType),
@@ -208,7 +209,7 @@ public:
         damage(damage) {}
   EntityID buildingId;
   int ownerId;
-  std::string buildingType;
+  Game::Units::SpawnType buildingType;
   EntityID attackerId;
   int attackerOwnerId;
   int damage;

+ 10 - 8
game/core/serialization.cpp

@@ -97,7 +97,8 @@ QJsonObject Serialization::serializeEntity(const Entity *entity) {
     unitObj["maxHealth"] = unit->maxHealth;
     unitObj["speed"] = unit->speed;
     unitObj["visionRange"] = unit->visionRange;
-    unitObj["unitType"] = QString::fromStdString(unit->unitType);
+    unitObj["unitType"] = 
+        QString::fromStdString(Game::Units::spawnTypeToString(unit->spawnType));
     unitObj["ownerId"] = unit->ownerId;
     entityObj["unit"] = unitObj;
   }
@@ -260,17 +261,18 @@ 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));
-    unit->unitType = unitObj["unitType"].toString().toStdString();
-    unit->ownerId = unitObj["ownerId"].toInt(0);
     
+    QString unitTypeStr = unitObj["unitType"].toString();
     Game::Units::SpawnType spawnType;
-    if (Game::Units::tryParseSpawnType(
-            QString::fromStdString(unit->unitType), spawnType)) {
-      unit->spawnTypeEnum = spawnType;
+    if (Game::Units::tryParseSpawnType(unitTypeStr, spawnType)) {
+      unit->spawnType = spawnType;
     } else {
-      qWarning() << "Unknown spawn type in save file:" 
-                 << QString::fromStdString(unit->unitType);
+      qWarning() << "Unknown spawn type in save file:" << unitTypeStr
+                 << "- defaulting to Archer";
+      unit->spawnType = Game::Units::SpawnType::Archer;
     }
+    
+    unit->ownerId = unitObj["ownerId"].toInt(0);
   }
 
   if (json.contains("movement")) {

+ 1 - 5
game/map/level_loader.cpp

@@ -69,7 +69,6 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
         sp.position = QVector3D(0.0f, 0.0f, 0.0f);
         sp.playerId = 0;
         sp.spawnType = Game::Units::SpawnType::Archer;
-        sp.unitType = Game::Units::spawnTypeToString(sp.spawnType);
         sp.aiControlled = !owners.isPlayer(sp.playerId);
         if (auto unit =
                 reg->create(Game::Units::SpawnType::Archer, world, sp)) {
@@ -83,8 +82,7 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
     bool hasBarracks = false;
     for (auto *e : world.getEntitiesWith<Engine::Core::UnitComponent>()) {
       if (auto *u = e->getComponent<Engine::Core::UnitComponent>()) {
-        if (u->spawnTypeEnum && 
-            *u->spawnTypeEnum == Game::Units::SpawnType::Barracks &&
+        if (u->spawnType == Game::Units::SpawnType::Barracks &&
             owners.isPlayer(u->ownerId)) {
           hasBarracks = true;
           break;
@@ -98,7 +96,6 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
         sp.position = QVector3D(-4.0f, 0.0f, -3.0f);
         sp.playerId = owners.getLocalPlayerId();
         sp.spawnType = Game::Units::SpawnType::Barracks;
-        sp.unitType = Game::Units::spawnTypeToString(sp.spawnType);
         sp.aiControlled = !owners.isPlayer(sp.playerId);
         reg2->create(Game::Units::SpawnType::Barracks, world, sp);
       }
@@ -123,7 +120,6 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
       sp.position = QVector3D(0.0f, 0.0f, 0.0f);
       sp.playerId = 0;
       sp.spawnType = Game::Units::SpawnType::Archer;
-      sp.unitType = Game::Units::spawnTypeToString(sp.spawnType);
       sp.aiControlled = !owners.isPlayer(sp.playerId);
       if (auto unit = reg->create(Game::Units::SpawnType::Archer, world, sp)) {
         res.playerUnitId = unit->id();

+ 0 - 1
game/map/map_transformer.cpp

@@ -147,7 +147,6 @@ MapTransformer::applyToWorld(const MapDefinition &def,
       sp.position = QVector3D(worldX, 0.0f, worldZ);
       sp.playerId = s.playerId;
       sp.spawnType = s.type;
-      sp.unitType = Game::Units::spawnTypeToString(s.type);
       sp.aiControlled = !ownerRegistry.isPlayer(s.playerId);
       sp.maxPopulation = s.maxPopulation;
       auto obj = s_registry->create(s.type, world, sp);

+ 1 - 2
game/map/skirmish_loader.cpp

@@ -351,8 +351,7 @@ SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
     auto *u = e->getComponent<Engine::Core::UnitComponent>();
     if (!u)
       continue;
-    if (u->spawnTypeEnum && 
-        *u->spawnTypeEnum == Game::Units::SpawnType::Barracks && 
+    if (u->spawnType == Game::Units::SpawnType::Barracks && 
         u->ownerId == playerOwnerId &&
         u->health > 0) {
       focusEntity = e;

+ 10 - 6
game/systems/ai_system/ai_reasoner.cpp

@@ -66,7 +66,8 @@ void AIReasoner::updateContext(const AISnapshot &snapshot, AIContext &ctx) {
     if (entity.isBuilding) {
       ctx.buildings.push_back(entity.id);
 
-      if (entity.unitType == "barracks" && ctx.primaryBarracks == 0) {
+      if (entity.spawnType == Game::Units::SpawnType::Barracks && 
+          ctx.primaryBarracks == 0) {
         ctx.primaryBarracks = entity.id;
         ctx.rallyX = entity.posX - 5.0f;
         ctx.rallyZ = entity.posZ;
@@ -81,11 +82,14 @@ void AIReasoner::updateContext(const AISnapshot &snapshot, AIContext &ctx) {
     ctx.totalUnits++;
 
     if (ctx.nation) {
-      auto troopType = Game::Units::troopTypeFromString(entity.unitType);
-      if (ctx.nation->isRangedUnit(troopType)) {
-        ctx.rangedCount++;
-      } else if (ctx.nation->isMeleeUnit(troopType)) {
-        ctx.meleeCount++;
+      auto troopTypeOpt = Game::Units::spawnTypeToTroopType(entity.spawnType);
+      if (troopTypeOpt) {
+        auto troopType = *troopTypeOpt;
+        if (ctx.nation->isRangedUnit(troopType)) {
+          ctx.rangedCount++;
+        } else if (ctx.nation->isMeleeUnit(troopType)) {
+          ctx.meleeCount++;
+        }
       }
     }
 

+ 2 - 2
game/systems/ai_system/ai_snapshot_builder.cpp

@@ -36,7 +36,7 @@ AISnapshot AISnapshotBuilder::build(const Engine::Core::World &world,
 
     EntitySnapshot data;
     data.id = entity->getId();
-    data.unitType = unit->unitType;
+    data.spawnType = unit->spawnType;
     data.ownerId = unit->ownerId;
     data.health = unit->health;
     data.maxHealth = unit->maxHealth;
@@ -97,7 +97,7 @@ AISnapshot AISnapshotBuilder::build(const Engine::Core::World &world,
 
     contact.health = unit->health;
     contact.maxHealth = unit->maxHealth;
-    contact.unitType = unit->unitType;
+    contact.spawnType = unit->spawnType;
 
     snapshot.visibleEnemies.push_back(std::move(contact));
   }

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

@@ -99,7 +99,8 @@ TacticalUtils::TargetScore TacticalUtils::selectFocusFireTarget(
       }
     }
 
-    float typePriority = getUnitTypePriority(enemy->unitType, context.nation);
+    float typePriority = getUnitTypePriority(
+        Game::Units::spawnTypeToString(enemy->spawnType), context.nation);
     score += typePriority * 3.0f;
 
     if (!enemy->isBuilding) {

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

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "../../units/spawn_type.h"
 #include "../../units/troop_type.h"
 #include <string>
 #include <unordered_map>
@@ -55,7 +56,7 @@ struct ProductionSnapshot {
 
 struct EntitySnapshot {
   Engine::Core::EntityID id = 0;
-  std::string unitType;
+  Game::Units::SpawnType spawnType = Game::Units::SpawnType::Archer;
   int ownerId = 0;
   int health = 0;
   int maxHealth = 0;
@@ -79,7 +80,7 @@ struct ContactSnapshot {
 
   int health = 0;
   int maxHealth = 0;
-  std::string unitType;
+  Game::Units::SpawnType spawnType = Game::Units::SpawnType::Archer;
 };
 
 struct AISnapshot {

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

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

+ 1 - 2
game/systems/camera_service.cpp

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

+ 3 - 5
game/systems/capture_system.cpp

@@ -31,8 +31,7 @@ int CaptureSystem::countNearbyTroops(Engine::Core::World *world, float barrackX,
     if (unit->ownerId != ownerId)
       continue;
 
-    if (unit->spawnTypeEnum && 
-        *unit->spawnTypeEnum == Game::Units::SpawnType::Barracks)
+    if (unit->spawnType == Game::Units::SpawnType::Barracks)
       continue;
 
     float dx = transform->position.x - barrackX;
@@ -42,7 +41,7 @@ int CaptureSystem::countNearbyTroops(Engine::Core::World *world, float barrackX,
     if (distSq <= radius * radius) {
       int individualsPerUnit =
           Game::Units::TroopConfig::instance().getIndividualsPerUnit(
-              unit->unitType);
+              unit->spawnType);
       totalTroops += individualsPerUnit;
     }
   }
@@ -111,8 +110,7 @@ void CaptureSystem::processBarrackCapture(Engine::Core::World *world,
     if (!unit || !transform)
       continue;
 
-    if (!unit->spawnTypeEnum || 
-        *unit->spawnTypeEnum != Game::Units::SpawnType::Barracks)
+    if (unit->spawnType != Game::Units::SpawnType::Barracks)
       continue;
 
     auto *capture = barrack->getComponent<Engine::Core::CaptureComponent>();

+ 5 - 7
game/systems/combat_system.cpp

@@ -121,13 +121,11 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
       auto *holdMode =
           attacker->getComponent<Engine::Core::HoldModeComponent>();
       if (holdMode && holdMode->active) {
-        if (attackerUnit->spawnTypeEnum && 
-            *attackerUnit->spawnTypeEnum == Game::Units::SpawnType::Archer) {
+        if (attackerUnit->spawnType == Game::Units::SpawnType::Archer) {
 
           range *= 1.5f;
           damage = static_cast<int>(damage * 1.3f);
-        } else if (attackerUnit->spawnTypeEnum && 
-                   *attackerUnit->spawnTypeEnum == Game::Units::SpawnType::Spearman) {
+        } else if (attackerUnit->spawnType == Game::Units::SpawnType::Spearman) {
 
           damage = static_cast<int>(damage * 1.4f);
         }
@@ -467,7 +465,7 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
           if (attU) {
             int troopSize =
                 Game::Units::TroopConfig::instance().getIndividualsPerUnit(
-                    attU->unitType);
+                    attU->spawnType);
             int maxArrows = std::max(1, troopSize / 3);
 
             static thread_local std::mt19937 gen(std::random_device{}());
@@ -622,7 +620,7 @@ void CombatSystem::dealDamage(Engine::Core::World *world,
         unit->health > 0) {
       Engine::Core::EventManager::instance().publish(
           Engine::Core::BuildingAttackedEvent(target->getId(), unit->ownerId,
-                                              unit->unitType, attackerId,
+                                              unit->spawnType, attackerId,
                                               attackerOwnerId, damage));
     }
 
@@ -632,7 +630,7 @@ void CombatSystem::dealDamage(Engine::Core::World *world,
 
       Engine::Core::EventManager::instance().publish(
           Engine::Core::UnitDiedEvent(target->getId(), unit->ownerId,
-                                      unit->unitType, attackerId,
+                                      unit->spawnType, attackerId,
                                       killerOwnerId));
 
       auto *targetAtk = target->getComponent<Engine::Core::AttackComponent>();

+ 7 - 8
game/systems/global_stats_registry.cpp

@@ -71,10 +71,10 @@ void GlobalStatsRegistry::onUnitSpawned(
 
   auto &stats = m_playerStats[event.ownerId];
 
-  if (event.unitType != "barracks") {
+  if (event.spawnType != Game::Units::SpawnType::Barracks) {
     int individualsPerUnit =
         Game::Units::TroopConfig::instance().getIndividualsPerUnit(
-            event.unitType);
+            event.spawnType);
     stats.troopsRecruited += individualsPerUnit;
   } else {
 
@@ -84,7 +84,7 @@ void GlobalStatsRegistry::onUnitSpawned(
 
 void GlobalStatsRegistry::onUnitDied(const Engine::Core::UnitDiedEvent &event) {
 
-  if (event.unitType == "barracks") {
+  if (event.spawnType == Game::Units::SpawnType::Barracks) {
     auto it = m_playerStats.find(event.ownerId);
     if (it != m_playerStats.end()) {
       it->second.barracksOwned--;
@@ -101,10 +101,10 @@ void GlobalStatsRegistry::onUnitDied(const Engine::Core::UnitDiedEvent &event) {
     if (ownerRegistry.areEnemies(event.killerOwnerId, event.ownerId)) {
       auto &stats = m_playerStats[event.killerOwnerId];
 
-      if (event.unitType != "barracks") {
+      if (event.spawnType != Game::Units::SpawnType::Barracks) {
         int individualsPerUnit =
             Game::Units::TroopConfig::instance().getIndividualsPerUnit(
-                event.unitType);
+                event.spawnType);
         stats.enemiesKilled += individualsPerUnit;
       }
     }
@@ -147,13 +147,12 @@ void GlobalStatsRegistry::rebuildFromWorld(Engine::Core::World &world) {
 
     auto &stats = m_playerStats[unit->ownerId];
 
-    if (unit->spawnTypeEnum && 
-        *unit->spawnTypeEnum == Game::Units::SpawnType::Barracks) {
+    if (unit->spawnType == Game::Units::SpawnType::Barracks) {
       stats.barracksOwned++;
     } else {
       int individualsPerUnit =
           Game::Units::TroopConfig::instance().getIndividualsPerUnit(
-              unit->unitType);
+              unit->spawnType);
       stats.troopsRecruited += individualsPerUnit;
     }
   }

+ 1 - 2
game/systems/production_service.cpp

@@ -16,8 +16,7 @@ findFirstSelectedBarracks(Engine::Core::World &world,
       auto *u = e->getComponent<Engine::Core::UnitComponent>();
       if (!u || u->ownerId != ownerId)
         continue;
-      if (u->spawnTypeEnum && 
-          *u->spawnTypeEnum == Game::Units::SpawnType::Barracks)
+      if (u->spawnType == Game::Units::SpawnType::Barracks)
         return e;
     }
   }

+ 0 - 1
game/systems/production_system.cpp

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

+ 1 - 1
game/systems/selection_system.cpp

@@ -173,7 +173,7 @@ 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(u->unitType) == type)
+        if (QString::fromStdString(Game::Units::spawnTypeToString(u->spawnType)) == type)
           return true;
       }
     }

+ 3 - 5
game/systems/terrain_alignment_system.cpp

@@ -34,11 +34,9 @@ void TerrainAlignmentSystem::alignEntityToTerrain(
 
   float entityBaseOffset = 0.0f;
   if (auto *unit = entity->getComponent<Engine::Core::UnitComponent>()) {
-    if (!unit->unitType.empty()) {
-      entityBaseOffset =
-          Game::Units::TroopConfig::instance().getSelectionRingGroundOffset(
-              unit->unitType);
-    }
+    entityBaseOffset =
+        Game::Units::TroopConfig::instance().getSelectionRingGroundOffset(
+            unit->spawnType);
   }
 
   transform->position.y = terrainHeight + entityBaseOffset * transform->scale.y;

+ 6 - 7
game/systems/troop_count_registry.cpp

@@ -34,22 +34,22 @@ int TroopCountRegistry::getTroopCount(int ownerId) const {
 
 void TroopCountRegistry::onUnitSpawned(
     const Engine::Core::UnitSpawnedEvent &event) {
-  if (event.unitType == "barracks")
+  if (event.spawnType == Game::Units::SpawnType::Barracks)
     return;
 
   int individualsPerUnit =
       Game::Units::TroopConfig::instance().getIndividualsPerUnit(
-          event.unitType);
+          event.spawnType);
   m_troopCounts[event.ownerId] += individualsPerUnit;
 }
 
 void TroopCountRegistry::onUnitDied(const Engine::Core::UnitDiedEvent &event) {
-  if (event.unitType == "barracks")
+  if (event.spawnType == Game::Units::SpawnType::Barracks)
     return;
 
   int individualsPerUnit =
       Game::Units::TroopConfig::instance().getIndividualsPerUnit(
-          event.unitType);
+          event.spawnType);
   m_troopCounts[event.ownerId] -= individualsPerUnit;
   if (m_troopCounts[event.ownerId] < 0) {
     m_troopCounts[event.ownerId] = 0;
@@ -65,13 +65,12 @@ void TroopCountRegistry::rebuildFromWorld(Engine::Core::World &world) {
     if (!unit || unit->health <= 0)
       continue;
 
-    if (unit->spawnTypeEnum && 
-        *unit->spawnTypeEnum == Game::Units::SpawnType::Barracks)
+    if (unit->spawnType == Game::Units::SpawnType::Barracks)
       continue;
 
     int individualsPerUnit =
         Game::Units::TroopConfig::instance().getIndividualsPerUnit(
-            unit->unitType);
+            unit->spawnType);
     m_troopCounts[unit->ownerId] += individualsPerUnit;
   }
 }

+ 6 - 4
game/systems/victory_service.cpp

@@ -193,8 +193,9 @@ bool VictoryService::checkElimination(Engine::Core::World &world) {
     if (m_ownerRegistry.areAllies(m_localOwnerId, unit->ownerId))
       continue;
 
-    QString unitType = QString::fromStdString(unit->unitType);
-    if (std::find(m_keyStructures.begin(), m_keyStructures.end(), unitType) !=
+    QString unitTypeStr = 
+        QString::fromStdString(Game::Units::spawnTypeToString(unit->spawnType));
+    if (std::find(m_keyStructures.begin(), m_keyStructures.end(), unitTypeStr) !=
         m_keyStructures.end()) {
       enemyKeyStructuresAlive = true;
       break;
@@ -233,8 +234,9 @@ bool VictoryService::checkNoKeyStructures(Engine::Core::World &world) {
       continue;
 
     if (unit->ownerId == m_localOwnerId) {
-      QString unitType = QString::fromStdString(unit->unitType);
-      if (std::find(m_keyStructures.begin(), m_keyStructures.end(), unitType) !=
+      QString unitTypeStr = 
+          QString::fromStdString(Game::Units::spawnTypeToString(unit->spawnType));
+      if (std::find(m_keyStructures.begin(), m_keyStructures.end(), unitTypeStr) !=
           m_keyStructures.end()) {
         return false;
       }

+ 2 - 3
game/units/archer.cpp

@@ -45,8 +45,7 @@ void Archer::init(const SpawnParams &params) {
   m_r->visible = true;
 
   m_u = e->addComponent<Engine::Core::UnitComponent>();
-  m_u->unitType = m_typeString;
-  m_u->spawnTypeEnum = params.spawnType;
+  m_u->spawnType = params.spawnType;
   m_u->health = 80;
   m_u->maxHealth = 80;
   m_u->speed = 3.0f;
@@ -88,7 +87,7 @@ void Archer::init(const SpawnParams &params) {
   m_atk->maxHeightDifference = 2.0f;
 
   Engine::Core::EventManager::instance().publish(
-      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->unitType));
+      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->spawnType));
 }
 
 } // namespace Units

+ 2 - 3
game/units/barracks.cpp

@@ -34,8 +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_typeString;
-  m_u->spawnTypeEnum = params.spawnType;
+  m_u->spawnType = params.spawnType;
   m_u->health = 2000;
   m_u->maxHealth = 2000;
   m_u->speed = 0.0f;
@@ -76,7 +75,7 @@ void Barracks::init(const SpawnParams &params) {
   }
 
   Engine::Core::EventManager::instance().publish(
-      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->unitType));
+      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->spawnType));
 }
 
 } // namespace Units

+ 2 - 3
game/units/knight.cpp

@@ -45,8 +45,7 @@ void Knight::init(const SpawnParams &params) {
   m_r->visible = true;
 
   m_u = e->addComponent<Engine::Core::UnitComponent>();
-  m_u->unitType = m_typeString;
-  m_u->spawnTypeEnum = params.spawnType;
+  m_u->spawnType = params.spawnType;
   m_u->health = 150;
   m_u->maxHealth = 150;
   m_u->speed = 2.0f;
@@ -88,7 +87,7 @@ void Knight::init(const SpawnParams &params) {
   m_atk->maxHeightDifference = 2.0f;
 
   Engine::Core::EventManager::instance().publish(
-      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->unitType));
+      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->spawnType));
 }
 
 } // namespace Units

+ 2 - 3
game/units/mounted_knight.cpp

@@ -46,8 +46,7 @@ void MountedKnight::init(const SpawnParams &params) {
   m_r->visible = true;
 
   m_u = e->addComponent<Engine::Core::UnitComponent>();
-  m_u->unitType = m_typeString;
-  m_u->spawnTypeEnum = params.spawnType;
+  m_u->spawnType = params.spawnType;
   m_u->health = 200;
   m_u->maxHealth = 200;
   m_u->speed = 8.0f;
@@ -89,7 +88,7 @@ void MountedKnight::init(const SpawnParams &params) {
   m_atk->maxHeightDifference = 2.0f;
 
   Engine::Core::EventManager::instance().publish(
-      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->unitType));
+      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->spawnType));
 }
 
 } // namespace Units

+ 2 - 3
game/units/spearman.cpp

@@ -46,8 +46,7 @@ void Spearman::init(const SpawnParams &params) {
   m_r->visible = true;
 
   m_u = e->addComponent<Engine::Core::UnitComponent>();
-  m_u->unitType = m_typeString;
-  m_u->spawnTypeEnum = params.spawnType;
+  m_u->spawnType = params.spawnType;
   m_u->health = 120;
   m_u->maxHealth = 120;
   m_u->speed = 2.5f;
@@ -89,7 +88,7 @@ void Spearman::init(const SpawnParams &params) {
   m_atk->maxHeightDifference = 2.0f;
 
   Engine::Core::EventManager::instance().publish(
-      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->unitType));
+      Engine::Core::UnitSpawnedEvent(m_id, m_u->ownerId, m_u->spawnType));
 }
 
 } // namespace Units

+ 41 - 0
game/units/troop_config.h

@@ -1,5 +1,6 @@
 #pragma once
 
+#include "spawn_type.h"
 #include "troop_type.h"
 #include <string>
 #include <unordered_map>
@@ -50,18 +51,50 @@ public:
     return getIndividualsPerUnit(troopTypeFromString(unitType));
   }
 
+  int getIndividualsPerUnit(SpawnType spawnType) const {
+    auto troopTypeOpt = spawnTypeToTroopType(spawnType);
+    if (troopTypeOpt) {
+      return getIndividualsPerUnit(*troopTypeOpt);
+    }
+    return 1;
+  }
+
   int getMaxUnitsPerRow(const std::string &unitType) const {
     return getMaxUnitsPerRow(troopTypeFromString(unitType));
   }
 
+  int getMaxUnitsPerRow(SpawnType spawnType) const {
+    auto troopTypeOpt = spawnTypeToTroopType(spawnType);
+    if (troopTypeOpt) {
+      return getMaxUnitsPerRow(*troopTypeOpt);
+    }
+    return 10;
+  }
+
   float getSelectionRingSize(const std::string &unitType) const {
     return getSelectionRingSize(troopTypeFromString(unitType));
   }
 
+  float getSelectionRingSize(SpawnType spawnType) const {
+    auto troopTypeOpt = spawnTypeToTroopType(spawnType);
+    if (troopTypeOpt) {
+      return getSelectionRingSize(*troopTypeOpt);
+    }
+    return 0.5f;
+  }
+
   float getSelectionRingYOffset(const std::string &unitType) const {
     return getSelectionRingYOffset(troopTypeFromString(unitType));
   }
 
+  float getSelectionRingYOffset(SpawnType spawnType) const {
+    auto troopTypeOpt = spawnTypeToTroopType(spawnType);
+    if (troopTypeOpt) {
+      return getSelectionRingYOffset(*troopTypeOpt);
+    }
+    return 0.0f;
+  }
+
   float getSelectionRingGroundOffset(TroopType unitType) const {
     auto it = m_selectionRingGroundOffset.find(unitType);
     if (it != m_selectionRingGroundOffset.end()) {
@@ -74,6 +107,14 @@ public:
     return getSelectionRingGroundOffset(troopTypeFromString(unitType));
   }
 
+  float getSelectionRingGroundOffset(SpawnType spawnType) const {
+    auto troopTypeOpt = spawnTypeToTroopType(spawnType);
+    if (troopTypeOpt) {
+      return getSelectionRingGroundOffset(*troopTypeOpt);
+    }
+    return 0.0f;
+  }
+
   void registerTroopType(TroopType unitType, int individualsPerUnit) {
     m_individualsPerUnit[unitType] = individualsPerUnit;
   }

+ 0 - 1
game/units/unit.h

@@ -28,7 +28,6 @@ struct SpawnParams {
   QVector3D position{0, 0, 0};
   int playerId = 0;
   SpawnType spawnType = SpawnType::Archer;
-  std::string unitType = "archer";
   bool aiControlled = false;
   int maxPopulation = 100;
 };