Browse Source

improve map rendering

djeada 2 months ago
parent
commit
124d80d912

+ 152 - 5
app/game_engine.cpp

@@ -106,6 +106,8 @@ void GameEngine::onAttackClick(qreal sx, qreal sy) {
   ensureInitialized();
   if (!m_selectionSystem || !m_pickingService || !m_camera || !m_world)
     return;
+  (void)sx;
+  (void)sy;
 
   const auto &selected = m_selectionSystem->getSelectedUnits();
   if (selected.empty()) {
@@ -118,7 +120,7 @@ void GameEngine::onAttackClick(qreal sx, qreal sy) {
                                       m_viewport.width, m_viewport.height, 0);
 
   if (targetId == 0) {
-
+    setCursorMode("normal");
     return;
   }
 
@@ -129,6 +131,7 @@ void GameEngine::onAttackClick(qreal sx, qreal sy) {
 
   auto *targetUnit = targetEntity->getComponent<Engine::Core::UnitComponent>();
   if (!targetUnit) {
+    (void)targetId;
     return;
   }
 
@@ -173,10 +176,24 @@ void GameEngine::onStopCommand() {
 
     if (auto *movement =
             entity->getComponent<Engine::Core::MovementComponent>()) {
+      auto *transform =
+          entity->getComponent<Engine::Core::TransformComponent>();
       movement->hasTarget = false;
-      movement->targetX = 0.0f;
-      movement->targetY = 0.0f;
       movement->path.clear();
+      movement->pathPending = false;
+      movement->pendingRequestId = 0;
+      movement->repathCooldown = 0.0f;
+      if (transform) {
+        movement->targetX = transform->position.x;
+        movement->targetY = transform->position.z;
+        movement->goalX = transform->position.x;
+        movement->goalY = transform->position.z;
+      } else {
+        movement->targetX = 0.0f;
+        movement->targetY = 0.0f;
+        movement->goalX = 0.0f;
+        movement->goalY = 0.0f;
+      }
     }
 
     if (auto *attack =
@@ -242,6 +259,16 @@ void GameEngine::onPatrolClick(qreal sx, qreal sy) {
             entity->getComponent<Engine::Core::MovementComponent>()) {
       movement->hasTarget = false;
       movement->path.clear();
+      movement->pathPending = false;
+      movement->pendingRequestId = 0;
+      movement->repathCooldown = 0.0f;
+      if (auto *transform =
+              entity->getComponent<Engine::Core::TransformComponent>()) {
+        movement->targetX = transform->position.x;
+        movement->targetY = transform->position.z;
+        movement->goalX = transform->position.x;
+        movement->goalY = transform->position.z;
+      }
     }
     if (auto *attack =
             entity->getComponent<Engine::Core::AttackTargetComponent>()) {
@@ -406,6 +433,10 @@ void GameEngine::update(float dt) {
     m_renderer->updateAnimationTime(dt);
   }
 
+  if (m_camera) {
+    m_camera->update(dt);
+  }
+
   if (m_world) {
     m_world->update(dt);
 
@@ -470,6 +501,8 @@ void GameEngine::render(int pixelWidth, int pixelHeight) {
   }
   if (m_renderer)
     m_renderer->setHoveredEntityId(m_hover.entityId);
+  if (m_renderer)
+    m_renderer->setLocalOwnerId(m_runtime.localOwnerId);
   m_renderer->renderWorld(m_world.get());
   if (m_arrowSystem) {
     if (auto *res = m_renderer->resources())
@@ -525,14 +558,26 @@ void GameEngine::syncSelectionFlags() {
     for (auto id : toKeep)
       m_selectionSystem->selectUnit(id);
   }
+
+  if (m_selectionSystem->getSelectedUnits().empty()) {
+    if (m_runtime.cursorMode != "normal") {
+      setCursorMode("normal");
+    }
+  }
 }
 
 void GameEngine::cameraMove(float dx, float dz) {
   ensureInitialized();
   if (!m_camera)
     return;
+
+  float scale = 0.12f;
+  if (m_camera) {
+    float dist = m_camera->getDistance();
+    scale = std::max(0.12f, dist * 0.05f);
+  }
   Game::Systems::CameraController ctrl;
-  ctrl.move(*m_camera, dx, dz);
+  ctrl.move(*m_camera, dx * scale, dz * scale);
 }
 
 void GameEngine::cameraElevate(float dy) {
@@ -540,7 +585,56 @@ void GameEngine::cameraElevate(float dy) {
   if (!m_camera)
     return;
   Game::Systems::CameraController ctrl;
-  ctrl.elevate(*m_camera, dy);
+
+  float distance = m_camera ? m_camera->getDistance() : 10.0f;
+  float scale = std::clamp(distance * 0.05f, 0.1f, 5.0f);
+  ctrl.moveUp(*m_camera, dy * scale);
+}
+
+void GameEngine::resetCamera() {
+  ensureInitialized();
+  if (!m_camera || !m_world)
+    return;
+
+  Engine::Core::Entity *focusEntity = nullptr;
+  for (auto *e : m_world->getEntitiesWith<Engine::Core::UnitComponent>()) {
+    if (!e)
+      continue;
+    auto *u = e->getComponent<Engine::Core::UnitComponent>();
+    if (!u)
+      continue;
+    if (u->unitType == "barracks" && u->ownerId == m_runtime.localOwnerId &&
+        u->health > 0) {
+      focusEntity = e;
+      break;
+    }
+  }
+  if (!focusEntity && m_level.playerUnitId != 0)
+    focusEntity = m_world->getEntity(m_level.playerUnitId);
+
+  if (focusEntity) {
+    if (auto *t =
+            focusEntity->getComponent<Engine::Core::TransformComponent>()) {
+      QVector3D center(t->position.x, t->position.y, t->position.z);
+      if (m_camera) {
+        m_camera->setRTSView(center, 12.0f, 45.0f, 225.0f);
+      }
+    }
+  }
+}
+
+void GameEngine::cameraZoom(float delta) {
+  ensureInitialized();
+  if (!m_camera)
+    return;
+  Game::Systems::CameraController ctrl;
+  ctrl.zoomDistance(*m_camera, delta);
+}
+
+float GameEngine::cameraDistance() const {
+  if (!m_camera)
+    return 0.0f;
+  return m_camera->getDistance();
 }
 
 void GameEngine::cameraYaw(float degrees) {
@@ -555,10 +649,32 @@ void GameEngine::cameraOrbit(float yawDeg, float pitchDeg) {
   ensureInitialized();
   if (!m_camera)
     return;
+  qDebug() << "GameEngine::cameraOrbit called:" << "yaw=" << yawDeg
+           << "pitch=" << pitchDeg;
+  if (!std::isfinite(yawDeg) || !std::isfinite(pitchDeg)) {
+    qDebug()
+        << "GameEngine::cameraOrbit received invalid input, applying fallback"
+        << yawDeg << pitchDeg;
+    float fallback = 2.0f;
+
+    if (!std::isfinite(pitchDeg))
+      pitchDeg = fallback;
+    if (!std::isfinite(yawDeg))
+      yawDeg = 0.0f;
+  }
   Game::Systems::CameraController ctrl;
   ctrl.orbit(*m_camera, yawDeg, pitchDeg);
 }
 
+void GameEngine::cameraOrbitDirection(int direction, bool shift) {
+
+  float step = shift ? 8.0f : 4.0f;
+  float pitch = step * float(direction);
+  qDebug() << "GameEngine::cameraOrbitDirection" << direction << shift
+           << "-> pitch=" << pitch;
+  cameraOrbit(0.0f, pitch);
+}
+
 void GameEngine::cameraFollowSelection(bool enable) {
   ensureInitialized();
   m_followSelectionEnabled = enable;
@@ -833,6 +949,37 @@ void GameEngine::startSkirmish(const QString &mapPath) {
       m_renderer->unlockWorldForModification();
       m_renderer->resume();
     }
+
+    if (m_world && m_camera) {
+      Engine::Core::Entity *focusEntity = nullptr;
+
+      auto candidates = m_world->getEntitiesWith<Engine::Core::UnitComponent>();
+      for (auto *e : candidates) {
+        if (!e)
+          continue;
+        auto *u = e->getComponent<Engine::Core::UnitComponent>();
+        if (!u)
+          continue;
+        if (u->unitType == "barracks" && u->ownerId == m_runtime.localOwnerId &&
+            u->health > 0) {
+          focusEntity = e;
+          break;
+        }
+      }
+
+      if (!focusEntity && m_level.playerUnitId != 0) {
+        focusEntity = m_world->getEntity(m_level.playerUnitId);
+      }
+
+      if (focusEntity) {
+        if (auto *t =
+                focusEntity->getComponent<Engine::Core::TransformComponent>()) {
+          QVector3D center(t->position.x, t->position.y, t->position.z);
+
+          m_camera->setRTSView(center, 12.0f, 45.0f, 225.0f);
+        }
+      }
+    }
     m_runtime.loading = false;
   }
 }

+ 4 - 0
app/game_engine.h

@@ -78,8 +78,12 @@ public:
 
   Q_INVOKABLE void cameraMove(float dx, float dz);
   Q_INVOKABLE void cameraElevate(float dy);
+  Q_INVOKABLE void resetCamera();
+  Q_INVOKABLE void cameraZoom(float delta);
+  Q_INVOKABLE float cameraDistance() const;
   Q_INVOKABLE void cameraYaw(float degrees);
   Q_INVOKABLE void cameraOrbit(float yawDeg, float pitchDeg);
+  Q_INVOKABLE void cameraOrbitDirection(int direction, bool shift);
   Q_INVOKABLE void cameraFollowSelection(bool enable);
   Q_INVOKABLE void cameraSetFollowLerp(float alpha);
 

+ 103 - 37
assets/maps/test_map.json

@@ -1,63 +1,129 @@
 {
-  "name": "Test Map (JSON)",
+  "name": "Test Map",
   "coordSystem": "grid",
   "maxTroopsPerPlayer": 30,
   "grid": {
-    "width": 120,
-    "height": 120,
+    "width": 300,
+    "height": 300,
     "tileSize": 1.0
   },
   "camera": {
-    "center": [0.0, 0.0, 0.0],
+    "center": [0, 0, 0],
     "distance": 25.0,
     "tiltDeg": 45.0,
+    "yaw": 225.0,
     "fovY": 45.0,
     "near": 1.0,
     "far": 400.0
   },
   "spawns": [
-    { "type": "barracks", "x": 45, "z": 50, "playerId": 1 },
-    { "type": "archer", "x": 50, "z": 50, "playerId": 1 },
-    { "type": "archer", "x": 52, "z": 50, "playerId": 1 },
-    { "type": "archer", "x": 50, "z": 52, "playerId": 1 },
-    { "type": "archer", "x": 48, "z": 52, "playerId": 1 },
-    { "type": "archer", "x": 52, "z": 52, "playerId": 1 },
-    { "type": "archer", "x": 48, "z": 48, "playerId": 1 },
-    { "type": "archer", "x": 52, "z": 48, "playerId": 1 },
-    { "type": "archer", "x": 50, "z": 48, "playerId": 1 },
-    { "type": "archer", "x": 48, "z": 48, "playerId": 1 },
-    
-    { "type": "barracks", "x": 65, "z": 50, "playerId": 2 },
-    { "type": "archer", "x": 56, "z": 50, "playerId": 2 }
+    { "type": "barracks", "x": 10, "z": 10, "playerId": 1 },
+    { "type": "archer", "x": 18, "z": 20, "playerId": 1 },
+    { "type": "archer", "x": 20, "z": 18, "playerId": 1 },
+    { "type": "archer", "x": 18, "z": 16, "playerId": 1 },
+    { "type": "archer", "x": 16, "z": 18, "playerId": 1 },
+    { "type": "archer", "x": 20, "z": 20, "playerId": 1 },
+    { "type": "archer", "x": 16, "z": 20, "playerId": 1 },
+    { "type": "archer", "x": 20, "z": 16, "playerId": 1 },
+    { "type": "archer", "x": 16, "z": 16, "playerId": 1 },
+    { "type": "barracks", "x": 130, "z": 130, "playerId": 2 },
+    { "type": "archer", "x": 28, "z": 28, "playerId": 2 },
+    { "type": "archer", "x": 100, "z": 102, "playerId": 2 },
+    { "type": "archer", "x": 102, "z": 104, "playerId": 2 },
+    { "type": "archer", "x": 104, "z": 102, "playerId": 2 },
+    { "type": "archer", "x": 100, "z": 100, "playerId": 2 },
+    { "type": "archer", "x": 104, "z": 104, "playerId": 2 },
+    { "type": "archer", "x": 100, "z": 104, "playerId": 2 },
+    { "type": "archer", "x": 104, "z": 100, "playerId": 2 }
   ],
   "terrain": [
     {
-      "type": "mountain",
-      "x": 58,
-      "z": 42,
-      "radius": 18,
-      "height": 7.2,
-      "rotation": 18
+      "type": "hill",
+      "x": 24,
+      "z": 24,
+      "radius": 8,
+      "height": 3.0,
+      "entrances": [
+        { "x": 24, "z": 16 },
+        { "x": 16, "z": 24 },
+        { "x": 32, "z": 24 }
+      ]
+    },
+    {
+      "type": "hill",
+      "x": 30,
+      "z": 18,
+      "radius": 5,
+      "height": 2.6,
+      "entrances": [
+        { "x": 24, "z": 18 },
+        { "x": 30, "z": 24 },
+        { "x": 36, "z": 18 }
+      ]
     },
     {
-      "type": "mountain",
-      "x": 34,
-      "z": 86,
-      "radius": 16,
-      "height": 6.4,
-      "rotation": 108
+      "type": "hill",
+      "x": 96,
+      "z": 96,
+      "radius": 8,
+      "height": 3.0,
+      "entrances": [
+        { "x": 96, "z": 104 },
+        { "x": 104, "z": 96 },
+        { "x": 88, "z": 96 }
+      ]
     },
     {
       "type": "hill",
-      "x": 78,
-      "z": 74,
-      "radius": 15,
-      "height": 3.1,
+      "x": 90,
+      "z": 102,
+      "radius": 5,
+      "height": 2.6,
       "entrances": [
-        { "x": 74, "z": 58 },
-        { "x": 88, "z": 78 },
-        { "x": 66, "z": 88 }
+        { "x": 96, "z": 102 },
+        { "x": 90, "z": 96 },
+        { "x": 84, "z": 102 }
       ]
-    }
+    },
+    { "type": "mountain", "x": 6, "z": 6, "radius": 7, "height": 6.0, "rotation": 0 },
+    { "type": "mountain", "x": 6, "z": 14, "radius": 6, "height": 6.0, "rotation": 20 },
+    { "type": "mountain", "x": 14, "z": 6, "radius": 6, "height": 6.0, "rotation": 40 },
+    { "type": "mountain", "x": 114, "z": 114, "radius": 7, "height": 6.0, "rotation": 0 },
+    { "type": "mountain", "x": 114, "z": 106, "radius": 6, "height": 6.0, "rotation": 20 },
+    { "type": "mountain", "x": 106, "z": 114, "radius": 6, "height": 6.0, "rotation": 40 },
+    { "type": "mountain", "x": 60, "z": 10, "radius": 8, "height": 8.0, "rotation": 15 },
+    { "type": "mountain", "x": 60, "z": 24, "radius": 8, "height": 8.0, "rotation": 25 },
+    { "type": "mountain", "x": 60, "z": 38, "radius": 8, "height": 8.0, "rotation": 35 },
+    { "type": "mountain", "x": 60, "z": 52, "radius": 8, "height": 8.0, "rotation": 45 },
+    { "type": "mountain", "x": 60, "z": 68, "radius": 8, "height": 8.0, "rotation": 55 },
+    { "type": "mountain", "x": 60, "z": 82, "radius": 8, "height": 8.0, "rotation": 65 },
+    { "type": "mountain", "x": 60, "z": 96, "radius": 8, "height": 8.0, "rotation": 75 },
+    { "type": "mountain", "x": 60, "z": 110, "radius": 8, "height": 8.0, "rotation": 85 },
+    { "type": "mountain", "x": 10, "z": 60, "radius": 8, "height": 8.0, "rotation": 105 },
+    { "type": "mountain", "x": 24, "z": 60, "radius": 8, "height": 8.0, "rotation": 115 },
+    { "type": "mountain", "x": 38, "z": 60, "radius": 8, "height": 8.0, "rotation": 125 },
+    { "type": "mountain", "x": 52, "z": 60, "radius": 8, "height": 8.0, "rotation": 135 },
+    { "type": "mountain", "x": 68, "z": 60, "radius": 8, "height": 8.0, "rotation": 145 },
+    { "type": "mountain", "x": 82, "z": 60, "radius": 8, "height": 8.0, "rotation": 155 },
+    { "type": "mountain", "x": 96, "z": 60, "radius": 8, "height": 8.0, "rotation": 165 },
+    { "type": "mountain", "x": 110, "z": 60, "radius": 8, "height": 8.0, "rotation": 175 },
+    { "type": "mountain", "x": 30, "z": 30, "radius": 7, "height": 7.0, "rotation": 10 },
+    { "type": "mountain", "x": 42, "z": 42, "radius": 7, "height": 7.0, "rotation": 20 },
+    { "type": "mountain", "x": 54, "z": 54, "radius": 7, "height": 7.0, "rotation": 30 },
+    { "type": "mountain", "x": 66, "z": 66, "radius": 7, "height": 7.0, "rotation": 40 },
+    { "type": "mountain", "x": 78, "z": 78, "radius": 7, "height": 7.0, "rotation": 50 },
+    { "type": "mountain", "x": 90, "z": 90, "radius": 7, "height": 7.0, "rotation": 60 },
+    { "type": "mountain", "x": 90, "z": 30, "radius": 7, "height": 7.0, "rotation": 200 },
+    { "type": "mountain", "x": 78, "z": 42, "radius": 7, "height": 7.0, "rotation": 210 },
+    { "type": "mountain", "x": 66, "z": 54, "radius": 7, "height": 7.0, "rotation": 220 },
+    { "type": "mountain", "x": 54, "z": 66, "radius": 7, "height": 7.0, "rotation": 230 },
+    { "type": "mountain", "x": 42, "z": 78, "radius": 7, "height": 7.0, "rotation": 240 },
+    { "type": "mountain", "x": 30, "z": 90, "radius": 7, "height": 7.0, "rotation": 250 },
+    { "type": "mountain", "x": 24, "z": 40, "radius": 6, "height": 6.0, "rotation": 15 },
+    { "type": "mountain", "x": 20, "z": 48, "radius": 6, "height": 6.0, "rotation": 30 },
+    { "type": "mountain", "x": 32, "z": 48, "radius": 6, "height": 6.0, "rotation": 60 },
+    { "type": "mountain", "x": 96, "z": 72, "radius": 6, "height": 6.0, "rotation": 195 },
+    { "type": "mountain", "x": 96, "z": 84, "radius": 6, "height": 6.0, "rotation": 210 },
+    { "type": "mountain", "x": 84, "z": 96, "radius": 6, "height": 6.0, "rotation": 225 }
   ]
 }

+ 5 - 2
game/core/component.h

@@ -63,15 +63,18 @@ public:
 class MovementComponent : public Component {
 public:
   MovementComponent()
-      : hasTarget(false), targetX(0.0f), targetY(0.0f), vx(0.0f), vz(0.0f),
-        pathPending(false), pendingRequestId(0) {}
+      : hasTarget(false), targetX(0.0f), targetY(0.0f), goalX(0.0f),
+        goalY(0.0f), vx(0.0f), vz(0.0f), pathPending(false),
+        pendingRequestId(0), repathCooldown(0.0f) {}
 
   bool hasTarget;
   float targetX, targetY;
+  float goalX, goalY;
   float vx, vz;
   std::vector<std::pair<float, float>> path;
   bool pathPending;
   std::uint64_t pendingRequestId;
+  float repathCooldown;
 };
 
 class AttackComponent : public Component {

+ 4 - 2
game/map/environment.cpp

@@ -9,7 +9,9 @@ namespace Map {
 void Environment::apply(const MapDefinition &def,
                         Render::GL::Renderer &renderer,
                         Render::GL::Camera &camera) {
-  camera.setRTSView(def.camera.center, def.camera.distance, def.camera.tiltDeg);
+
+  camera.setRTSView(def.camera.center, def.camera.distance, def.camera.tiltDeg,
+                    def.camera.yawDeg);
   camera.setPerspective(def.camera.fovY, 16.0f / 9.0f, def.camera.nearPlane,
                         def.camera.farPlane);
   Render::GL::Renderer::GridParams gp;
@@ -21,7 +23,7 @@ void Environment::apply(const MapDefinition &def,
 
 void Environment::applyDefault(Render::GL::Renderer &renderer,
                                Render::GL::Camera &camera) {
-  camera.setRTSView(QVector3D(0, 0, 0), 15.0f, 45.0f);
+  camera.setRTSView(QVector3D(0, 0, 0), 15.0f, 45.0f, 225.0f);
 
   camera.setPerspective(45.0f, 16.0f / 9.0f, 1.0f, 200.0f);
   Render::GL::Renderer::GridParams gp;

+ 2 - 0
game/map/map_definition.h

@@ -21,6 +21,8 @@ struct CameraDefinition {
 
   float nearPlane = 1.0f;
   float farPlane = 200.0f;
+
+  float yawDeg = 225.0f;
 };
 
 struct UnitSpawn {

+ 5 - 0
game/map/map_loader.cpp

@@ -36,6 +36,11 @@ static bool readCamera(const QJsonObject &obj, CameraDefinition &cam) {
     cam.nearPlane = float(obj.value("near").toDouble(cam.nearPlane));
   if (obj.contains("far"))
     cam.farPlane = float(obj.value("far").toDouble(cam.farPlane));
+  if (obj.contains("yaw") || obj.contains("yawDeg")) {
+
+    const QString k = obj.contains("yaw") ? "yaw" : "yawDeg";
+    cam.yawDeg = float(obj.value(k).toDouble(cam.yawDeg));
+  }
   return true;
 }
 

+ 29 - 0
game/map/map_transformer.cpp

@@ -3,6 +3,7 @@
 #include "../core/component.h"
 #include "../core/world.h"
 #include "../units/factory.h"
+#include "terrain_service.h"
 #include <QDebug>
 #include <QVector3D>
 
@@ -38,6 +39,34 @@ MapTransformer::applyToWorld(const MapDefinition &def,
       worldZ = (s.z - (def.grid.height * 0.5f - 0.5f)) * tile;
     }
 
+    auto &terrain = Game::Map::TerrainService::instance();
+    if (terrain.isInitialized() && terrain.isForbiddenWorld(worldX, worldZ)) {
+      const float tile = std::max(0.0001f, def.grid.tileSize);
+      bool found = false;
+      const int maxRadius = 12;
+      for (int r = 1; r <= maxRadius && !found; ++r) {
+        for (int ox = -r; ox <= r && !found; ++ox) {
+          for (int oz = -r; oz <= r && !found; ++oz) {
+
+            if (std::abs(ox) != r && std::abs(oz) != r)
+              continue;
+            float candX = worldX + float(ox) * tile;
+            float candZ = worldZ + float(oz) * tile;
+            if (!terrain.isForbiddenWorld(candX, candZ)) {
+              worldX = candX;
+              worldZ = candZ;
+              found = true;
+            }
+          }
+        }
+      }
+      if (!found) {
+        qWarning()
+            << "MapTransformer: spawn at" << s.x << s.z
+            << "is forbidden and no nearby free tile found; spawning anyway";
+      }
+    }
+
     Engine::Core::Entity *e = nullptr;
     if (s_registry) {
       Game::Units::SpawnParams sp;

+ 1 - 1
game/map/terrain.cpp

@@ -54,7 +54,7 @@ void TerrainHeightMap::buildFromFeatures(
       const int maxZ =
           std::min(m_height - 1, int(std::ceil(gridCenterZ + bound)));
 
-  const float angleRad = feature.rotationDeg * kDegToRad;
+      const float angleRad = feature.rotationDeg * kDegToRad;
       const float cosA = std::cos(angleRad);
       const float sinA = std::sin(angleRad);
 

+ 36 - 0
game/map/terrain_service.cpp

@@ -1,4 +1,5 @@
 #include "terrain_service.h"
+#include "../systems/building_collision_registry.h"
 #include "map_definition.h"
 #include <QDebug>
 
@@ -37,6 +38,41 @@ bool TerrainService::isWalkable(int gridX, int gridZ) const {
   return m_heightMap->isWalkable(gridX, gridZ);
 }
 
+bool TerrainService::isForbidden(int gridX, int gridZ) const {
+  if (!m_heightMap)
+    return false;
+
+  if (!m_heightMap->isWalkable(gridX, gridZ))
+    return true;
+
+  float halfW = m_heightMap->getWidth() * 0.5f - 0.5f;
+  float halfH = m_heightMap->getHeight() * 0.5f - 0.5f;
+  const float tile = m_heightMap->getTileSize();
+  float worldX = (static_cast<float>(gridX) - halfW) * tile;
+  float worldZ = (static_cast<float>(gridZ) - halfH) * tile;
+
+  auto &registry = Game::Systems::BuildingCollisionRegistry::instance();
+  if (registry.isPointInBuilding(worldX, worldZ))
+    return true;
+
+  return false;
+}
+
+bool TerrainService::isForbiddenWorld(float worldX, float worldZ) const {
+  if (!m_heightMap)
+    return false;
+
+  const float gridHalfWidth = m_heightMap->getWidth() * 0.5f - 0.5f;
+  const float gridHalfHeight = m_heightMap->getHeight() * 0.5f - 0.5f;
+
+  float gx = worldX / m_heightMap->getTileSize() + gridHalfWidth;
+  float gz = worldZ / m_heightMap->getTileSize() + gridHalfHeight;
+
+  int ix = static_cast<int>(std::round(gx));
+  int iz = static_cast<int>(std::round(gz));
+  return isForbidden(ix, iz);
+}
+
 bool TerrainService::isHillEntrance(int gridX, int gridZ) const {
   if (!m_heightMap)
     return false;

+ 4 - 0
game/map/terrain_service.h

@@ -21,6 +21,10 @@ public:
 
   bool isHillEntrance(int gridX, int gridZ) const;
 
+  bool isForbidden(int gridX, int gridZ) const;
+
+  bool isForbiddenWorld(float worldX, float worldZ) const;
+
   TerrainType getTerrainType(int gridX, int gridZ) const;
 
   const TerrainHeightMap *getHeightMap() const { return m_heightMap.get(); }

+ 13 - 0
game/systems/camera_controller.cpp

@@ -17,6 +17,12 @@ void CameraController::elevate(Render::GL::Camera &camera, float dy) const {
     camera.captureFollowOffset();
 }
 
+void CameraController::moveUp(Render::GL::Camera &camera, float dy) const {
+  camera.moveUp(dy);
+  if (camera.isFollowEnabled())
+    camera.captureFollowOffset();
+}
+
 void CameraController::yaw(Render::GL::Camera &camera, float degrees) const {
   camera.yaw(degrees);
   if (camera.isFollowEnabled())
@@ -30,6 +36,13 @@ void CameraController::orbit(Render::GL::Camera &camera, float yawDeg,
     camera.captureFollowOffset();
 }
 
+void CameraController::zoomDistance(Render::GL::Camera &camera,
+                                    float delta) const {
+  camera.zoomDistance(delta);
+  if (camera.isFollowEnabled())
+    camera.captureFollowOffset();
+}
+
 void CameraController::setFollowEnabled(Render::GL::Camera &camera,
                                         bool enable) const {
   camera.setFollowEnabled(enable);

+ 2 - 0
game/systems/camera_controller.h

@@ -13,8 +13,10 @@ class CameraController {
 public:
   void move(Render::GL::Camera &camera, float dx, float dz) const;
   void elevate(Render::GL::Camera &camera, float dy) const;
+  void moveUp(Render::GL::Camera &camera, float dy) const;
   void yaw(Render::GL::Camera &camera, float degrees) const;
   void orbit(Render::GL::Camera &camera, float yawDeg, float pitchDeg) const;
+  void zoomDistance(Render::GL::Camera &camera, float delta) const;
   void setFollowEnabled(Render::GL::Camera &camera, bool enable) const;
   void setFollowLerp(Render::GL::Camera &camera, float alpha) const;
 };

+ 7 - 2
game/systems/combat_system.cpp

@@ -36,7 +36,6 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
 
     if (attackerUnit->health <= 0)
       continue;
-
     float range = 2.0f;
     int damage = 10;
     float cooldown = 1.0f;
@@ -133,6 +132,12 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
                   movement->vx = 0.0f;
                   movement->vz = 0.0f;
                   movement->path.clear();
+                  if (attackerTransformComponent) {
+                    movement->targetX = attackerTransformComponent->position.x;
+                    movement->targetY = attackerTransformComponent->position.z;
+                    movement->goalX = attackerTransformComponent->position.x;
+                    movement->goalY = attackerTransformComponent->position.z;
+                  }
                 } else {
                   QVector3D plannedTarget(movement->targetX, 0.0f,
                                           movement->targetY);
@@ -152,7 +157,7 @@ void CombatSystem::processAttacks(Engine::Core::World *world, float deltaTime) {
                     CommandService::MoveOptions options;
                     options.clearAttackIntent = false;
 
-                    options.allowDirectFallback = false;
+                    options.allowDirectFallback = true;
                     std::vector<Engine::Core::EntityID> unitIds = {
                         attacker->getId()};
                     std::vector<QVector3D> moveTargets = {desiredPos};

+ 169 - 20
game/systems/command_service.cpp

@@ -1,15 +1,18 @@
 #include "command_service.h"
 #include "../core/component.h"
 #include "../core/world.h"
-#include "building_collision_registry.h"
 #include "pathfinding.h"
 #include <QVector3D>
+#include <algorithm>
 #include <cmath>
-#include <cstdlib>
 
 namespace Game {
 namespace Systems {
 
+namespace {
+constexpr float SAME_TARGET_THRESHOLD_SQ = 0.01f;
+}
+
 std::unique_ptr<Pathfinding> CommandService::s_pathfinder = nullptr;
 std::unordered_map<std::uint64_t, CommandService::PendingPathRequest>
     CommandService::s_pendingRequests;
@@ -27,17 +30,14 @@ void CommandService::initialize(int worldWidth, int worldHeight) {
   }
   s_nextRequestId.store(1, std::memory_order_release);
 
-  float offsetX = -worldWidth / 2.0f;
-  float offsetZ = -worldHeight / 2.0f;
+  float offsetX = -(worldWidth * 0.5f - 0.5f);
+  float offsetZ = -(worldHeight * 0.5f - 0.5f);
   s_pathfinder->setGridOffset(offsetX, offsetZ);
 }
 
 Pathfinding *CommandService::getPathfinder() { return s_pathfinder.get(); }
-
 Point CommandService::worldToGrid(float worldX, float worldZ) {
-
   if (s_pathfinder) {
-
     int gridX =
         static_cast<int>(std::round(worldX - s_pathfinder->getGridOffsetX()));
     int gridZ =
@@ -100,10 +100,70 @@ void CommandService::moveUnits(Engine::Core::World &world,
       e->removeComponent<Engine::Core::AttackTargetComponent>();
     }
 
+    const float targetX = targets[i].x();
+    const float targetZ = targets[i].z();
+
+    bool matchedPending = false;
+    if (mv->pathPending) {
+      std::lock_guard<std::mutex> lock(s_pendingMutex);
+      auto requestIt = s_entityToRequest.find(units[i]);
+      if (requestIt != s_entityToRequest.end()) {
+        auto pendingIt = s_pendingRequests.find(requestIt->second);
+        if (pendingIt != s_pendingRequests.end()) {
+          float pdx = pendingIt->second.target.x() - targetX;
+          float pdz = pendingIt->second.target.z() - targetZ;
+          if (pdx * pdx + pdz * pdz <= SAME_TARGET_THRESHOLD_SQ) {
+            pendingIt->second.options = options;
+            matchedPending = true;
+          }
+        } else {
+          s_entityToRequest.erase(requestIt);
+        }
+      }
+    }
+
+    mv->goalX = targetX;
+    mv->goalY = targetZ;
+
+    if (matchedPending) {
+      continue;
+    }
+
+    if (!mv->pathPending) {
+      bool currentTargetMatches = mv->hasTarget && mv->path.empty();
+      if (currentTargetMatches) {
+        float dx = mv->targetX - targetX;
+        float dz = mv->targetY - targetZ;
+        if (dx * dx + dz * dz <= SAME_TARGET_THRESHOLD_SQ) {
+          continue;
+        }
+      }
+
+      if (!mv->path.empty()) {
+        const auto &lastWaypoint = mv->path.back();
+        float dx = lastWaypoint.first - targetX;
+        float dz = lastWaypoint.second - targetZ;
+        if (dx * dx + dz * dz <= SAME_TARGET_THRESHOLD_SQ) {
+          continue;
+        }
+      }
+    }
+
     if (s_pathfinder) {
       Point start = worldToGrid(transform->position.x, transform->position.z);
       Point end = worldToGrid(targets[i].x(), targets[i].z());
 
+      if (start == end) {
+        mv->targetX = targetX;
+        mv->targetY = targetZ;
+        mv->hasTarget = true;
+        mv->path.clear();
+        mv->pathPending = false;
+        mv->pendingRequestId = 0;
+        clearPendingRequest(units[i]);
+        continue;
+      }
+
       int dx = std::abs(end.x - start.x);
       int dz = std::abs(end.y - start.y);
       bool useDirectPath = (dx + dz) <= CommandService::DIRECT_PATH_THRESHOLD;
@@ -113,8 +173,8 @@ void CommandService::moveUnits(Engine::Core::World &world,
 
       if (useDirectPath) {
 
-        mv->targetX = targets[i].x();
-        mv->targetY = targets[i].z();
+        mv->targetX = targetX;
+        mv->targetY = targetZ;
         mv->hasTarget = true;
         mv->path.clear();
         mv->pathPending = false;
@@ -122,6 +182,32 @@ void CommandService::moveUnits(Engine::Core::World &world,
         clearPendingRequest(units[i]);
       } else {
 
+        bool skipNewRequest = false;
+        {
+          std::lock_guard<std::mutex> lock(s_pendingMutex);
+          auto existingIt = s_entityToRequest.find(units[i]);
+          if (existingIt != s_entityToRequest.end()) {
+            auto pendingIt = s_pendingRequests.find(existingIt->second);
+            if (pendingIt != s_pendingRequests.end()) {
+              float dx = pendingIt->second.target.x() - targetX;
+              float dz = pendingIt->second.target.z() - targetZ;
+              if (dx * dx + dz * dz <= SAME_TARGET_THRESHOLD_SQ) {
+                pendingIt->second.options = options;
+                skipNewRequest = true;
+              } else {
+                s_pendingRequests.erase(pendingIt);
+                s_entityToRequest.erase(existingIt);
+              }
+            } else {
+              s_entityToRequest.erase(existingIt);
+            }
+          }
+        }
+
+        if (skipNewRequest) {
+          continue;
+        }
+
         mv->path.clear();
         mv->hasTarget = false;
         mv->pathPending = true;
@@ -132,11 +218,6 @@ void CommandService::moveUnits(Engine::Core::World &world,
 
         {
           std::lock_guard<std::mutex> lock(s_pendingMutex);
-          auto it = s_entityToRequest.find(units[i]);
-          if (it != s_entityToRequest.end()) {
-            s_pendingRequests.erase(it->second);
-            s_entityToRequest.erase(it);
-          }
           s_pendingRequests[requestId] = {units[i], targets[i], options};
           s_entityToRequest[units[i]] = requestId;
         }
@@ -145,8 +226,8 @@ void CommandService::moveUnits(Engine::Core::World &world,
       }
     } else {
 
-      mv->targetX = targets[i].x();
-      mv->targetY = targets[i].z();
+      mv->targetX = targetX;
+      mv->targetY = targetZ;
       mv->hasTarget = true;
       mv->path.clear();
       mv->pathPending = false;
@@ -185,20 +266,23 @@ void CommandService::processPathResults(Engine::Core::World &world) {
       }
     }
 
-    if (!found)
+    if (!found) {
       continue;
+    }
 
     auto *entity = world.getEntity(requestInfo.entityId);
     if (!entity)
       continue;
 
     auto *movement = entity->getComponent<Engine::Core::MovementComponent>();
-    if (!movement)
+    if (!movement) {
       continue;
+    }
 
     auto *transform = entity->getComponent<Engine::Core::TransformComponent>();
-    if (!transform)
+    if (!transform) {
       continue;
+    }
 
     if (!movement->pathPending ||
         movement->pendingRequestId != result.requestId) {
@@ -208,6 +292,8 @@ void CommandService::processPathResults(Engine::Core::World &world) {
     movement->pathPending = false;
     movement->pendingRequestId = 0;
     movement->path.clear();
+    movement->goalX = requestInfo.target.x();
+    movement->goalY = requestInfo.target.z();
 
     const auto &pathPoints = result.path;
 
@@ -224,6 +310,7 @@ void CommandService::processPathResults(Engine::Core::World &world) {
       continue;
     }
 
+    movement->path.reserve(pathPoints.size() > 1 ? pathPoints.size() - 1 : 0);
     for (size_t idx = 1; idx < pathPoints.size(); ++idx) {
       const auto &point = pathPoints[idx];
       QVector3D worldPos = gridToWorld(point);
@@ -265,7 +352,6 @@ void CommandService::attackTarget(
     Engine::Core::EntityID targetId, bool shouldChase) {
   if (targetId == 0)
     return;
-
   for (auto unitId : units) {
     auto *e = world.getEntity(unitId);
     if (!e)
@@ -280,6 +366,69 @@ void CommandService::attackTarget(
 
     attackTarget->targetId = targetId;
     attackTarget->shouldChase = shouldChase;
+
+    if (!shouldChase)
+      continue;
+
+    auto *targetEnt = world.getEntity(targetId);
+    if (!targetEnt)
+      continue;
+
+    auto *tTrans = targetEnt->getComponent<Engine::Core::TransformComponent>();
+    auto *attTrans = e->getComponent<Engine::Core::TransformComponent>();
+    if (!tTrans || !attTrans)
+      continue;
+
+    QVector3D targetPos(tTrans->position.x, 0.0f, tTrans->position.z);
+    QVector3D attackerPos(attTrans->position.x, 0.0f, attTrans->position.z);
+
+    QVector3D desiredPos = targetPos;
+
+    float range = 2.0f;
+    if (auto *atk = e->getComponent<Engine::Core::AttackComponent>()) {
+      range = std::max(0.1f, atk->range);
+    }
+
+    QVector3D direction = targetPos - attackerPos;
+    float distance = direction.length();
+    if (distance > 0.001f) {
+      direction /= distance;
+      if (targetEnt->hasComponent<Engine::Core::BuildingComponent>()) {
+        float scaleX = tTrans->scale.x;
+        float scaleZ = tTrans->scale.z;
+        float targetRadius = std::max(scaleX, scaleZ) * 0.5f;
+        float desiredDistance = targetRadius + std::max(range - 0.2f, 0.2f);
+        if (distance > desiredDistance + 0.15f) {
+          desiredPos = targetPos - direction * desiredDistance;
+        }
+      } else {
+        float desiredDistance = std::max(range - 0.2f, 0.2f);
+        if (distance > desiredDistance + 0.15f) {
+          desiredPos = targetPos - direction * desiredDistance;
+        }
+      }
+    }
+
+    CommandService::MoveOptions opts;
+    opts.clearAttackIntent = false;
+    opts.allowDirectFallback = true;
+    std::vector<Engine::Core::EntityID> unitIds = {unitId};
+    std::vector<QVector3D> moveTargets = {desiredPos};
+    CommandService::moveUnits(world, unitIds, moveTargets, opts);
+
+    auto *mv = e->getComponent<Engine::Core::MovementComponent>();
+    if (!mv) {
+      mv = e->addComponent<Engine::Core::MovementComponent>();
+    }
+    if (mv) {
+
+      mv->targetX = desiredPos.x();
+      mv->targetY = desiredPos.z();
+      mv->goalX = desiredPos.x();
+      mv->goalY = desiredPos.z();
+      mv->hasTarget = true;
+      mv->path.clear();
+    }
   }
 }
 

+ 124 - 2
game/systems/movement_system.cpp

@@ -1,22 +1,82 @@
 #include "movement_system.h"
+#include "../map/terrain_service.h"
+#include "building_collision_registry.h"
 #include "command_service.h"
+#include "pathfinding.h"
+#include <QVector3D>
 #include <algorithm>
 #include <cmath>
+#include <vector>
 
 namespace Game::Systems {
 
 static constexpr int MAX_WAYPOINT_SKIP_COUNT = 4;
+static constexpr float REPATH_COOLDOWN_SECONDS = 0.4f;
+
+namespace {
+
+bool isSegmentWalkable(const QVector3D &from, const QVector3D &to,
+                       Engine::Core::EntityID ignoreEntity) {
+  auto &registry = BuildingCollisionRegistry::instance();
+  auto &terrainService = Game::Map::TerrainService::instance();
+  Pathfinding *pathfinder = CommandService::getPathfinder();
+
+  auto samplePoint = [&](const QVector3D &pos) {
+    if (registry.isPointInBuilding(pos.x(), pos.z(), ignoreEntity)) {
+      return false;
+    }
+
+    if (pathfinder) {
+      int gridX =
+          static_cast<int>(std::round(pos.x() - pathfinder->getGridOffsetX()));
+      int gridZ =
+          static_cast<int>(std::round(pos.z() - pathfinder->getGridOffsetZ()));
+      if (!pathfinder->isWalkable(gridX, gridZ)) {
+        return false;
+      }
+    } else if (terrainService.isInitialized()) {
+      int gridX = static_cast<int>(std::round(pos.x()));
+      int gridZ = static_cast<int>(std::round(pos.z()));
+      if (!terrainService.isWalkable(gridX, gridZ)) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  QVector3D delta = to - from;
+  float distance = delta.length();
+
+  if (distance < 0.001f) {
+    return samplePoint(from);
+  }
+
+  int steps = std::max(1, static_cast<int>(std::ceil(distance)) * 2);
+  QVector3D step = delta / static_cast<float>(steps);
+  QVector3D pos = from;
+  for (int i = 0; i <= steps; ++i, pos += step) {
+    if (!samplePoint(pos)) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+} // namespace
 
 void MovementSystem::update(Engine::Core::World *world, float deltaTime) {
   CommandService::processPathResults(*world);
   auto entities = world->getEntitiesWith<Engine::Core::MovementComponent>();
 
   for (auto entity : entities) {
-    moveUnit(entity, deltaTime);
+    moveUnit(entity, world, deltaTime);
   }
 }
 
-void MovementSystem::moveUnit(Engine::Core::Entity *entity, float deltaTime) {
+void MovementSystem::moveUnit(Engine::Core::Entity *entity,
+                              Engine::Core::World *world, float deltaTime) {
   auto transform = entity->getComponent<Engine::Core::TransformComponent>();
   auto movement = entity->getComponent<Engine::Core::MovementComponent>();
   auto unit = entity->getComponent<Engine::Core::UnitComponent>();
@@ -25,6 +85,11 @@ void MovementSystem::moveUnit(Engine::Core::Entity *entity, float deltaTime) {
     return;
   }
 
+  if (movement->repathCooldown > 0.0f) {
+    movement->repathCooldown =
+        std::max(0.0f, movement->repathCooldown - deltaTime);
+  }
+
   const float maxSpeed = std::max(0.1f, unit->speed);
   const float accel = maxSpeed * 4.0f;
   const float damping = 6.0f;
@@ -33,6 +98,43 @@ void MovementSystem::moveUnit(Engine::Core::Entity *entity, float deltaTime) {
     movement->vx *= std::max(0.0f, 1.0f - damping * deltaTime);
     movement->vz *= std::max(0.0f, 1.0f - damping * deltaTime);
   } else {
+    QVector3D currentPos(transform->position.x, 0.0f, transform->position.z);
+    QVector3D segmentTarget(movement->targetX, 0.0f, movement->targetY);
+    if (!movement->path.empty()) {
+      segmentTarget = QVector3D(movement->path.front().first, 0.0f,
+                                movement->path.front().second);
+    }
+    QVector3D finalGoal(movement->goalX, 0.0f, movement->goalY);
+
+    if (!isSegmentWalkable(currentPos, segmentTarget, entity->getId())) {
+      bool issuedPathRequest = false;
+      if (!movement->pathPending && movement->repathCooldown <= 0.0f) {
+        float goalDistSq = (finalGoal - currentPos).lengthSquared();
+        if (goalDistSq > 0.01f) {
+          CommandService::MoveOptions opts;
+          opts.clearAttackIntent = false;
+          opts.allowDirectFallback = false;
+          std::vector<Engine::Core::EntityID> ids = {entity->getId()};
+          std::vector<QVector3D> targets = {
+              QVector3D(movement->goalX, 0.0f, movement->goalY)};
+          CommandService::moveUnits(*world, ids, targets, opts);
+          movement->repathCooldown = REPATH_COOLDOWN_SECONDS;
+          issuedPathRequest = true;
+        }
+      }
+
+      if (!issuedPathRequest) {
+        movement->pathPending = false;
+        movement->pendingRequestId = 0;
+      }
+
+      movement->path.clear();
+      movement->hasTarget = false;
+      movement->vx = 0.0f;
+      movement->vz = 0.0f;
+      return;
+    }
+
     float arriveRadius = std::clamp(maxSpeed * deltaTime * 2.0f, 0.05f, 0.25f);
     float arriveRadiusSq = arriveRadius * arriveRadius;
 
@@ -90,6 +192,26 @@ void MovementSystem::moveUnit(Engine::Core::Entity *entity, float deltaTime) {
   transform->position.x += movement->vx * deltaTime;
   transform->position.z += movement->vz * deltaTime;
 
+  auto &terrain = Game::Map::TerrainService::instance();
+  if (terrain.isInitialized()) {
+    const Game::Map::TerrainHeightMap *hm = terrain.getHeightMap();
+    if (hm) {
+      const float tile = hm->getTileSize();
+      const int w = hm->getWidth();
+      const int h = hm->getHeight();
+      if (w > 0 && h > 0) {
+        const float halfW = w * 0.5f - 0.5f;
+        const float halfH = h * 0.5f - 0.5f;
+        const float minX = -halfW * tile;
+        const float maxX = halfW * tile;
+        const float minZ = -halfH * tile;
+        const float maxZ = halfH * tile;
+        transform->position.x = std::clamp(transform->position.x, minX, maxX);
+        transform->position.z = std::clamp(transform->position.z, minZ, maxZ);
+      }
+    }
+  }
+
   if (!entity->hasComponent<Engine::Core::BuildingComponent>()) {
     float speed2 = movement->vx * movement->vx + movement->vz * movement->vz;
     if (speed2 > 1e-5f) {

+ 2 - 1
game/systems/movement_system.h

@@ -17,7 +17,8 @@ public:
   void update(Engine::Core::World *world, float deltaTime) override;
 
 private:
-  void moveUnit(Engine::Core::Entity *entity, float deltaTime);
+  void moveUnit(Engine::Core::Entity *entity, Engine::Core::World *world,
+                float deltaTime);
   bool hasReachedTarget(const Engine::Core::TransformComponent *transform,
                         const Engine::Core::MovementComponent *movement);
 };

+ 21 - 6
game/systems/pathfinding.cpp

@@ -64,12 +64,21 @@ void Pathfinding::updateBuildingObstacles() {
 
   auto &terrainService = Game::Map::TerrainService::instance();
   if (terrainService.isInitialized()) {
+    const Game::Map::TerrainHeightMap *heightMap =
+        terrainService.getHeightMap();
+    const int terrainWidth = heightMap ? heightMap->getWidth() : 0;
+    const int terrainHeight = heightMap ? heightMap->getHeight() : 0;
+
     for (int z = 0; z < m_height; ++z) {
       for (int x = 0; x < m_width; ++x) {
-        int terrainX = x + static_cast<int>(m_gridOffsetX);
-        int terrainZ = z + static_cast<int>(m_gridOffsetZ);
+        bool blocked = false;
+        if (x < terrainWidth && z < terrainHeight) {
+          blocked = !terrainService.isWalkable(x, z);
+        } else {
+          blocked = true;
+        }
 
-        if (!terrainService.isWalkable(terrainX, terrainZ)) {
+        if (blocked) {
           m_obstacles[z][x] = true;
         }
       }
@@ -82,9 +91,8 @@ void Pathfinding::updateBuildingObstacles() {
   for (const auto &building : buildings) {
     auto cells = registry.getOccupiedGridCells(building, m_gridCellSize);
     for (const auto &cell : cells) {
-
-      int gridX = cell.first - static_cast<int>(m_gridOffsetX);
-      int gridZ = cell.second - static_cast<int>(m_gridOffsetZ);
+      int gridX = static_cast<int>(std::round(cell.first - m_gridOffsetX));
+      int gridZ = static_cast<int>(std::round(cell.second - m_gridOffsetZ));
 
       if (gridX >= 0 && gridX < m_width && gridZ >= 0 && gridZ < m_height) {
         m_obstacles[gridZ][gridX] = true;
@@ -233,6 +241,13 @@ std::vector<Point> Pathfinding::getNeighbors(const Point &point) const {
       int y = point.y + dy;
 
       if (x >= 0 && x < m_width && y >= 0 && y < m_height) {
+        if (dx != 0 && dy != 0) {
+
+          if (!isWalkable(point.x + dx, point.y) ||
+              !isWalkable(point.x, point.y + dy)) {
+            continue;
+          }
+        }
         neighbors.emplace_back(x, y);
       }
     }

+ 2 - 0
game/systems/patrol_system.cpp

@@ -92,6 +92,8 @@ void PatrolSystem::update(Engine::Core::World *world, float deltaTime) {
     movement->hasTarget = true;
     movement->targetX = targetX;
     movement->targetY = targetZ;
+    movement->goalX = targetX;
+    movement->goalY = targetZ;
   }
 }
 

+ 5 - 0
game/units/unit.cpp

@@ -39,6 +39,11 @@ void Unit::moveTo(float x, float z) {
     m_mv->targetX = x;
     m_mv->targetY = z;
     m_mv->hasTarget = true;
+    m_mv->goalX = x;
+    m_mv->goalY = z;
+    m_mv->path.clear();
+    m_mv->pathPending = false;
+    m_mv->pendingRequestId = 0;
   }
 }
 

+ 5 - 0
main.cpp

@@ -1,5 +1,7 @@
+#include <QDateTime>
 #include <QDebug>
 #include <QDir>
+#include <QFile>
 #include <QGuiApplication>
 #include <QOpenGLContext>
 #include <QQmlApplicationEngine>
@@ -7,6 +9,7 @@
 #include <QQuickWindow>
 #include <QSGRendererInterface>
 #include <QSurfaceFormat>
+#include <QTextStream>
 
 #include "app/game_engine.h"
 #include "ui/gl_view.h"
@@ -20,6 +23,8 @@ int main(int argc, char *argv[]) {
   qputenv("QSG_RHI_BACKEND", "opengl");
   QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
 
+  (void)0;
+
   QSurfaceFormat fmt;
   fmt.setVersion(3, 3);
   fmt.setProfile(QSurfaceFormat::CoreProfile);

+ 1 - 20
render/entity/archer_renderer.cpp

@@ -4,6 +4,7 @@
 #include "../../game/core/world.h"
 #include "../../game/visuals/team_colors.h"
 #include "../geom/math_utils.h"
+#include "../geom/selection_disc.h"
 #include "../geom/selection_ring.h"
 #include "../geom/transforms.h"
 #include "../gl/mesh.h"
@@ -416,23 +417,6 @@ static inline void drawSelectionFX(const DrawContext &p, ISubmitter &out) {
   }
 }
 
-static inline void drawGroundShadow(const DrawContext &p, ISubmitter &out,
-                                    float formationWidth,
-                                    float formationDepth) {
-  if (!p.resources)
-    return;
-  Mesh *quad = p.resources->quad();
-  Texture *white = p.resources->white();
-  if (!quad || !white)
-    return;
-
-  QMatrix4x4 decal = p.model;
-  decal.translate(0.0f, 0.02f, 0.0f);
-  decal.rotate(-90.0f, 1.0f, 0.0f, 0.0f);
-  decal.scale(formationWidth * 0.55f, formationDepth * 0.45f, 1.0f);
-  out.mesh(quad, decal, QVector3D(0.07f, 0.07f, 0.066f), white, 0.52f);
-}
-
 void registerArcherRenderer(Render::GL::EntityRendererRegistry &registry) {
   registry.registerRenderer("archer", [](const DrawContext &p,
                                          ISubmitter &out) {
@@ -461,9 +445,6 @@ void registerArcherRenderer(Render::GL::EntityRendererRegistry &registry) {
 
     ArcherColors colors = makeColors(tunic);
 
-    drawGroundShadow(p, out, std::max(1, cols - 1) * spacing + 0.8f,
-                     std::max(1, rows - 1) * spacing + 0.9f);
-
     bool isMoving = false;
     bool isAttacking = false;
     float targetRotationY = 0.0f;

+ 102 - 10
render/gl/camera.cpp

@@ -1,4 +1,5 @@
 #include "camera.h"
+#include "../../game/map/visibility_service.h"
 #include <QtMath>
 #include <cmath>
 
@@ -77,6 +78,25 @@ void Camera::zoom(float delta) {
   }
 }
 
+void Camera::zoomDistance(float delta) {
+
+  QVector3D offset = m_position - m_target;
+  float r = offset.length();
+  if (r < 1e-4f)
+    r = 1e-4f;
+
+  float factor = 1.0f - delta * 0.15f;
+  factor = std::clamp(factor, 0.1f, 10.0f);
+  float newR = r * factor;
+
+  newR = std::clamp(newR, 1.0f, 500.0f);
+  QVector3D dir = offset.normalized();
+  m_position = m_target + dir * newR;
+  clampAboveGround();
+  m_front = (m_target - m_position).normalized();
+  updateVectors();
+}
+
 void Camera::rotate(float yaw, float pitch) { orbit(yaw, pitch); }
 
 void Camera::pan(float rightDist, float forwardDist) {
@@ -108,15 +128,42 @@ void Camera::yaw(float degrees) {
 void Camera::orbit(float yawDeg, float pitchDeg) {
 
   QVector3D offset = m_position - m_target;
-  if (offset.lengthSquared() < 1e-6f) {
-
-    offset = QVector3D(0, 0, 1);
-  }
   float curYaw = 0.f, curPitch = 0.f;
   computeYawPitchFromOffset(offset, curYaw, curPitch);
-  float newYaw = curYaw + yawDeg;
-  float newPitch = qBound(m_pitchMinDeg, curPitch + pitchDeg, m_pitchMaxDeg);
+
+  m_orbitStartYaw = curYaw;
+  m_orbitStartPitch = curPitch;
+  m_orbitTargetYaw = curYaw + yawDeg;
+  m_orbitTargetPitch =
+      qBound(m_pitchMinDeg, curPitch + pitchDeg, m_pitchMaxDeg);
+  m_orbitTime = 0.0f;
+  m_orbitPending = true;
+  qDebug() << "Camera::orbit start:" << "startYaw=" << m_orbitStartYaw
+           << "startPitch=" << m_orbitStartPitch
+           << "targetYaw=" << m_orbitTargetYaw
+           << "targetPitch=" << m_orbitTargetPitch;
+}
+
+void Camera::update(float dt) {
+  if (!m_orbitPending)
+    return;
+
+  m_orbitTime += dt;
+  float t = m_orbitDuration <= 0.0f
+                ? 1.0f
+                : std::clamp(m_orbitTime / m_orbitDuration, 0.0f, 1.0f);
+
+  float s = t * t * (3.0f - 2.0f * t);
+
+  float newYaw = m_orbitStartYaw + (m_orbitTargetYaw - m_orbitStartYaw) * s;
+  float newPitch =
+      m_orbitStartPitch + (m_orbitTargetPitch - m_orbitStartPitch) * s;
+
+  QVector3D offset = m_position - m_target;
   float r = offset.length();
+  if (r < 1e-4f)
+    r = 1e-4f;
+
   float yawRad = qDegreesToRadians(newYaw);
   float pitchRad = qDegreesToRadians(newPitch);
   QVector3D newDir(std::sin(yawRad) * std::cos(pitchRad), std::sin(pitchRad),
@@ -125,6 +172,11 @@ void Camera::orbit(float yawDeg, float pitchDeg) {
   clampAboveGround();
   m_front = (m_target - m_position).normalized();
   updateVectors();
+
+  if (t >= 1.0f) {
+    m_orbitPending = false;
+    qDebug() << "Camera::update finished";
+  }
 }
 
 bool Camera::screenToGround(float sx, float sy, float screenW, float screenH,
@@ -193,12 +245,20 @@ void Camera::updateFollow(const QVector3D &targetCenter) {
   updateVectors();
 }
 
-void Camera::setRTSView(const QVector3D &center, float distance, float angle) {
+void Camera::setRTSView(const QVector3D &center, float distance, float angle,
+                        float yawDeg) {
   m_target = center;
 
-  float radians = qDegreesToRadians(angle);
-  m_position =
-      center + QVector3D(0, distance * qSin(radians), distance * qCos(radians));
+  float pitchRad = qDegreesToRadians(angle);
+  float yawRad = qDegreesToRadians(yawDeg);
+
+  float y = distance * qSin(pitchRad);
+  float horiz = distance * qCos(pitchRad);
+
+  float x = std::sin(yawRad) * horiz;
+  float z = std::cos(yawRad) * horiz;
+
+  m_position = center + QVector3D(x, y, z);
 
   m_up = QVector3D(0, 1, 0);
   m_front = (m_target - m_position).normalized();
@@ -236,6 +296,21 @@ QMatrix4x4 Camera::getViewProjectionMatrix() const {
   return getProjectionMatrix() * getViewMatrix();
 }
 
+float Camera::getDistance() const { return (m_position - m_target).length(); }
+
+float Camera::getPitchDeg() const {
+  QVector3D off = m_position - m_target;
+  float yaw = 0.0f, pitch = 0.0f;
+
+  QVector3D dir = -off;
+  if (dir.lengthSquared() < 1e-6f) {
+    return 0.0f;
+  }
+  float lenXZ = std::sqrt(dir.x() * dir.x() + dir.z() * dir.z());
+  float pitchRad = std::atan2(dir.y(), lenXZ);
+  return qRadiansToDegrees(pitchRad);
+}
+
 void Camera::updateVectors() {
 
   QVector3D worldUp(0, 1, 0);
@@ -251,6 +326,23 @@ void Camera::clampAboveGround() {
   if (m_position.y() < m_groundY + m_minHeight) {
     m_position.setY(m_groundY + m_minHeight);
   }
+
+  auto &vis = Game::Map::VisibilityService::instance();
+  if (vis.isInitialized()) {
+    const float tile = vis.getTileSize();
+    const float halfW = vis.getWidth() * 0.5f - 0.5f;
+    const float halfH = vis.getHeight() * 0.5f - 0.5f;
+    const float minX = -halfW * tile;
+    const float maxX = halfW * tile;
+    const float minZ = -halfH * tile;
+    const float maxZ = halfH * tile;
+
+    m_position.setX(std::clamp(m_position.x(), minX, maxX));
+    m_position.setZ(std::clamp(m_position.z(), minZ, maxZ));
+
+    m_target.setX(std::clamp(m_target.x(), minX, maxX));
+    m_target.setZ(std::clamp(m_target.z(), minZ, maxZ));
+  }
 }
 
 void Camera::computeYawPitchFromOffset(const QVector3D &off, float &yawDeg,

+ 15 - 1
render/gl/camera.h

@@ -24,6 +24,8 @@ public:
   void moveRight(float distance);
   void moveUp(float distance);
   void zoom(float delta);
+
+  void zoomDistance(float delta);
   void rotate(float yaw, float pitch);
 
   void pan(float rightDist, float forwardDist);
@@ -31,6 +33,8 @@ public:
   void elevate(float dy);
   void yaw(float degrees);
   void orbit(float yawDeg, float pitchDeg);
+
+  void update(float dt);
   bool screenToGround(float sx, float sy, float screenW, float screenH,
                       QVector3D &outWorld) const;
   bool worldToScreen(const QVector3D &world, int screenW, int screenH,
@@ -44,7 +48,7 @@ public:
   void updateFollow(const QVector3D &targetCenter);
 
   void setRTSView(const QVector3D &center, float distance = 10.0f,
-                  float angle = 45.0f);
+                  float angle = 45.0f, float yawDeg = 45.0f);
   void setTopDownView(const QVector3D &center, float distance = 10.0f);
 
   QMatrix4x4 getViewMatrix() const;
@@ -53,6 +57,8 @@ public:
 
   const QVector3D &getPosition() const { return m_position; }
   const QVector3D &getTarget() const { return m_target; }
+  float getDistance() const;
+  float getPitchDeg() const;
   float getFOV() const { return m_fov; }
   float getAspect() const { return m_aspect; }
   float getNear() const { return m_nearPlane; }
@@ -87,6 +93,14 @@ private:
   float m_pitchMinDeg = -85.0f;
   float m_pitchMaxDeg = -5.0f;
 
+  bool m_orbitPending = false;
+  float m_orbitStartYaw = 0.0f;
+  float m_orbitStartPitch = 0.0f;
+  float m_orbitTargetYaw = 0.0f;
+  float m_orbitTargetPitch = 0.0f;
+  float m_orbitTime = 0.0f;
+  float m_orbitDuration = 0.12f;
+
   void updateVectors();
   void clampAboveGround();
   void computeYawPitchFromOffset(const QVector3D &off, float &yawDeg,

+ 3 - 2
render/ground/ground_renderer.cpp

@@ -11,8 +11,9 @@ void GroundRenderer::recomputeModel() {
 
   m_model.translate(0.0f, -0.02f, 0.0f);
   if (m_width > 0 && m_height > 0) {
-    float scaleX = float(m_width) * m_tileSize * 0.5f;
-    float scaleZ = float(m_height) * m_tileSize * 0.5f;
+
+    float scaleX = float(m_width) * m_tileSize;
+    float scaleZ = float(m_height) * m_tileSize;
     m_model.scale(scaleX, 1.0f, scaleZ);
   } else {
     m_model.scale(m_extent, 1.0f, m_extent);

+ 343 - 72
render/ground/terrain_renderer.cpp

@@ -5,6 +5,7 @@
 #include "../scene_renderer.h"
 #include <QDebug>
 #include <QElapsedTimer>
+#include <QQuaternion>
 #include <algorithm>
 #include <cmath>
 #include <unordered_map>
@@ -35,6 +36,35 @@ inline QVector3D applyTint(const QVector3D &color, float tint) {
                    std::clamp(c.z(), 0.0f, 1.0f));
 }
 
+inline QVector3D clamp01(const QVector3D &c) {
+  return QVector3D(std::clamp(c.x(), 0.0f, 1.0f), std::clamp(c.y(), 0.0f, 1.0f),
+                   std::clamp(c.z(), 0.0f, 1.0f));
+}
+
+inline float hashTo01(uint32_t h) {
+  h ^= h >> 17;
+  h *= 0xed5ad4bbu;
+  h ^= h >> 11;
+  h *= 0xac4c1b51u;
+  h ^= h >> 15;
+  h *= 0x31848babu;
+  h ^= h >> 14;
+  return (h & 0x00FFFFFFu) / float(0x01000000);
+}
+
+inline float valueNoise(float x, float z, uint32_t salt = 0u) {
+  int x0 = int(std::floor(x)), z0 = int(std::floor(z));
+  int x1 = x0 + 1, z1 = z0 + 1;
+  float tx = x - float(x0), tz = z - float(z0);
+  float n00 = hashTo01(hashCoords(x0, z0, salt));
+  float n10 = hashTo01(hashCoords(x1, z0, salt));
+  float n01 = hashTo01(hashCoords(x0, z1, salt));
+  float n11 = hashTo01(hashCoords(x1, z1, salt));
+  float nx0 = n00 * (1 - tx) + n10 * tx;
+  float nx1 = n01 * (1 - tx) + n11 * tx;
+  return nx0 * (1 - tz) + nx1 * tz;
+}
+
 } // namespace
 
 namespace Render::GL {
@@ -72,6 +102,41 @@ void TerrainRenderer::submit(Renderer &renderer, ResourceManager &resources) {
   const float halfWidth = m_width * 0.5f - 0.5f;
   const float halfHeight = m_height * 0.5f - 0.5f;
 
+  auto sampleHeightAt = [&](float gx, float gz) {
+    gx = std::clamp(gx, 0.0f, float(m_width - 1));
+    gz = std::clamp(gz, 0.0f, float(m_height - 1));
+    int x0 = int(std::floor(gx));
+    int z0 = int(std::floor(gz));
+    int x1 = std::min(x0 + 1, m_width - 1);
+    int z1 = std::min(z0 + 1, m_height - 1);
+    float tx = gx - float(x0);
+    float tz = gz - float(z0);
+    float h00 = m_heightData[z0 * m_width + x0];
+    float h10 = m_heightData[z0 * m_width + x1];
+    float h01 = m_heightData[z1 * m_width + x0];
+    float h11 = m_heightData[z1 * m_width + x1];
+    float h0 = h00 * (1.0f - tx) + h10 * tx;
+    float h1 = h01 * (1.0f - tx) + h11 * tx;
+    return h0 * (1.0f - tz) + h1 * tz;
+  };
+
+  auto normalFromHeights = [&](float gx, float gz) {
+    float gx0 = std::clamp(gx - 1.0f, 0.0f, float(m_width - 1));
+    float gx1 = std::clamp(gx + 1.0f, 0.0f, float(m_width - 1));
+    float gz0 = std::clamp(gz - 1.0f, 0.0f, float(m_height - 1));
+    float gz1 = std::clamp(gz + 1.0f, 0.0f, float(m_height - 1));
+    float hL = sampleHeightAt(gx0, gz);
+    float hR = sampleHeightAt(gx1, gz);
+    float hD = sampleHeightAt(gx, gz0);
+    float hU = sampleHeightAt(gx, gz1);
+    QVector3D dx(2.0f * m_tileSize, hR - hL, 0.0f);
+    QVector3D dz(0.0f, hU - hD, 2.0f * m_tileSize);
+    QVector3D n = QVector3D::crossProduct(dz, dx);
+    if (n.lengthSquared() > 0.0f)
+      n.normalize();
+    return n.isNull() ? QVector3D(0, 1, 0) : n;
+  };
+
   for (const auto &chunk : m_chunks) {
     if (!chunk.mesh)
       continue;
@@ -98,9 +163,11 @@ void TerrainRenderer::submit(Renderer &renderer, ResourceManager &resources) {
   for (const auto &prop : m_props) {
     if (useVisibility) {
       int gridX = static_cast<int>(
-          std::round(prop.position.x() / m_tileSize + halfWidth));
+          std::floor(prop.position.x() / m_tileSize + halfWidth + 0.5f));
       int gridZ = static_cast<int>(
-          std::round(prop.position.z() / m_tileSize + halfHeight));
+          std::floor(prop.position.z() / m_tileSize + halfHeight + 0.5f));
+      gridX = std::clamp(gridX, 0, m_width - 1);
+      gridZ = std::clamp(gridZ, 0, m_height - 1);
       if (visibility.stateAt(gridX, gridZ) !=
           Game::Map::VisibilityState::Visible)
         continue;
@@ -121,20 +188,47 @@ void TerrainRenderer::submit(Renderer &renderer, ResourceManager &resources) {
     if (!mesh)
       continue;
 
+    float gx = prop.position.x() / m_tileSize + halfWidth;
+    float gz = prop.position.z() / m_tileSize + halfHeight;
+    QVector3D n = normalFromHeights(gx, gz);
+    float slope = 1.0f - std::clamp(n.y(), 0.0f, 1.0f);
+
+    QQuaternion tilt = QQuaternion::rotationTo(QVector3D(0, 1, 0), n);
+
     QMatrix4x4 model;
     model.translate(prop.position);
+    model.rotate(tilt);
     model.rotate(prop.rotationDeg, 0.0f, 1.0f, 0.0f);
-    model.scale(prop.scale);
-    renderer.mesh(mesh, model, prop.color, white, prop.alpha);
+
+    QVector3D scale = prop.scale;
+    float along = 1.0f + 0.25f * (1.0f - slope);
+    float across = 1.0f - 0.15f * (1.0f - slope);
+    scale.setX(scale.x() * across);
+    scale.setZ(scale.z() * along);
+    model.scale(scale);
+
+    QVector3D propColor = prop.color;
+    float shade = 0.9f + 0.2f * (1.0f - slope);
+    propColor *= shade;
+    renderer.mesh(mesh, model, clamp01(propColor), white, prop.alpha);
 
     if (quadMesh) {
       QMatrix4x4 decal;
       decal.translate(prop.position.x(), prop.position.y() + 0.01f,
                       prop.position.z());
       decal.rotate(-90.0f, 1.0f, 0.0f, 0.0f);
-      decal.scale(prop.scale.x() * 1.4f, prop.scale.z() * 1.4f, 1.0f);
-      QVector3D aoColor(0.06f, 0.06f, 0.055f);
-      renderer.mesh(quadMesh, decal, aoColor, white, 0.35f);
+      decal.rotate(tilt);
+      float scaleBoost = (prop.type == PropType::Tuft)
+                             ? 1.25f
+                             : (prop.type == PropType::Pebble ? 1.6f : 1.4f);
+      decal.scale(prop.scale.x() * scaleBoost, prop.scale.z() * scaleBoost,
+                  1.0f);
+      float ao = (prop.type == PropType::Tuft)
+                     ? 0.22f
+                     : (prop.type == PropType::Pebble ? 0.42f : 0.35f);
+      ao = std::clamp(ao + 0.15f * slope, 0.18f, 0.55f);
+      QVector3D aoColor(0.05f, 0.05f, 0.048f);
+      renderer.mesh(quadMesh, decal, aoColor, white, ao);
     }
   }
 }
@@ -206,6 +300,24 @@ void TerrainRenderer::buildMeshes() {
     }
   }
 
+  {
+    std::vector<QVector3D> smoothed = normals;
+    auto getN = [&](int x, int z) -> QVector3D & {
+      return normals[z * m_width + x];
+    };
+    for (int z = 1; z < m_height - 1; ++z) {
+      for (int x = 1; x < m_width - 1; ++x) {
+        QVector3D acc(0, 0, 0);
+        for (int dz = -1; dz <= 1; ++dz)
+          for (int dx = -1; dx <= 1; ++dx)
+            acc += getN(x + dx, z + dz);
+        acc.normalize();
+        smoothed[z * m_width + x] = acc;
+      }
+    }
+    normals.swap(smoothed);
+  }
+
   auto quadSection = [&](Game::Map::TerrainType a, Game::Map::TerrainType b,
                          Game::Map::TerrainType c, Game::Map::TerrainType d) {
     int priorityA = sectionFor(a);
@@ -236,6 +348,13 @@ void TerrainRenderer::buildMeshes() {
         float rotationDeg = 0.0f;
         bool flipU = false;
         float tint = 1.0f;
+        QVector3D normalSum = QVector3D(0, 0, 0);
+        float slopeSum = 0.0f;
+        float heightVarSum = 0.0f;
+        int statCount = 0;
+
+        float aoSum = 0.0f;
+        int aoCount = 0;
       };
 
       SectionData sections[3];
@@ -244,8 +363,9 @@ void TerrainRenderer::buildMeshes() {
       uint32_t variantSeed = chunkSeed ^ 0x9e3779b9u;
       float rotationStep = static_cast<float>((variantSeed >> 5) & 3) * 90.0f;
       bool flip = ((variantSeed >> 7) & 1u) != 0u;
-      static const float tintVariants[5] = {0.92f, 0.96f, 1.0f, 1.04f, 1.08f};
-      float tint = tintVariants[(variantSeed >> 12) % 5];
+      static const float tintVariants[7] = {0.9f,  0.94f, 0.97f, 1.0f,
+                                            1.03f, 1.06f, 1.1f};
+      float tint = tintVariants[(variantSeed >> 12) % 7];
 
       for (auto &section : sections) {
         section.rotationDeg = rotationStep;
@@ -270,36 +390,40 @@ void TerrainRenderer::buildMeshes() {
         v.normal[0] = normal.x();
         v.normal[1] = normal.y();
         v.normal[2] = normal.z();
-        float uu = gx / float(std::max(m_width - 1, 1));
-        float vv = gz / float(std::max(m_height - 1, 1));
+
+        float texScale = 0.2f / std::max(1.0f, m_tileSize);
+        float uu = pos.x() * texScale;
+        float vv = pos.z() * texScale;
+
         if (section.flipU)
-          uu = 1.0f - uu;
-        float ru = uu - 0.5f;
-        float rv = vv - 0.5f;
+          uu = -uu;
+        float ru = uu, rv = vv;
         switch (static_cast<int>(section.rotationDeg)) {
-        case 90:
-          std::swap(ru, rv);
-          ru = -ru;
-          break;
+        case 90: {
+          float t = ru;
+          ru = -rv;
+          rv = t;
+        } break;
         case 180:
           ru = -ru;
           rv = -rv;
           break;
-        case 270:
-          std::swap(ru, rv);
-          rv = -rv;
-          break;
+        case 270: {
+          float t = ru;
+          ru = rv;
+          rv = -t;
+        } break;
         default:
           break;
         }
-        uu = ru + 0.5f;
-        vv = rv + 0.5f;
-        v.texCoord[0] = uu;
-        v.texCoord[1] = vv;
+        v.texCoord[0] = ru;
+        v.texCoord[1] = rv;
+
         section.vertices.push_back(v);
         unsigned int localIndex =
             static_cast<unsigned int>(section.vertices.size() - 1);
         section.remap.emplace(globalIndex, localIndex);
+        section.normalSum += normal;
         return localIndex;
       };
 
@@ -338,6 +462,37 @@ void TerrainRenderer::buildMeshes() {
                              0.25f;
           section.heightSum += quadHeight;
           section.heightCount += 1;
+
+          float nY = (normals[idx0].y() + normals[idx1].y() +
+                      normals[idx2].y() + normals[idx3].y()) *
+                     0.25f;
+          float slope = 1.0f - std::clamp(nY, 0.0f, 1.0f);
+          section.slopeSum += slope;
+
+          float hmin =
+              std::min(std::min(m_heightData[idx0], m_heightData[idx1]),
+                       std::min(m_heightData[idx2], m_heightData[idx3]));
+          float hmax =
+              std::max(std::max(m_heightData[idx0], m_heightData[idx1]),
+                       std::max(m_heightData[idx2], m_heightData[idx3]));
+          section.heightVarSum += (hmax - hmin);
+          section.statCount += 1;
+
+          auto H = [&](int gx, int gz) {
+            gx = std::clamp(gx, 0, m_width - 1);
+            gz = std::clamp(gz, 0, m_height - 1);
+            return m_heightData[gz * m_width + gx];
+          };
+          int cx = x, cz = z;
+          float hC = quadHeight;
+          float ao = 0.0f;
+          ao += std::max(0.0f, H(cx - 1, cz) - hC);
+          ao += std::max(0.0f, H(cx + 1, cz) - hC);
+          ao += std::max(0.0f, H(cx, cz - 1) - hC);
+          ao += std::max(0.0f, H(cx, cz + 1) - hC);
+          ao = std::clamp(ao * 0.15f, 0.0f, 1.0f);
+          section.aoSum += ao;
+          section.aoCount += 1;
         }
       }
 
@@ -364,10 +519,68 @@ void TerrainRenderer::buildMeshes() {
             (section.heightCount > 0)
                 ? section.heightSum / float(section.heightCount)
                 : 0.0f;
+
         QVector3D baseColor = getTerrainColor(chunk.type, chunk.averageHeight);
+        float avgSlope = (section.statCount > 0)
+                             ? (section.slopeSum / float(section.statCount))
+                             : 0.0f;
+        float roughness =
+            (section.statCount > 0)
+                ? (section.heightVarSum / float(section.statCount))
+                : 0.0f;
+        QVector3D rockTint(0.52f, 0.49f, 0.47f);
+        float slopeMix = std::clamp(
+            avgSlope *
+                ((chunk.type == Game::Map::TerrainType::Flat)
+                     ? 0.3f
+                     : (chunk.type == Game::Map::TerrainType::Hill ? 0.6f
+                                                                   : 0.85f)),
+            0.0f, 1.0f);
+
+        QVector3D avgN = section.normalSum;
+        if (avgN.lengthSquared() > 0.0f)
+          avgN.normalize();
+        QVector3D lightDir = QVector3D(0.35f, 0.8f, 0.45f);
+        lightDir.normalize();
+        float ndl = std::clamp(
+            QVector3D::dotProduct(avgN, lightDir) * 0.5f + 0.5f, 0.0f, 1.0f);
+        float dirShade = 0.9f + 0.25f * ndl;
+
+        float valleyShade =
+            1.0f -
+            std::clamp((4.0f - chunk.averageHeight) * 0.06f, 0.0f, 0.15f);
+        float roughShade = 1.0f - std::clamp(roughness * 0.35f, 0.0f, 0.2f);
+
+        QVector3D north(0, 0, 1);
+        float northness = std::clamp(
+            QVector3D::dotProduct(avgN, north) * 0.5f + 0.5f, 0.0f, 1.0f);
+        QVector3D coolTint(0.95f, 1.03f, 1.05f);
+        QVector3D warmTint(1.05f, 1.0f, 0.95f);
+        QVector3D aspectTint =
+            coolTint * northness + warmTint * (1.0f - northness);
+
+        float centerGX = 0.5f * (chunk.minX + chunk.maxX);
+        float centerGZ = 0.5f * (chunk.minZ + chunk.maxZ);
+        float centerWX = (centerGX - halfWidth) * m_tileSize;
+        float centerWZ = (centerGZ - halfHeight) * m_tileSize;
+        float macro = valueNoise(centerWX * 0.02f, centerWZ * 0.02f, 1337u);
+        float macroShade = 0.9f + 0.2f * macro;
+
+        float aoAvg = (section.aoCount > 0)
+                          ? (section.aoSum / float(section.aoCount))
+                          : 0.0f;
+        float aoShade = 1.0f - 0.35f * aoAvg;
+
         chunk.tint = section.tint;
-        chunk.color = applyTint(baseColor, section.tint);
-        chunk.color = 0.88f * chunk.color + QVector3D(0.07f, 0.07f, 0.07f);
+        QVector3D color = baseColor * (1.0f - slopeMix) + rockTint * slopeMix;
+        color = applyTint(color, chunk.tint);
+        color *= dirShade * valleyShade * roughShade * macroShade;
+        color.setX(color.x() * aspectTint.x());
+        color.setY(color.y() * aspectTint.y());
+        color.setZ(color.z() * aspectTint.z());
+        color *= aoShade;
+        color = color * 0.96f + QVector3D(0.04f, 0.04f, 0.04f);
+        chunk.color = clamp01(color);
 
         if (chunk.type != Game::Map::TerrainType::Mountain) {
           uint32_t propSeed = hashCoords(chunk.minX, chunk.minZ,
@@ -375,65 +588,128 @@ void TerrainRenderer::buildMeshes() {
           uint32_t state = propSeed ^ 0x6d2b79f5u;
           float spawnChance = rand01(state);
           int clusterCount = 0;
-          if (spawnChance > 0.65f) {
+          if (spawnChance > 0.58f) {
             clusterCount = 1;
-            if (rand01(state) > 0.8f)
+            if (rand01(state) > 0.7f)
+              clusterCount += 1;
+            if (rand01(state) > 0.9f)
               clusterCount += 1;
           }
 
           for (int cluster = 0; cluster < clusterCount; ++cluster) {
             float gridSpanX = float(chunk.maxX - chunk.minX + 1);
             float gridSpanZ = float(chunk.maxZ - chunk.minZ + 1);
-            float centerGX = float(chunk.minX) + rand01(state) * gridSpanX;
-            float centerGZ = float(chunk.minZ) + rand01(state) * gridSpanZ;
+            float centerGX2 = float(chunk.minX) + rand01(state) * gridSpanX;
+            float centerGZ2 = float(chunk.minZ) + rand01(state) * gridSpanZ;
 
-            int propsPerCluster = 2 + static_cast<int>(rand01(state) * 3.0f);
-            float scatterRadius = remap(rand01(state), 0.2f, 0.8f) * m_tileSize;
+            int propsPerCluster = 2 + static_cast<int>(rand01(state) * 4.0f);
+            float scatterRadius =
+                remap(rand01(state), 0.25f, 0.85f) * m_tileSize;
 
             for (int p = 0; p < propsPerCluster; ++p) {
               float angle = rand01(state) * 6.2831853f;
-              float radius = scatterRadius * rand01(state);
-              float gx = centerGX + std::cos(angle) * radius / m_tileSize;
-              float gz = centerGZ + std::sin(angle) * radius / m_tileSize;
-
-              int sampleGX =
-                  std::clamp(static_cast<int>(std::round(gx)), 0, m_width - 1);
-              int sampleGZ =
-                  std::clamp(static_cast<int>(std::round(gz)), 0, m_height - 1);
+              float radius = scatterRadius * std::sqrt(rand01(state));
+              float gx = centerGX2 + std::cos(angle) * radius / m_tileSize;
+              float gz = centerGZ2 + std::sin(angle) * radius / m_tileSize;
+
               float worldX = (gx - halfWidth) * m_tileSize;
               float worldZ = (gz - halfHeight) * m_tileSize;
-              float worldY = m_heightData[sampleGZ * m_width + sampleGX];
+              float worldY = 0.0f;
+              {
+                float sgx = std::clamp(gx, 0.0f, float(m_width - 1));
+                float sgz = std::clamp(gz, 0.0f, float(m_height - 1));
+                int x0 = int(std::floor(sgx));
+                int z0 = int(std::floor(sgz));
+                int x1 = std::min(x0 + 1, m_width - 1);
+                int z1 = std::min(z0 + 1, m_height - 1);
+                float tx = sgx - float(x0);
+                float tz = sgz - float(z0);
+                float h00 = m_heightData[z0 * m_width + x0];
+                float h10 = m_heightData[z0 * m_width + x1];
+                float h01 = m_heightData[z1 * m_width + x0];
+                float h11 = m_heightData[z1 * m_width + x1];
+                float h0 = h00 * (1.0f - tx) + h10 * tx;
+                float h1 = h01 * (1.0f - tx) + h11 * tx;
+                worldY = h0 * (1.0f - tz) + h1 * tz;
+              }
 
-              float pick = rand01(state);
+              QVector3D n;
+              {
+                float sgx = std::clamp(gx, 0.0f, float(m_width - 1));
+                float sgz = std::clamp(gz, 0.0f, float(m_height - 1));
+                float gx0 = std::clamp(sgx - 1.0f, 0.0f, float(m_width - 1));
+                float gx1 = std::clamp(sgx + 1.0f, 0.0f, float(m_width - 1));
+                float gz0 = std::clamp(sgz - 1.0f, 0.0f, float(m_height - 1));
+                float gz1 = std::clamp(sgz + 1.0f, 0.0f, float(m_height - 1));
+                auto h = [&](float x, float z) {
+                  int xi = int(std::round(x));
+                  int zi = int(std::round(z));
+                  return m_heightData[zi * m_width + xi];
+                };
+                QVector3D dx(2.0f * m_tileSize, h(gx1, sgz) - h(gx0, sgz),
+                             0.0f);
+                QVector3D dz(0.0f, h(sgx, gz1) - h(sgx, gz0),
+                             2.0f * m_tileSize);
+                n = QVector3D::crossProduct(dz, dx);
+                if (n.lengthSquared() > 0.0f)
+                  n.normalize();
+                if (n.isNull())
+                  n = QVector3D(0, 1, 0);
+              }
+              float slope = 1.0f - std::clamp(n.y(), 0.0f, 1.0f);
+
+              float northnessP = std::clamp(
+                  QVector3D::dotProduct(n, QVector3D(0, 0, 1)) * 0.5f + 0.5f,
+                  0.0f, 1.0f);
+              auto Hs = [&](int ix, int iz) {
+                ix = std::clamp(ix, 0, m_width - 1);
+                iz = std::clamp(iz, 0, m_height - 1);
+                return m_heightData[iz * m_width + ix];
+              };
+              int ix = int(std::round(gx)), iz = int(std::round(gz));
+              float concav = std::max(0.0f, (Hs(ix - 1, iz) + Hs(ix + 1, iz) +
+                                             Hs(ix, iz - 1) + Hs(ix, iz + 1)) *
+                                                    0.25f -
+                                                Hs(ix, iz));
+              concav = std::clamp(concav * 0.15f, 0.0f, 1.0f);
+
+              float tuftAffinity = (1.0f - slope) * (0.6f + 0.4f * northnessP) *
+                                   (0.7f + 0.3f * concav);
+              float pebbleAffinity =
+                  (0.3f + 0.7f * slope) * (0.6f + 0.4f * concav);
+
+              float r = rand01(state);
               PropInstance instance;
-              if (pick < 0.45f) {
+              if (r < 0.55f * tuftAffinity && slope < 0.6f) {
                 instance.type = PropType::Tuft;
-                instance.color = applyTint(QVector3D(0.26f, 0.6f, 0.22f),
+                instance.color = applyTint(QVector3D(0.28f, 0.62f, 0.24f),
                                            remap(rand01(state), 0.9f, 1.15f));
-                instance.scale = QVector3D(remap(rand01(state), 0.18f, 0.28f),
-                                           remap(rand01(state), 0.4f, 0.6f),
-                                           remap(rand01(state), 0.18f, 0.28f));
+                instance.scale = QVector3D(remap(rand01(state), 0.20f, 0.32f),
+                                           remap(rand01(state), 0.45f, 0.7f),
+                                           remap(rand01(state), 0.20f, 0.32f));
                 instance.alpha = 1.0f;
-              } else if (pick < 0.8f) {
+              } else if (r < 0.85f * pebbleAffinity) {
                 instance.type = PropType::Pebble;
-                instance.color = applyTint(QVector3D(0.42f, 0.41f, 0.39f),
-                                           remap(rand01(state), 0.85f, 1.05f));
-                instance.scale = QVector3D(remap(rand01(state), 0.12f, 0.22f),
-                                           remap(rand01(state), 0.06f, 0.1f),
-                                           remap(rand01(state), 0.12f, 0.22f));
+                instance.color = applyTint(QVector3D(0.44f, 0.42f, 0.40f),
+                                           remap(rand01(state), 0.85f, 1.08f));
+                instance.scale = QVector3D(remap(rand01(state), 0.12f, 0.26f),
+                                           remap(rand01(state), 0.06f, 0.12f),
+                                           remap(rand01(state), 0.12f, 0.26f));
                 instance.alpha = 1.0f;
               } else {
                 instance.type = PropType::Stick;
-                instance.color = applyTint(QVector3D(0.35f, 0.24f, 0.12f),
-                                           remap(rand01(state), 0.95f, 1.1f));
-                instance.scale = QVector3D(remap(rand01(state), 0.05f, 0.09f),
-                                           remap(rand01(state), 0.35f, 0.55f),
-                                           remap(rand01(state), 0.05f, 0.09f));
+                instance.color = applyTint(QVector3D(0.36f, 0.25f, 0.13f),
+                                           remap(rand01(state), 0.95f, 1.12f));
+                instance.scale = QVector3D(remap(rand01(state), 0.06f, 0.1f),
+                                           remap(rand01(state), 0.35f, 0.6f),
+                                           remap(rand01(state), 0.06f, 0.1f));
                 instance.alpha = 1.0f;
               }
               instance.rotationDeg = rand01(state) * 360.0f;
               instance.position = QVector3D(worldX, worldY, worldZ);
-              m_props.push_back(instance);
+
+              if (slope < 0.95f)
+                m_props.push_back(instance);
             }
           }
         }
@@ -452,25 +728,20 @@ QVector3D TerrainRenderer::getTerrainColor(Game::Map::TerrainType type,
                                            float height) const {
   switch (type) {
   case Game::Map::TerrainType::Mountain:
-
     if (height > 4.0f) {
-      return QVector3D(0.68f, 0.69f, 0.72f);
+      return QVector3D(0.66f, 0.68f, 0.72f);
     } else {
-      return QVector3D(0.52f, 0.49f, 0.47f);
+      return QVector3D(0.50f, 0.48f, 0.46f);
     }
-
-  case Game::Map::TerrainType::Hill:
-
-  {
+  case Game::Map::TerrainType::Hill: {
     float t = std::clamp(height / 3.0f, 0.0f, 1.0f);
-    QVector3D lushGrass(0.36f, 0.65f, 0.30f);
-    QVector3D sunKissed(0.58f, 0.48f, 0.34f);
+    QVector3D lushGrass(0.34f, 0.66f, 0.30f);
+    QVector3D sunKissed(0.58f, 0.49f, 0.35f);
     return lushGrass * (1.0f - t) + sunKissed * t;
   }
-
   case Game::Map::TerrainType::Flat:
   default:
-    return QVector3D(0.26f, 0.56f, 0.29f);
+    return QVector3D(0.27f, 0.58f, 0.30f);
   }
 }
 

+ 43 - 7
render/scene_renderer.cpp

@@ -1,4 +1,5 @@
 #include "scene_renderer.h"
+#include "../game/map/visibility_service.h"
 #include "entity/registry.h"
 #include "game/core/component.h"
 #include "game/core/world.h"
@@ -123,6 +124,16 @@ void Renderer::renderWorld(Engine::Core::World *world) {
       continue;
     }
 
+    auto *unitComp = entity->getComponent<Engine::Core::UnitComponent>();
+    if (unitComp && unitComp->ownerId != m_localOwnerId) {
+      auto &vis = Game::Map::VisibilityService::instance();
+      if (vis.isInitialized()) {
+        if (!vis.isVisibleWorld(transform->position.x, transform->position.z)) {
+          continue;
+        }
+      }
+    }
+
     QMatrix4x4 modelMatrix;
     modelMatrix.translate(transform->position.x, transform->position.y,
                           transform->position.z);
@@ -184,15 +195,40 @@ void Renderer::renderWorld(Engine::Core::World *world) {
       Mesh *contactQuad = res->quad();
       Texture *white = res->white();
       if (contactQuad && white) {
-        QMatrix4x4 contact;
-        contact.translate(transform->position.x, transform->position.y + 0.03f,
-                          transform->position.z);
-        contact.rotate(-90.0f, 1.0f, 0.0f, 0.0f);
+        QMatrix4x4 contactBase;
+        contactBase.translate(transform->position.x,
+                              transform->position.y + 0.03f,
+                              transform->position.z);
+        contactBase.rotate(-90.0f, 1.0f, 0.0f, 0.0f);
         float footprint =
             std::max({transform->scale.x, transform->scale.z, 0.6f});
-        contact.scale(footprint * 0.55f, footprint * 0.35f, 1.0f);
-        mesh(contactQuad, contact, QVector3D(0.08f, 0.08f, 0.075f), white,
-             0.55f);
+
+        float sizeRatio = 1.0f;
+        if (auto *unit = entity->getComponent<Engine::Core::UnitComponent>()) {
+          int mh = std::max(1, unit->maxHealth);
+          sizeRatio = std::clamp(unit->health / float(mh), 0.0f, 1.0f);
+        }
+        float eased = 0.25f + 0.75f * sizeRatio;
+
+        float baseScaleX = footprint * 0.55f * eased;
+        float baseScaleY = footprint * 0.35f * eased;
+
+        QVector3D col(0.03f, 0.03f, 0.03f);
+        float centerAlpha = 0.32f * eased;
+        float midAlpha = 0.16f * eased;
+        float outerAlpha = 0.07f * eased;
+
+        QMatrix4x4 c0 = contactBase;
+        c0.scale(baseScaleX * 0.60f, baseScaleY * 0.60f, 1.0f);
+        mesh(contactQuad, c0, col, white, centerAlpha);
+
+        QMatrix4x4 c1 = contactBase;
+        c1.scale(baseScaleX * 0.95f, baseScaleY * 0.95f, 1.0f);
+        mesh(contactQuad, c1, col, white, midAlpha);
+
+        QMatrix4x4 c2 = contactBase;
+        c2.scale(baseScaleX * 1.35f, baseScaleY * 1.35f, 1.0f);
+        mesh(contactQuad, c2, col, white, outerAlpha);
       }
     }
     mesh(meshToDraw, modelMatrix, color, res ? res->white() : nullptr, 1.0f);

+ 2 - 0
render/scene_renderer.h

@@ -55,6 +55,7 @@ public:
     return m_backend ? m_backend->resources() : nullptr;
   }
   void setHoveredEntityId(unsigned int id) { m_hoveredEntityId = id; }
+  void setLocalOwnerId(int ownerId) { m_localOwnerId = ownerId; }
 
   void setSelectedEntities(const std::vector<unsigned int> &ids) {
     m_selectedIds.clear();
@@ -134,6 +135,7 @@ private:
   std::atomic<bool> m_paused{false};
 
   std::mutex m_worldMutex;
+  int m_localOwnerId = 1;
 };
 
 } // namespace Render::GL

+ 88 - 18
ui/qml/GameView.qml

@@ -56,6 +56,10 @@ Item {
                 gameView.cursorMode = game.cursorMode
             }
         }
+
+        
+        property int keyPanCount: 0
+        property bool mousePanActive: false
         
         
         
@@ -175,12 +179,10 @@ Item {
                 }
             }
             onWheel: function(w) {
-                
-                
                 var dy = (w.angleDelta ? w.angleDelta.y / 120 : w.delta / 120)
-                if (dy !== 0 && typeof game !== 'undefined' && game.cameraElevate) {
+                if (dy !== 0 && typeof game !== 'undefined' && game.cameraZoom) {
                     
-                    game.cameraElevate(-dy * 0.5)
+                    game.cameraZoom(dy * 0.8)
                 }
                 w.accepted = true
             }
@@ -233,6 +235,9 @@ Item {
                     selectionBox.visible = true
                 } else if (mouse.button === Qt.RightButton) {
                     
+                    mousePanActive = true
+                    mainWindow.edgeScrollDisabled = true
+
                     if (gameView.setRallyMode) {
                         gameView.setRallyMode = false
                     }
@@ -266,6 +271,11 @@ Item {
                         }
                     }
                 }
+                if (mouse.button === Qt.RightButton) {
+                    mousePanActive = false
+                    
+                    mainWindow.edgeScrollDisabled = (renderArea.keyPanCount > 0) || mousePanActive
+                }
             }
         }
 
@@ -471,9 +481,11 @@ Item {
     
     Keys.onPressed: function(event) {
         if (typeof game === 'undefined') return
-        var yawStep = event.modifiers & Qt.ShiftModifier ? 4 : 2
-        var panStep = 0.6
-        switch (event.key) {
+    var yawStep = (event.modifiers & Qt.ShiftModifier) ? 8 : 4
+    
+    
+    var inputStep = event.modifiers & Qt.ShiftModifier ? 2 : 1
+    switch (event.key) {
             
             case Qt.Key_Escape:
                 if (typeof mainWindow !== 'undefined' && !mainWindow.menuVisible) {
@@ -490,23 +502,81 @@ Item {
                 }
                 break
             
-            case Qt.Key_W: game.cameraMove(0, panStep);  event.accepted = true; break
-            case Qt.Key_S: game.cameraMove(0, -panStep); event.accepted = true; break
-            case Qt.Key_A: game.cameraMove(-panStep, 0); event.accepted = true; break
-            case Qt.Key_D: game.cameraMove(panStep, 0);  event.accepted = true; break
+            case Qt.Key_W:
+                game.cameraMove(0, inputStep);
+                
+                renderArea.keyPanCount += 1
+                mainWindow.edgeScrollDisabled = true
+                event.accepted = true; break
+            case Qt.Key_S:
+                game.cameraMove(0, -inputStep);
+                renderArea.keyPanCount += 1
+                mainWindow.edgeScrollDisabled = true
+                event.accepted = true; break
+            case Qt.Key_A:
+                game.cameraMove(-inputStep, 0);
+                renderArea.keyPanCount += 1
+                mainWindow.edgeScrollDisabled = true
+                event.accepted = true; break
+            case Qt.Key_D:
+                game.cameraMove(inputStep, 0);
+                renderArea.keyPanCount += 1
+                mainWindow.edgeScrollDisabled = true
+                event.accepted = true; break
             
-            case Qt.Key_Up:    game.cameraMove(0, panStep);  event.accepted = true; break
-            case Qt.Key_Down:  game.cameraMove(0, -panStep); event.accepted = true; break
-            case Qt.Key_Left:  game.cameraMove(-panStep, 0); event.accepted = true; break
-            case Qt.Key_Right: game.cameraMove(panStep, 0);  event.accepted = true; break
+            case Qt.Key_Up:
+                game.cameraMove(0, inputStep);
+                renderArea.keyPanCount += 1
+                mainWindow.edgeScrollDisabled = true
+                event.accepted = true; break
+            case Qt.Key_Down:
+                game.cameraMove(0, -inputStep);
+                renderArea.keyPanCount += 1
+                mainWindow.edgeScrollDisabled = true
+                event.accepted = true; break
+            case Qt.Key_Left:
+                game.cameraMove(-inputStep, 0);
+                renderArea.keyPanCount += 1
+                mainWindow.edgeScrollDisabled = true
+                event.accepted = true; break
+            case Qt.Key_Right:
+                game.cameraMove(inputStep, 0);
+                renderArea.keyPanCount += 1
+                mainWindow.edgeScrollDisabled = true
+                event.accepted = true; break
             
             case Qt.Key_Q: game.cameraYaw(-yawStep); event.accepted = true; break
             case Qt.Key_E: game.cameraYaw(yawStep);  event.accepted = true; break
             
-            case Qt.Key_R: game.cameraElevate(0.5);  event.accepted = true; break
-            case Qt.Key_F: game.cameraElevate(-0.5); event.accepted = true; break
+            
+            
+            
+            
+            var shiftHeld = (event.modifiers & Qt.ShiftModifier) !== 0
+            var pitchStep = shiftHeld ? 8 : 4
+            
+            console.log("GameView Keys: key=", event.key, "shift=", shiftHeld, "pitchStep=", pitchStep)
+            case Qt.Key_R: game.cameraOrbitDirection(1, shiftHeld);  event.accepted = true; break
+            case Qt.Key_F: game.cameraOrbitDirection(-1, shiftHeld); event.accepted = true; break
         }
     }
-    
+    Keys.onReleased: function(event) {
+        if (typeof game === 'undefined') return
+        var movementKeys = [Qt.Key_W, Qt.Key_A, Qt.Key_S, Qt.Key_D, Qt.Key_Up, Qt.Key_Down, Qt.Key_Left, Qt.Key_Right]
+        if (movementKeys.indexOf(event.key) !== -1) {
+            renderArea.keyPanCount = Math.max(0, renderArea.keyPanCount - 1)
+            if (renderArea.keyPanCount === 0 && !renderArea.mousePanActive) {
+                mainWindow.edgeScrollDisabled = false
+            }
+        }
+        
+        if ((event.key === Qt.Key_Shift) || (event.key === Qt.Key_Shift || event.key === Qt.Key_Shift)) {
+            
+            if (renderArea.keyPanCount === 0 && !renderArea.mousePanActive) {
+                mainWindow.edgeScrollDisabled = false
+            }
+        }
+    }
+
     focus: true
 }

+ 5 - 0
ui/qml/HUDTop.qml

@@ -123,6 +123,11 @@ Item {
                     contentItem: Text { text: parent.text; font.pointSize: 9; font.bold: true; color: "#ecf0f1"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
                     onToggled: { if (typeof game !== 'undefined' && game.cameraFollowSelection) game.cameraFollowSelection(checked) }
                 }
+                Button { width: 80; height: 32; text: "Reset"; focusPolicy: Qt.NoFocus
+                    background: Rectangle { color: parent.hovered ? "#34495e" : "#2c3e50"; radius: 4; border.color: "#1a252f"; border.width: 1 }
+                    contentItem: Text { text: parent.text; font.pointSize: 9; font.bold: true; color: "#ecf0f1"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                    onClicked: { if (typeof game !== 'undefined' && game.resetCamera) game.resetCamera() }
+                }
             }
 
             Rectangle { width: 2; Layout.fillHeight: true; Layout.topMargin: 8; Layout.bottomMargin: 8

+ 17 - 9
ui/qml/Main.qml

@@ -16,6 +16,9 @@ ApplicationWindow {
     property bool menuVisible: true
     property bool gameStarted: false  
     property bool gamePaused: false   
+    
+    
+    property bool edgeScrollDisabled: false
 
     
     GameView {
@@ -178,12 +181,14 @@ ApplicationWindow {
         enabled: visible
 
         
-        property real horzThreshold: 80
-        property real horzMaxSpeed: 0.5
-        
-        property real vertThreshold: 120
-        property real verticalDeadZone: 32
-        property real vertMaxSpeed: 0.1
+    
+    
+    property real horzThreshold: 120
+    property real horzMaxSpeed: 0.25
+
+    property real vertThreshold: 160
+    property real verticalDeadZone: 48
+    property real vertMaxSpeed: 0.05
         property real xPos: -1
         property real yPos: -1
         
@@ -248,7 +253,7 @@ ApplicationWindow {
                 const y = edgeScrollOverlay.yPos
                 if (x < 0 || y < 0) return
                 
-                if (edgeScrollOverlay.inHudZone(x, y)) {
+                if (edgeScrollOverlay.inHudZone(x, y) || mainWindow.edgeScrollDisabled) {
                     if (game.setHoverAtScreen) game.setHoverAtScreen(-1, -1)
                     return
                 }
@@ -279,8 +284,11 @@ ApplicationWindow {
                 const curveH = function(a) { return a*a }
                 
                 const curveV = function(a) { return a*a*a }
-                const dx = (curveH(ir) - curveH(il)) * edgeScrollOverlay.horzMaxSpeed
-                const dz = (curveV(iu) - curveV(id)) * edgeScrollOverlay.vertMaxSpeed
+                const rawDx = (curveH(ir) - curveH(il)) * edgeScrollOverlay.horzMaxSpeed
+                const rawDz = (curveV(iu) - curveV(id)) * edgeScrollOverlay.vertMaxSpeed
+                
+                const dx = rawDx / edgeScrollOverlay.horzMaxSpeed
+                const dz = rawDz / edgeScrollOverlay.vertMaxSpeed
                 if (dx !== 0 || dz !== 0) game.cameraMove(dx, dz)
             }
         }