Jelajahi Sumber

Merge pull request #180 from djeada/copilot/allow-unowned-barracks

🏰 Allow Unowned (Neutral) Barracks
Adam Djellouli 2 bulan lalu
induk
melakukan
4cc6f9d0d6

+ 48 - 0
README.md

@@ -289,6 +289,54 @@ Maps can define custom victory and defeat conditions in their JSON files. Add a
 }
 ```
 
+### Neutral (Unowned) Barracks
+Maps can include neutral barracks that start without an owner. These barracks are inactive until captured by a player.
+
+**To create a neutral barracks, omit the `playerId` field:**
+```json
+{
+  "type": "barracks",
+  "x": 50,
+  "z": 50,
+  "maxPopulation": 150
+}
+```
+
+**Properties of neutral barracks:**
+- Appear **gray/neutral** on the map
+- Do **not produce troops**
+- Do **not respond** to player or AI commands
+- Can be **captured** by players (capture mechanics handled separately)
+- AI systems automatically **skip** neutral barracks
+
+**Example map with neutral barracks:**
+```json
+"spawns": [
+  {
+    "type": "barracks",
+    "x": 30,
+    "z": 50,
+    "playerId": 1,
+    "maxPopulation": 100
+  },
+  {
+    "type": "barracks",
+    "x": 50,
+    "z": 50,
+    "maxPopulation": 150
+  },
+  {
+    "type": "barracks",
+    "x": 70,
+    "z": 50,
+    "playerId": 2,
+    "maxPopulation": 100
+  }
+]
+```
+
+In this example, the middle barracks starts neutral while players 1 and 2 each have their own barracks.
+
 ## Contributing
 
 We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines on:

+ 88 - 0
assets/maps/neutral_barracks_test.json

@@ -0,0 +1,88 @@
+{
+  "name": "Neutral Barracks Test",
+  "description": "Test map demonstrating neutral (unowned) barracks. The barracks at (50, 50) has no playerId and will appear gray/neutral.",
+  "coordSystem": "grid",
+  "maxTroopsPerPlayer": 200,
+  "grid": {
+    "width": 100,
+    "height": 100,
+    "tileSize": 1.0
+  },
+  "biome": {
+    "seed": 12345,
+    "patchDensity": 3.2,
+    "patchJitter": 0.8,
+    "bladeHeight": [0.5, 1.2],
+    "bladeWidth": [0.025, 0.055],
+    "swayStrength": 0.25,
+    "swaySpeed": 1.2,
+    "heightNoise": [0.2, 0.08],
+    "grassPrimary": [0.3, 0.6, 0.35],
+    "grassSecondary": [0.4, 0.7, 0.36],
+    "grassDry": [0.55, 0.48, 0.38],
+    "soilColor": [0.3, 0.25, 0.2],
+    "rockLow": [0.48, 0.46, 0.44],
+    "rockHigh": [0.65, 0.66, 0.7]
+  },
+  "camera": {
+    "center": [50, 0, 50],
+    "distance": 20.0,
+    "tiltDeg": 45.0,
+    "yaw": 225.0,
+    "fovY": 45.0,
+    "near": 1.0,
+    "far": 200.0
+  },
+  "spawns": [
+    {
+      "type": "barracks",
+      "x": 30,
+      "z": 50,
+      "playerId": 1,
+      "maxPopulation": 100
+    },
+    {
+      "type": "archer",
+      "x": 28,
+      "z": 52,
+      "playerId": 1
+    },
+    {
+      "type": "archer",
+      "x": 32,
+      "z": 48,
+      "playerId": 1
+    },
+    {
+      "type": "barracks",
+      "x": 50,
+      "z": 50,
+      "maxPopulation": 150
+    },
+    {
+      "type": "barracks",
+      "x": 70,
+      "z": 50,
+      "playerId": 2,
+      "maxPopulation": 100
+    },
+    {
+      "type": "archer",
+      "x": 68,
+      "z": 52,
+      "playerId": 2
+    },
+    {
+      "type": "archer",
+      "x": 72,
+      "z": 48,
+      "playerId": 2
+    }
+  ],
+  "terrain": [],
+  "victory": {
+    "type": "elimination",
+    "key_structures": ["barracks"],
+    "defeat_conditions": ["no_key_structures"]
+  }
+}

+ 61 - 0
game/core/NEUTRAL_BARRACKS.md

@@ -0,0 +1,61 @@
+# Neutral (Unowned) Barracks Implementation
+
+## Overview
+This document describes the implementation of neutral barracks - barracks that exist on the map without an assigned owner.
+
+## Neutral Owner ID
+- Constant: `Game::Core::NEUTRAL_OWNER_ID = -1`
+- Defined in: `game/core/ownership_constants.h`
+- Helper function: `Game::Core::isNeutralOwner(int ownerId)`
+
+## Key Implementation Points
+
+### Map Loading (`game/map/map_loader.cpp`)
+- The `readSpawns` function checks if `playerId` is present in the JSON
+- If missing or null, assigns `NEUTRAL_OWNER_ID (-1)`
+- Example JSON:
+  ```json
+  {
+    "type": "barracks",
+    "x": 50,
+    "z": 50,
+    "maxPopulation": 150
+  }
+  ```
+
+### Map Transformation (`game/map/map_transformer.cpp`)
+- Skips registering neutral spawns as players in the `OwnerRegistry`
+- Neutral entities are not added to any team or player group
+
+### Barracks Initialization (`game/units/barracks.cpp`)
+- Checks `isNeutralOwner(m_u->ownerId)` before adding `ProductionComponent`
+- Neutral barracks do NOT get a production component
+- Still registered in `BuildingCollisionRegistry` for pathfinding/collision
+- Still publishes `UnitSpawnedEvent` (with neutral owner ID)
+
+### Visual Rendering (`game/visuals/team_colors.h`)
+- `teamColorForOwner()` returns gray color (0.5, 0.5, 0.5) for neutral owners
+- Neutral barracks appear visually distinct from player-owned barracks
+
+### Production System (`game/systems/production_system.cpp`)
+- Added safety check to skip entities with neutral ownership
+- Since neutral barracks don't have `ProductionComponent`, this is defensive
+
+### AI System (`game/systems/ai_system/behaviors/production_behavior.cpp`)
+- Added explicit check to skip neutral barracks
+- AI's `AISnapshot` only includes owned entities, so neutral barracks are already excluded
+- Extra check added for safety and clarity
+
+## Combat and Interaction
+- Neutral barracks can be attacked (combat system allows it)
+- They behave as enemies to all players (not allies with anyone)
+- Future capture mechanics can change ownership from neutral to a player
+
+## Testing
+- Test map: `assets/maps/neutral_barracks_test.json`
+- Contains barracks for player 1, neutral barracks, and barracks for player 2
+
+## Future Enhancements
+- Implement capture mechanics to claim neutral barracks
+- Add visual indicators for neutral structures (flags, icons)
+- Consider making neutral structures invulnerable until capture initiated

+ 11 - 0
game/core/ownership_constants.h

@@ -0,0 +1,11 @@
+#pragma once
+
+namespace Game {
+namespace Core {
+
+constexpr int NEUTRAL_OWNER_ID = -1;
+
+inline bool isNeutralOwner(int ownerId) { return ownerId == NEUTRAL_OWNER_ID; }
+
+} // namespace Core
+} // namespace Game

+ 7 - 1
game/map/map_loader.cpp

@@ -187,7 +187,13 @@ static void readSpawns(const QJsonArray &arr, std::vector<UnitSpawn> &out) {
     s.type = o.value("type").toString();
     s.x = float(o.value("x").toDouble(0.0));
     s.z = float(o.value("z").toDouble(0.0));
-    s.playerId = o.value("playerId").toInt(0);
+
+    if (o.contains("playerId") && !o.value("playerId").isNull()) {
+      s.playerId = o.value("playerId").toInt(0);
+    } else {
+      s.playerId = -1;
+    }
+
     s.teamId = o.value("teamId").toInt(0);
     s.maxPopulation = o.value("maxPopulation").toInt(100);
     out.push_back(s);

+ 4 - 0
game/map/map_transformer.cpp

@@ -1,6 +1,7 @@
 #include "map_transformer.h"
 
 #include "../core/component.h"
+#include "../core/ownership_constants.h"
 #include "../core/world.h"
 #include "../systems/owner_registry.h"
 #include "../units/factory.h"
@@ -56,6 +57,9 @@ MapTransformer::applyToWorld(const MapDefinition &def,
   std::unordered_map<int, int> playerIdToTeam;
 
   for (const auto &spawn : def.spawns) {
+    if (spawn.playerId == Game::Core::NEUTRAL_OWNER_ID) {
+      continue;
+    }
     uniquePlayerIds.insert(spawn.playerId);
 
     if (spawn.teamId > 0) {

+ 4 - 0
game/systems/ai_system/behaviors/production_behavior.cpp

@@ -1,4 +1,5 @@
 #include "production_behavior.h"
+#include "../../../core/ownership_constants.h"
 #include "../../nation_registry.h"
 #include "../ai_tactical.h"
 
@@ -54,6 +55,9 @@ void ProductionBehavior::execute(const AISnapshot &snapshot, AIContext &context,
     if (!entity.isBuilding || entity.unitType != "barracks")
       continue;
 
+    if (Game::Core::isNeutralOwner(entity.ownerId))
+      continue;
+
     static int logCounter = 0;
 
     if (!entity.production.hasComponent)

+ 7 - 0
game/systems/production_system.cpp

@@ -1,5 +1,6 @@
 #include "production_system.h"
 #include "../core/component.h"
+#include "../core/ownership_constants.h"
 #include "../core/world.h"
 #include "../game_config.h"
 #include "../map/map_transformer.h"
@@ -18,6 +19,12 @@ void ProductionSystem::update(Engine::Core::World *world, float deltaTime) {
     auto *prod = e->getComponent<Engine::Core::ProductionComponent>();
     if (!prod)
       continue;
+
+    auto *unitComp = e->getComponent<Engine::Core::UnitComponent>();
+    if (unitComp && Game::Core::isNeutralOwner(unitComp->ownerId)) {
+      continue;
+    }
+
     if (!prod->inProgress)
       continue;
 

+ 16 - 13
game/units/barracks.cpp

@@ -1,6 +1,7 @@
 #include "barracks.h"
 #include "../core/component.h"
 #include "../core/event_manager.h"
+#include "../core/ownership_constants.h"
 #include "../core/world.h"
 #include "../systems/building_collision_registry.h"
 #include "../visuals/team_colors.h"
@@ -55,20 +56,22 @@ void Barracks::init(const SpawnParams &params) {
   Game::Systems::BuildingCollisionRegistry::instance().registerBuilding(
       m_id, m_type, m_t->position.x, m_t->position.z, m_u->ownerId);
 
-  if (auto *prod = e->addComponent<Engine::Core::ProductionComponent>()) {
-    prod->productType = "archer";
-    prod->buildTime = 10.0f;
-    prod->maxUnits = params.maxPopulation;
-    prod->inProgress = false;
-    prod->timeRemaining = 0.0f;
-    prod->producedCount = 0;
-    prod->rallyX = m_t->position.x + 4.0f;
-    prod->rallyZ = m_t->position.z + 2.0f;
-    prod->rallySet = true;
+  if (!Game::Core::isNeutralOwner(m_u->ownerId)) {
+    if (auto *prod = e->addComponent<Engine::Core::ProductionComponent>()) {
+      prod->productType = "archer";
+      prod->buildTime = 10.0f;
+      prod->maxUnits = params.maxPopulation;
+      prod->inProgress = false;
+      prod->timeRemaining = 0.0f;
+      prod->producedCount = 0;
+      prod->rallyX = m_t->position.x + 4.0f;
+      prod->rallyZ = m_t->position.z + 2.0f;
+      prod->rallySet = true;
 
-    prod->villagerCost =
-        Game::Units::TroopConfig::instance().getIndividualsPerUnit(
-            prod->productType);
+      prod->villagerCost =
+          Game::Units::TroopConfig::instance().getIndividualsPerUnit(
+              prod->productType);
+    }
   }
 
   Engine::Core::EventManager::instance().publish(

+ 5 - 0
game/visuals/team_colors.h

@@ -1,10 +1,15 @@
 #pragma once
+#include "../core/ownership_constants.h"
 #include "../systems/owner_registry.h"
 #include <QVector3D>
 
 namespace Game::Visuals {
 inline QVector3D teamColorForOwner(int ownerId) {
 
+  if (Game::Core::isNeutralOwner(ownerId)) {
+    return QVector3D(0.5f, 0.5f, 0.5f);
+  }
+
   auto &registry = Game::Systems::OwnerRegistry::instance();
   auto color = registry.getOwnerColor(ownerId);
   return QVector3D(color[0], color[1], color[2]);