Browse Source

Clear selection only on right-click when selection is non-empty

djeada 2 months ago
parent
commit
11d17797b3
8 changed files with 560 additions and 311 deletions
  1. 30 8
      app/game_engine.cpp
  2. 5 1
      app/game_engine.h
  3. 11 0
      game/core/event_manager.h
  4. 5 0
      game/systems/combat_system.cpp
  5. 0 1
      main.cpp
  6. 255 135
      render/gl/camera.cpp
  7. 4 76
      ui/qml/GameView.qml
  8. 250 90
      ui/qml/HUDTop.qml

+ 30 - 8
app/game_engine.cpp

@@ -28,6 +28,7 @@
 #include "game/systems/selection_system.h"
 #include "game/systems/terrain_alignment_system.h"
 #include "render/geom/arrow.h"
+#include "game/core/event_manager.h"
 #include "render/geom/patrol_flags.h"
 #include "render/gl/bootstrap.h"
 #include "render/gl/camera.h"
@@ -64,12 +65,27 @@ GameEngine::GameEngine() {
   m_world->addSystem(std::make_unique<Game::Systems::ProductionSystem>());
   m_world->addSystem(std::make_unique<Game::Systems::TerrainAlignmentSystem>());
 
-  m_selectionSystem = std::make_unique<Game::Systems::SelectionSystem>();
-  m_world->addSystem(std::make_unique<Game::Systems::SelectionSystem>());
+  {
+    std::unique_ptr<Engine::Core::System> selSys =
+        std::make_unique<Game::Systems::SelectionSystem>();
+    m_selectionSystem =
+        dynamic_cast<Game::Systems::SelectionSystem *>(selSys.get());
+    m_world->addSystem(std::move(selSys));
+  }
 
   m_selectedUnitsModel = new SelectedUnitsModel(this, this);
   QMetaObject::invokeMethod(m_selectedUnitsModel, "refresh");
   m_pickingService = std::make_unique<Game::Systems::PickingService>();
+
+  // subscribe to unit died events to track enemy defeats
+  Engine::Core::EventManager::instance().subscribe<Engine::Core::UnitDiedEvent>(
+      [this](const Engine::Core::UnitDiedEvent &e) {
+        // increment only if the unit belonged to an enemy
+        if (e.ownerId != m_runtime.localOwnerId) {
+          m_enemyTroopsDefeated++;
+          emit enemyTroopsDefeatedChanged();
+        }
+      });
 }
 
 GameEngine::~GameEngine() = default;
@@ -91,13 +107,17 @@ void GameEngine::onRightClick(qreal sx, qreal sy) {
   if (!m_selectionSystem)
     return;
 
-  m_selectionSystem->clearSelection();
-  syncSelectionFlags();
-  emit selectedUnitsChanged();
-  if (m_selectedUnitsModel)
-    QMetaObject::invokeMethod(m_selectedUnitsModel, "refresh");
+  const auto &sel = m_selectionSystem->getSelectedUnits();
+  if (!sel.empty()) {
+    m_selectionSystem->clearSelection();
+    syncSelectionFlags();
+    emit selectedUnitsChanged();
+    if (m_selectedUnitsModel)
+      QMetaObject::invokeMethod(m_selectedUnitsModel, "refresh");
 
-  setCursorMode("normal");
+    setCursorMode("normal");
+    return;
+  }
 }
 
 void GameEngine::onAttackClick(qreal sx, qreal sy) {
@@ -417,6 +437,8 @@ void GameEngine::ensureInitialized() {
     initialize();
 }
 
+int GameEngine::enemyTroopsDefeated() const { return m_enemyTroopsDefeated; }
+
 void GameEngine::update(float dt) {
 
   if (m_runtime.loading) {

+ 5 - 1
app/game_engine.h

@@ -65,6 +65,7 @@ public:
       int maxTroopsPerPlayer READ maxTroopsPerPlayer NOTIFY troopCountChanged)
   Q_PROPERTY(
       QVariantList availableMaps READ availableMaps NOTIFY availableMapsChanged)
+  Q_PROPERTY(int enemyTroopsDefeated READ enemyTroopsDefeated NOTIFY enemyTroopsDefeatedChanged)
 
   Q_INVOKABLE void onMapClicked(qreal sx, qreal sy);
   Q_INVOKABLE void onRightClick(qreal sx, qreal sy);
@@ -101,6 +102,7 @@ public:
   bool hasUnitsSelected() const;
   int playerTroopCount() const;
   int maxTroopsPerPlayer() const { return m_level.maxTroopsPerPlayer; }
+  int enemyTroopsDefeated() const;
 
   Q_INVOKABLE bool hasSelectedType(const QString &type) const;
   Q_INVOKABLE void recruitNearSelected(const QString &unitType);
@@ -173,7 +175,7 @@ private:
   std::unique_ptr<Render::GL::GroundRenderer> m_ground;
   std::unique_ptr<Render::GL::TerrainRenderer> m_terrain;
   std::unique_ptr<Render::GL::FogRenderer> m_fog;
-  std::unique_ptr<Game::Systems::SelectionSystem> m_selectionSystem;
+  Game::Systems::SelectionSystem *m_selectionSystem = nullptr;
   std::unique_ptr<Game::Systems::PickingService> m_pickingService;
   QQuickWindow *m_window = nullptr;
   RuntimeState m_runtime;
@@ -183,8 +185,10 @@ private:
   QObject *m_selectedUnitsModel = nullptr;
   HoverState m_hover;
   PatrolState m_patrol;
+  int m_enemyTroopsDefeated = 0;
 signals:
   void selectedUnitsChanged();
+  void enemyTroopsDefeatedChanged();
   void victoryStateChanged();
   void cursorModeChanged();
   void globalCursorChanged();

+ 11 - 0
game/core/event_manager.h

@@ -18,6 +18,10 @@ template <typename T> using EventHandler = std::function<void(const T &)>;
 
 class EventManager {
 public:
+  static EventManager &instance() {
+    static EventManager inst;
+    return inst;
+  }
   template <typename T> void subscribe(EventHandler<T> handler) {
     static_assert(std::is_base_of_v<Event, T>, "T must inherit from Event");
     auto wrapper = [handler](const void *event) {
@@ -56,4 +60,11 @@ public:
   float x, y;
 };
 
+class UnitDiedEvent : public Event {
+public:
+  UnitDiedEvent(EntityID unitId, int ownerId) : unitId(unitId), ownerId(ownerId) {}
+  EntityID unitId;
+  int ownerId;
+};
+
 } // namespace Engine::Core

+ 5 - 0
game/systems/combat_system.cpp

@@ -1,4 +1,5 @@
 #include "combat_system.h"
+#include "../core/event_manager.h"
 #include "../core/component.h"
 #include "../core/world.h"
 #include "../visuals/team_colors.h"
@@ -309,6 +310,10 @@ void CombatSystem::dealDamage(Engine::Core::Entity *target, int damage) {
 
     if (unit->health <= 0) {
 
+      // publish unit died event
+      Engine::Core::EventManager::instance().publish(
+          Engine::Core::UnitDiedEvent(target->getId(), unit->ownerId));
+
       if (target->hasComponent<Engine::Core::BuildingComponent>()) {
         BuildingCollisionRegistry::instance().unregisterBuilding(
             target->getId());

+ 0 - 1
main.cpp

@@ -23,7 +23,6 @@ int main(int argc, char *argv[]) {
   qputenv("QSG_RHI_BACKEND", "opengl");
   QQuickWindow::setGraphicsApi(QSGRendererInterface::OpenGLRhi);
 
-
   QSurfaceFormat fmt;
   fmt.setVersion(3, 3);
   fmt.setProfile(QSurfaceFormat::CoreProfile);

+ 255 - 135
render/gl/camera.cpp

@@ -2,172 +2,282 @@
 #include "../../game/map/visibility_service.h"
 #include <QtMath>
 #include <cmath>
+#include <algorithm>
+#include <limits>
 
 namespace Render::GL {
 
+// -------- internal helpers, do not change public API --------
+namespace {
+constexpr float kEps      = 1e-6f;
+constexpr float kTiny     = 1e-4f;
+constexpr float kMinDist  = 1.0f;    // zoomDistance floor
+constexpr float kMaxDist  = 500.0f;  // zoomDistance ceiling
+constexpr float kMinFov   = 1.0f;    // perspective clamp (deg)
+constexpr float kMaxFov   = 89.0f;
+
+inline bool finite(const QVector3D& v) {
+  return qIsFinite(v.x()) && qIsFinite(v.y()) && qIsFinite(v.z());
+}
+inline bool finite(float v) { return qIsFinite(v); }
+
+inline QVector3D safeNormalize(const QVector3D& v,
+                               const QVector3D& fallback,
+                               float eps = kEps) {
+  if (!finite(v)) return fallback;
+  float len2 = v.lengthSquared();
+  if (len2 < eps) return fallback;
+  return v / std::sqrt(len2);
+}
+
+// Keep up-vector not collinear with front; pick a sane orthonormal basis.
+inline void orthonormalize(const QVector3D& frontIn,
+                           QVector3D& frontOut,
+                           QVector3D& rightOut,
+                           QVector3D& upOut) {
+  QVector3D worldUp(0.f, 1.f, 0.f);
+  QVector3D f = safeNormalize(frontIn, QVector3D(0, 0, -1));
+  // If front is near-collinear with worldUp, choose another temp up.
+  QVector3D u = (std::abs(QVector3D::dotProduct(f, worldUp)) > 1.f - 1e-3f)
+                  ? QVector3D(0, 0, 1)
+                  : worldUp;
+  QVector3D r = QVector3D::crossProduct(f, u);
+  if (r.lengthSquared() < kEps) r = QVector3D(1, 0, 0);
+  r = r.normalized();
+  u = QVector3D::crossProduct(r, f).normalized();
+
+  frontOut = f;
+  rightOut = r;
+  upOut    = u;
+}
+
+inline void clampOrthoBox(float& left, float& right,
+                          float& bottom, float& top) {
+  if (left == right) {
+    left -= 0.5f; right += 0.5f;
+  } else if (left > right) {
+    std::swap(left, right);
+  }
+  if (bottom == top) {
+    bottom -= 0.5f; top += 0.5f;
+  } else if (bottom > top) {
+    std::swap(bottom, top);
+  }
+}
+} // anonymous namespace
+// ------------------------------------------------------------
+
 Camera::Camera() { updateVectors(); }
 
-void Camera::setPosition(const QVector3D &position) { m_position = position; }
+void Camera::setPosition(const QVector3D &position) {
+  if (!finite(position)) return; // ignore invalid input
+  m_position = position;
+  clampAboveGround();
+  // keep looking at current target; repair front/up basis if needed
+  QVector3D newFront = (m_target - m_position);
+  orthonormalize(newFront, m_front, m_right, m_up);
+}
 
 void Camera::setTarget(const QVector3D &target) {
+  if (!finite(target)) return;
   m_target = target;
-  m_front = (m_target - m_position).normalized();
-  updateVectors();
+
+  QVector3D dir = (m_target - m_position);
+  if (dir.lengthSquared() < kEps) {
+    // Nudge target forward to avoid zero-length front
+    m_target = m_position + (m_front.lengthSquared() < kEps
+                              ? QVector3D(0, 0, -1)
+                              : m_front);
+    dir = (m_target - m_position);
+  }
+  orthonormalize(dir, m_front, m_right, m_up);
+  clampAboveGround();
 }
 
 void Camera::setUp(const QVector3D &up) {
-  m_up = up.normalized();
-  updateVectors();
+  if (!finite(up)) return;
+  QVector3D upN = up;
+  if (upN.lengthSquared() < kEps) upN = QVector3D(0, 1, 0);
+  // Ensure up is not collinear with front; re-orthonormalize
+  orthonormalize(m_target - m_position, m_front, m_right, m_up);
 }
 
 void Camera::lookAt(const QVector3D &position, const QVector3D &target,
                     const QVector3D &up) {
+  if (!finite(position) || !finite(target) || !finite(up)) return;
   m_position = position;
-  m_target = target;
-  m_up = up.normalized();
-  m_front = (m_target - m_position).normalized();
-  updateVectors();
+  m_target   = (position == target)
+                 ? position + QVector3D(0, 0, -1)
+                 : target;
+
+  QVector3D f = (m_target - m_position);
+  m_up = up.lengthSquared() < kEps ? QVector3D(0, 1, 0) : up.normalized();
+  orthonormalize(f, m_front, m_right, m_up);
+  clampAboveGround();
 }
 
 void Camera::setPerspective(float fov, float aspect, float nearPlane,
                             float farPlane) {
+  if (!finite(fov) || !finite(aspect) || !finite(nearPlane) || !finite(farPlane))
+    return;
+
   m_isPerspective = true;
-  m_fov = fov;
-  m_aspect = aspect;
-  m_nearPlane = nearPlane;
-  m_farPlane = farPlane;
+  // Robust clamps
+  m_fov       = std::clamp(fov, kMinFov, kMaxFov);
+  m_aspect    = std::max(aspect, 1e-6f);
+  m_nearPlane = std::max(nearPlane, 1e-4f);
+  m_farPlane  = std::max(farPlane, m_nearPlane + 1e-3f);
 }
 
 void Camera::setOrthographic(float left, float right, float bottom, float top,
                              float nearPlane, float farPlane) {
+  if (!finite(left) || !finite(right) || !finite(bottom) || !finite(top) ||
+      !finite(nearPlane) || !finite(farPlane))
+    return;
+
   m_isPerspective = false;
-  m_orthoLeft = left;
-  m_orthoRight = right;
+  clampOrthoBox(left, right, bottom, top);
+  m_orthoLeft   = left;
+  m_orthoRight  = right;
   m_orthoBottom = bottom;
-  m_orthoTop = top;
-  m_nearPlane = nearPlane;
-  m_farPlane = farPlane;
+  m_orthoTop    = top;
+  m_nearPlane   = std::min(nearPlane, farPlane - 1e-3f);
+  m_farPlane    = std::max(farPlane, m_nearPlane + 1e-3f);
 }
 
 void Camera::moveForward(float distance) {
+  if (!finite(distance)) return;
   m_position += m_front * distance;
-  m_target = m_position + m_front;
+  m_target    = m_position + m_front;
+  clampAboveGround();
 }
 
 void Camera::moveRight(float distance) {
+  if (!finite(distance)) return;
   m_position += m_right * distance;
-  m_target = m_position + m_front;
+  m_target    = m_position + m_front;
+  clampAboveGround();
 }
 
 void Camera::moveUp(float distance) {
-
+  if (!finite(distance)) return;
   m_position += QVector3D(0, 1, 0) * distance;
   clampAboveGround();
   m_target = m_position + m_front;
 }
 
 void Camera::zoom(float delta) {
+  if (!finite(delta)) return;
   if (m_isPerspective) {
-    m_fov = qBound(1.0f, m_fov - delta, 89.0f);
+    m_fov = qBound(kMinFov, m_fov - delta, kMaxFov);
   } else {
+    // Keep scale positive and bounded
     float scale = 1.0f + delta * 0.1f;
-    m_orthoLeft *= scale;
-    m_orthoRight *= scale;
+    if (!finite(scale) || scale <= 0.05f) scale = 0.05f;
+    if (scale > 20.0f) scale = 20.0f;
+    m_orthoLeft   *= scale;
+    m_orthoRight  *= scale;
     m_orthoBottom *= scale;
-    m_orthoTop *= scale;
+    m_orthoTop    *= scale;
+    clampOrthoBox(m_orthoLeft, m_orthoRight, m_orthoBottom, m_orthoTop);
   }
 }
 
 void Camera::zoomDistance(float delta) {
+  if (!finite(delta)) return;
 
   QVector3D offset = m_position - m_target;
   float r = offset.length();
-  if (r < 1e-4f)
-    r = 1e-4f;
+  if (r < kTiny) r = kTiny;
 
   float factor = 1.0f - delta * 0.15f;
+  if (!finite(factor)) factor = 1.0f;
   factor = std::clamp(factor, 0.1f, 10.0f);
-  float newR = r * factor;
 
-  newR = std::clamp(newR, 1.0f, 500.0f);
-  QVector3D dir = offset.normalized();
+  float newR = std::clamp(r * factor, kMinDist, kMaxDist);
+
+  QVector3D dir = safeNormalize(offset, QVector3D(0,0,1));
   m_position = m_target + dir * newR;
   clampAboveGround();
-  m_front = (m_target - m_position).normalized();
-  updateVectors();
+  QVector3D f = (m_target - m_position);
+  orthonormalize(f, m_front, m_right, m_up);
 }
 
 void Camera::rotate(float yaw, float pitch) { orbit(yaw, pitch); }
 
 void Camera::pan(float rightDist, float forwardDist) {
+  if (!finite(rightDist) || !finite(forwardDist)) return;
 
   QVector3D right = m_right;
   QVector3D front = m_front;
   front.setY(0.0f);
-  if (front.lengthSquared() > 0)
-    front.normalize();
+  if (front.lengthSquared() > 0) front.normalize();
+
   QVector3D delta = right * rightDist + front * forwardDist;
+  if (!finite(delta)) return;
+
   m_position += delta;
-  m_target += delta;
+  m_target   += delta;
   clampAboveGround();
 }
 
 void Camera::elevate(float dy) {
+  if (!finite(dy)) return;
   m_position.setY(m_position.y() + dy);
   clampAboveGround();
 }
 
 void Camera::yaw(float degrees) {
-
-  QVector3D offset = m_position - m_target;
-  float curYaw = 0.f, curPitch = 0.f;
-  computeYawPitchFromOffset(offset, curYaw, curPitch);
+  if (!finite(degrees)) return;
+  // computeYawPitchFromOffset already guards degeneracy
   orbit(degrees, 0.0f);
 }
 
 void Camera::orbit(float yawDeg, float pitchDeg) {
+  if (!finite(yawDeg) || !finite(pitchDeg)) return;
 
   QVector3D offset = m_position - m_target;
   float curYaw = 0.f, curPitch = 0.f;
   computeYawPitchFromOffset(offset, curYaw, curPitch);
 
-  m_orbitStartYaw = curYaw;
+  m_orbitStartYaw   = curYaw;
   m_orbitStartPitch = curPitch;
-  m_orbitTargetYaw = curYaw + yawDeg;
-  m_orbitTargetPitch =
-      qBound(m_pitchMinDeg, curPitch + pitchDeg, m_pitchMaxDeg);
-  m_orbitTime = 0.0f;
+  m_orbitTargetYaw  = curYaw + yawDeg;
+  m_orbitTargetPitch = qBound(m_pitchMinDeg, curPitch + pitchDeg, m_pitchMaxDeg);
+  m_orbitTime    = 0.0f;
   m_orbitPending = true;
 }
 
 void Camera::update(float dt) {
-  if (!m_orbitPending)
-    return;
+  if (!m_orbitPending) return;
+  if (!finite(dt)) return;
 
-  m_orbitTime += dt;
-  float t = m_orbitDuration <= 0.0f
-                ? 1.0f
-                : std::clamp(m_orbitTime / m_orbitDuration, 0.0f, 1.0f);
+  m_orbitTime += std::max(0.0f, dt);
+  float t = (m_orbitDuration <= 0.0f)
+              ? 1.0f
+              : std::clamp(m_orbitTime / m_orbitDuration, 0.0f, 1.0f);
 
+  // Smoothstep
   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;
+  // Interpolate yaw/pitch
+  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;
+  if (r < kTiny) r = kTiny;
 
-  float yawRad = qDegreesToRadians(newYaw);
+  float yawRad   = qDegreesToRadians(newYaw);
   float pitchRad = qDegreesToRadians(newPitch);
-  QVector3D newDir(std::sin(yawRad) * std::cos(pitchRad), std::sin(pitchRad),
+  QVector3D newDir(std::sin(yawRad) * std::cos(pitchRad),
+                   std::sin(pitchRad),
                    std::cos(yawRad) * std::cos(pitchRad));
-  m_position = m_target - newDir.normalized() * r;
+
+  QVector3D fwd = safeNormalize(newDir, m_front);
+  m_position = m_target - fwd * r;
   clampAboveGround();
-  m_front = (m_target - m_position).normalized();
-  updateVectors();
+  orthonormalize((m_target - m_position), m_front, m_right, m_up);
 
   if (t >= 1.0f) {
     m_orbitPending = false;
@@ -176,96 +286,108 @@ void Camera::update(float dt) {
 
 bool Camera::screenToGround(float sx, float sy, float screenW, float screenH,
                             QVector3D &outWorld) const {
-  if (screenW <= 0 || screenH <= 0)
-    return false;
+  if (screenW <= 0 || screenH <= 0) return false;
+  if (!finite(sx) || !finite(sy))   return false;
+
   float x = (2.0f * sx / screenW) - 1.0f;
   float y = 1.0f - (2.0f * sy / screenH);
+
   bool ok = false;
   QMatrix4x4 invVP = (getProjectionMatrix() * getViewMatrix()).inverted(&ok);
-  if (!ok)
-    return false;
+  if (!ok) return false;
+
   QVector4D nearClip(x, y, 0.0f, 1.0f);
-  QVector4D farClip(x, y, 1.0f, 1.0f);
+  QVector4D farClip (x, y, 1.0f, 1.0f);
   QVector4D nearWorld4 = invVP * nearClip;
-  QVector4D farWorld4 = invVP * farClip;
-  if (nearWorld4.w() == 0.0f || farWorld4.w() == 0.0f)
-    return false;
+  QVector4D farWorld4  = invVP * farClip;
+
+  if (std::abs(nearWorld4.w()) < kEps || std::abs(farWorld4.w()) < kEps) return false;
+
   QVector3D rayOrigin = (nearWorld4 / nearWorld4.w()).toVector3D();
-  QVector3D rayEnd = (farWorld4 / farWorld4.w()).toVector3D();
-  QVector3D rayDir = (rayEnd - rayOrigin).normalized();
-  if (qFuzzyIsNull(rayDir.y()))
-    return false;
+  QVector3D rayEnd    = (farWorld4  / farWorld4.w()).toVector3D();
+  if (!finite(rayOrigin) || !finite(rayEnd)) return false;
+
+  QVector3D rayDir = safeNormalize(rayEnd - rayOrigin, QVector3D(0, -1, 0));
+  if (std::abs(rayDir.y()) < kEps) return false;
 
   float t = (m_groundY - rayOrigin.y()) / rayDir.y();
-  if (t < 0.0f)
-    return false;
+  if (!finite(t) || t < 0.0f) return false;
+
   outWorld = rayOrigin + rayDir * t;
-  return true;
+  return finite(outWorld);
 }
 
 bool Camera::worldToScreen(const QVector3D &world, int screenW, int screenH,
                            QPointF &outScreen) const {
-  if (screenW <= 0 || screenH <= 0)
-    return false;
-  QVector4D clip =
-      getProjectionMatrix() * getViewMatrix() * QVector4D(world, 1.0f);
-  if (clip.w() == 0.0f)
-    return false;
+  if (screenW <= 0 || screenH <= 0) return false;
+  if (!finite(world)) return false;
+
+  QVector4D clip = getProjectionMatrix() * getViewMatrix() * QVector4D(world, 1.0f);
+  if (std::abs(clip.w()) < kEps) return false;
+
   QVector3D ndc = (clip / clip.w()).toVector3D();
-  if (ndc.z() < -1.0f || ndc.z() > 1.0f)
-    return false;
+  if (!qIsFinite(ndc.x()) || !qIsFinite(ndc.y()) || !qIsFinite(ndc.z())) return false;
+  if (ndc.z() < -1.0f || ndc.z() > 1.0f) return false;
+
   float sx = (ndc.x() * 0.5f + 0.5f) * float(screenW);
   float sy = (1.0f - (ndc.y() * 0.5f + 0.5f)) * float(screenH);
   outScreen = QPointF(sx, sy);
-  return true;
+  return qIsFinite(sx) && qIsFinite(sy);
 }
 
 void Camera::updateFollow(const QVector3D &targetCenter) {
-  if (!m_followEnabled)
-    return;
-  if (m_followOffset.lengthSquared() < 1e-5f) {
+  if (!m_followEnabled) return;
+  if (!finite(targetCenter)) return;
 
-    m_followOffset = m_position - m_target;
+  if (m_followOffset.lengthSquared() < 1e-5f) {
+    m_followOffset = m_position - m_target; // initialize lazily
   }
   QVector3D desiredPos = targetCenter + m_followOffset;
   QVector3D newPos =
       (m_followLerp >= 0.999f)
           ? desiredPos
-          : (m_position + (desiredPos - m_position) * m_followLerp);
-  m_target = targetCenter;
-  m_position = newPos;
+          : (m_position + (desiredPos - m_position) * std::clamp(m_followLerp, 0.0f, 1.0f));
+
+  if (!finite(newPos)) return;
 
-  m_front = (m_target - m_position).normalized();
+  m_target   = targetCenter;
+  m_position = newPos;
   clampAboveGround();
-  updateVectors();
+  orthonormalize((m_target - m_position), m_front, m_right, m_up);
 }
 
 void Camera::setRTSView(const QVector3D &center, float distance, float angle,
                         float yawDeg) {
+  if (!finite(center) || !finite(distance) || !finite(angle) || !finite(yawDeg)) return;
+
   m_target = center;
 
+  distance = std::max(distance, 0.01f);
   float pitchRad = qDegreesToRadians(angle);
-  float yawRad = qDegreesToRadians(yawDeg);
+  float yawRad   = qDegreesToRadians(yawDeg);
 
-  float y = distance * qSin(pitchRad);
-  float horiz = distance * qCos(pitchRad);
+  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();
-  updateVectors();
+  QVector3D f = (m_target - m_position);
+  orthonormalize(f, m_front, m_right, m_up);
+  clampAboveGround();
 }
 
 void Camera::setTopDownView(const QVector3D &center, float distance) {
-  m_target = center;
-  m_position = center + QVector3D(0, distance, 0);
-  m_up = QVector3D(0, 0, -1);
-  m_front = (m_target - m_position).normalized();
+  if (!finite(center) || !finite(distance)) return;
+
+  m_target   = center;
+  m_position = center + QVector3D(0, std::max(distance, 0.01f), 0);
+  m_up       = QVector3D(0, 0, -1);
+  m_front    = safeNormalize((m_target - m_position), QVector3D(0,0,1));
   updateVectors();
+  clampAboveGround();
 }
 
 QMatrix4x4 Camera::getViewMatrix() const {
@@ -276,14 +398,19 @@ QMatrix4x4 Camera::getViewMatrix() const {
 
 QMatrix4x4 Camera::getProjectionMatrix() const {
   QMatrix4x4 projection;
-
   if (m_isPerspective) {
+    // perspective() assumes sane inputs — we enforce those in setters
     projection.perspective(m_fov, m_aspect, m_nearPlane, m_farPlane);
   } else {
-    projection.ortho(m_orthoLeft, m_orthoRight, m_orthoBottom, m_orthoTop,
-                     m_nearPlane, m_farPlane);
+    // get local copies because this method is const and clampOrthoBox
+    // expects non-const references
+    float left = m_orthoLeft;
+    float right = m_orthoRight;
+    float bottom = m_orthoBottom;
+    float top = m_orthoTop;
+    clampOrthoBox(left, right, bottom, top);
+    projection.ortho(left, right, bottom, top, m_nearPlane, m_farPlane);
   }
-
   return projection;
 }
 
@@ -295,48 +422,43 @@ 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;
-  }
+  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);
-
-  QVector3D r = QVector3D::crossProduct(m_front, worldUp);
-  if (r.lengthSquared() < 1e-6f)
-    r = QVector3D(1, 0, 0);
-  m_right = r.normalized();
-  m_up = QVector3D::crossProduct(m_right, m_front).normalized();
+  QVector3D f = (m_target - m_position);
+  orthonormalize(f, m_front, m_right, m_up);
 }
 
 void Camera::clampAboveGround() {
+  if (!qIsFinite(m_position.y())) return;
+
   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 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));
+    if (tile > 0.0f && halfW >= 0.0f && halfH >= 0.0f) {
+      const float minX = -halfW * tile;
+      const float maxX =  halfW * tile;
+      const float minZ = -halfH * tile;
+      const float maxZ =  halfH * tile;
 
-    m_target.setX(std::clamp(m_target.x(), minX, maxX));
-    m_target.setZ(std::clamp(m_target.z(), minZ, maxZ));
+      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));
+    }
   }
 }
 
@@ -344,15 +466,13 @@ void Camera::computeYawPitchFromOffset(const QVector3D &off, float &yawDeg,
                                        float &pitchDeg) const {
   QVector3D dir = -off;
   if (dir.lengthSquared() < 1e-6f) {
-    yawDeg = 0.f;
-    pitchDeg = 0.f;
-    return;
+    yawDeg = 0.f; pitchDeg = 0.f; return;
   }
-  float yaw = qRadiansToDegrees(std::atan2(dir.x(), dir.z()));
+  float yaw   = qRadiansToDegrees(std::atan2(dir.x(), dir.z()));
   float lenXZ = std::sqrt(dir.x() * dir.x() + dir.z() * dir.z());
   float pitch = qRadiansToDegrees(std::atan2(dir.y(), lenXZ));
   yawDeg = yaw;
   pitchDeg = pitch;
 }
 
-} // namespace Render::GL
+} // namespace Render::GL

+ 4 - 76
ui/qml/GameView.qml

@@ -62,78 +62,6 @@ Item {
         property bool mousePanActive: false
         
         
-        
-        
-        
-        
-        
-        
-        
-        
-        
-        Rectangle {
-            anchors.top: parent.top
-            anchors.left: parent.left
-            anchors.margins: 10
-            anchors.topMargin: 70   
-            width: 200
-            height: 160
-            color: "#34495e"
-            opacity: 0.8
-            
-            Column {
-                anchors.fill: parent
-                anchors.margins: 8
-                spacing: 4
-                
-                Text {
-                    text: "Camera Controls:"
-                    color: "white"
-                    font.bold: true
-                    font.pointSize: 10
-                }
-                Text {
-                    text: "WASD - Move"
-                    color: "white"
-                    font.pointSize: 9
-                }
-                Text {
-                    text: "Mouse - Look"
-                    color: "white"
-                    font.pointSize: 9
-                }
-                Text {
-                    text: "Scroll - Zoom"
-                    color: "white"
-                    font.pointSize: 9
-                }
-                Text {
-                    text: "Q/E - Rotate"
-                    color: "white"
-                    font.pointSize: 9
-                }
-                Text {
-                    text: "R/F - Up/Down"
-                    color: "white"
-                    font.pointSize: 9
-                }
-                
-                Rectangle {
-                    width: parent.width - 16
-                    height: 1
-                    color: "#7f8c8d"
-                    opacity: 0.5
-                }
-                
-                Text {
-                    text: "Cursor: " + gameView.cursorMode.toUpperCase()
-                    color: gameView.cursorMode === "normal" ? "#bdc3c7" : "#3498db"
-                    font.bold: gameView.cursorMode !== "normal"
-                    font.pointSize: 9
-                }
-            }
-        }
-        
         MouseArea {
             id: mouseArea
             anchors.fill: parent
@@ -235,7 +163,7 @@ Item {
                     selectionBox.visible = true
                 } else if (mouse.button === Qt.RightButton) {
                     
-                    mousePanActive = true
+                    renderArea.mousePanActive = true
                     mainWindow.edgeScrollDisabled = true
 
                     if (gameView.setRallyMode) {
@@ -272,9 +200,9 @@ Item {
                     }
                 }
                 if (mouse.button === Qt.RightButton) {
-                    mousePanActive = false
-                    
-                    mainWindow.edgeScrollDisabled = (renderArea.keyPanCount > 0) || mousePanActive
+                    renderArea.mousePanActive = false
+
+                    mainWindow.edgeScrollDisabled = (renderArea.keyPanCount > 0) || renderArea.mousePanActive
                 }
             }
         }

+ 250 - 90
ui/qml/HUDTop.qml

@@ -9,26 +9,32 @@ Item {
     signal pauseToggled()
     signal speedChanged(real speed)
 
+    // --- Responsive helpers
+    readonly property int barMinHeight: 72
+    readonly property bool compact: width < 800
+    readonly property bool ultraCompact: width < 560
+
     Rectangle {
         id: topPanel
-        anchors.top: parent.top
         anchors.left: parent.left
         anchors.right: parent.right
-        height: Math.max(50, parent.height * 0.08)
+        anchors.top: parent.top
+        height: barMinHeight
         color: "#1a1a1a"
-        opacity: 0.95
+        opacity: 0.98
+        clip: true
 
-        
+        // Subtle gradient
         Rectangle {
             anchors.fill: parent
             gradient: Gradient {
-                GradientStop { position: 0.0; color: "#2c3e50" }
-                GradientStop { position: 1.0; color: "#1a252f" }
+                GradientStop { position: 0.0; color: "#22303a" }
+                GradientStop { position: 1.0; color: "#0f1a22" }
             }
-            opacity: 0.8
+            opacity: 0.9
         }
 
-        
+        // Bottom accent line
         Rectangle {
             anchors.left: parent.left
             anchors.right: parent.right
@@ -41,112 +47,266 @@ Item {
             }
         }
 
+        // === Flex-like layout: left group | spacer | right group
         RowLayout {
+            id: barRow
             anchors.fill: parent
             anchors.margins: 8
-            spacing: 15
-
-            
-            Button {
-                Layout.preferredWidth: 50
-                Layout.fillHeight: true
-                text: topRoot.gameIsPaused ? "▶" : "⏸"
-                font.pointSize: 18
-                font.bold: true
-                focusPolicy: Qt.NoFocus
-                background: Rectangle {
-                    color: parent.pressed ? "#e74c3c" : (parent.hovered ? "#c0392b" : "#34495e")
-                    radius: 4
-                    border.color: "#2c3e50"
-                    border.width: 1
-                }
-                contentItem: Text {
-                    text: parent.text
-                    font: parent.font
-                    color: "#ecf0f1"
-                    horizontalAlignment: Text.AlignHCenter
-                    verticalAlignment: Text.AlignVCenter
+            spacing: 12
+
+            // ---------- LEFT GROUP ----------
+            RowLayout {
+                id: leftGroup
+                spacing: 10
+                Layout.alignment: Qt.AlignVCenter
+
+                // Pause/Play
+                Button {
+                    id: pauseBtn
+                    Layout.preferredWidth: topRoot.compact ? 48 : 56
+                    Layout.preferredHeight: Math.min(40, topPanel.height - 12)
+                    text: topRoot.gameIsPaused ? "\u25B6" : "\u23F8" // ▶ / ⏸
+                    font.pixelSize: 26
+                    font.bold: true
+                    focusPolicy: Qt.NoFocus
+                    background: Rectangle {
+                        color: parent.pressed ? "#e74c3c"
+                              : parent.hovered ? "#c0392b" : "#34495e"
+                        radius: 6
+                        border.color: "#2c3e50"
+                        border.width: 1
+                    }
+                    contentItem: Text {
+                        text: parent.text
+                        font: parent.font
+                        color: "#ecf0f1"
+                        horizontalAlignment: Text.AlignHCenter
+                        verticalAlignment: Text.AlignVCenter
+                    }
+                    onClicked: topRoot.pauseToggled()
                 }
-                onClicked: {
-                    
-                    topRoot.pauseToggled()
+
+                // Separator
+                Rectangle {
+                    width: 2; Layout.fillHeight: true; radius: 1
+                    visible: !topRoot.compact
+                    gradient: Gradient {
+                        GradientStop { position: 0.0; color: "transparent" }
+                        GradientStop { position: 0.5; color: "#34495e" }
+                        GradientStop { position: 1.0; color: "transparent" }
+                    }
                 }
-            }
 
-            Rectangle { width: 2; Layout.fillHeight: true; Layout.topMargin: 8; Layout.bottomMargin: 8
-                gradient: Gradient { GradientStop { position: 0.0; color: "transparent" } GradientStop { position: 0.5; color: "#34495e" } GradientStop { position: 1.0; color: "transparent" } }
-            }
+                // Speed controls (buttons on wide, ComboBox on compact)
+                RowLayout {
+                    spacing: 8
+                    Layout.alignment: Qt.AlignVCenter
 
-            
-            Row {
-                Layout.preferredWidth: 220
-                spacing: 8
+                    Label {
+                        text: "Speed:"
+                        visible: !topRoot.compact
+                        color: "#ecf0f1"
+                        font.pixelSize: 14
+                        font.bold: true
+                        verticalAlignment: Text.AlignVCenter
+                    }
 
-                Text { text: "Speed:"; color: "#ecf0f1"; font.pointSize: 11; font.bold: true; anchors.verticalCenter: parent.verticalCenter }
+                    Row {
+                        id: speedRow
+                        spacing: 8
+                        visible: !topRoot.compact
 
-                ButtonGroup { id: speedGroup }
+                        property var options: [0.5, 1.0, 2.0]
+                        ButtonGroup { id: speedGroup }
 
-                Button {
-                    width: 50; height: 32; text: "0.5x"
-                    enabled: !topRoot.gameIsPaused
-                    checkable: true
-                    checked: topRoot.currentSpeed === 0.5 && !topRoot.gameIsPaused
-                    focusPolicy: Qt.NoFocus
-                    ButtonGroup.group: speedGroup
-                    background: Rectangle { color: parent.checked ? "#27ae60" : (parent.hovered ? "#34495e" : "#2c3e50"); radius: 4; border.color: parent.checked ? "#229954" : "#1a252f"; border.width: 1 }
-                    contentItem: Text { text: parent.text; font.pointSize: 9; font.bold: true; color: parent.enabled ? "#ecf0f1" : "#7f8c8d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
-                    onClicked: { topRoot.speedChanged(0.5) }
+                        Repeater {
+                            model: speedRow.options
+                            delegate: Button {
+                                Layout.minimumWidth: 48
+                                width: 56; height: Math.min(34, topPanel.height - 16)
+                                checkable: true
+                                enabled: !topRoot.gameIsPaused
+                                checked: (topRoot.currentSpeed === modelData) && !topRoot.gameIsPaused
+                                focusPolicy: Qt.NoFocus
+                                text: modelData + "x"
+                                background: Rectangle {
+                                    color: parent.checked ? "#27ae60"
+                                          : parent.hovered ? "#34495e" : "#2c3e50"
+                                    radius: 6
+                                    border.color: parent.checked ? "#229954" : "#1a252f"
+                                    border.width: 1
+                                }
+                                contentItem: Text {
+                                    text: parent.text
+                                    font.pixelSize: 13
+                                    font.bold: true
+                                    color: parent.enabled ? "#ecf0f1" : "#7f8c8d"
+                                    horizontalAlignment: Text.AlignHCenter
+                                    verticalAlignment: Text.AlignVCenter
+                                }
+                                ButtonGroup.group: speedGroup
+                                onClicked: topRoot.speedChanged(modelData)
+                            }
+                        }
+                    }
+
+                    ComboBox {
+                        id: speedCombo
+                        visible: topRoot.compact
+                        Layout.preferredWidth: 120
+                        model: ["0.5x", "1x", "2x"]
+                        currentIndex: topRoot.currentSpeed === 0.5 ? 0
+                                     : topRoot.currentSpeed === 1.0 ? 1 : 2
+                        enabled: !topRoot.gameIsPaused
+                        font.pixelSize: 13
+                        onActivated: function(i) {
+                            var v = i === 0 ? 0.5 : (i === 1 ? 1.0 : 2.0)
+                            topRoot.speedChanged(v)
+                        }
+                    }
                 }
 
-                Button { width: 50; height: 32; text: "1x"; enabled: !topRoot.gameIsPaused; checkable: true; checked: topRoot.currentSpeed === 1.0 && !topRoot.gameIsPaused; focusPolicy: Qt.NoFocus; ButtonGroup.group: speedGroup
-                    background: Rectangle { color: parent.checked ? "#27ae60" : (parent.hovered ? "#34495e" : "#2c3e50"); radius: 4; border.color: parent.checked ? "#229954" : "#1a252f"; border.width: 1 }
-                    contentItem: Text { text: parent.text; font.pointSize: 9; font.bold: true; color: parent.enabled ? "#ecf0f1" : "#7f8c8d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
-                    onClicked: { topRoot.speedChanged(1.0) }
+                // Separator
+                Rectangle {
+                    width: 2; Layout.fillHeight: true; radius: 1
+                    visible: !topRoot.compact
+                    gradient: Gradient {
+                        GradientStop { position: 0.0; color: "transparent" }
+                        GradientStop { position: 0.5; color: "#34495e" }
+                        GradientStop { position: 1.0; color: "transparent" }
+                    }
                 }
 
-                Button { width: 50; height: 32; text: "2x"; enabled: !topRoot.gameIsPaused; checkable: true; checked: topRoot.currentSpeed === 2.0 && !topRoot.gameIsPaused; focusPolicy: Qt.NoFocus; ButtonGroup.group: speedGroup
-                    background: Rectangle { color: parent.checked ? "#27ae60" : (parent.hovered ? "#34495e" : "#2c3e50"); radius: 4; border.color: parent.checked ? "#229954" : "#1a252f"; border.width: 1 }
-                    contentItem: Text { text: parent.text; font.pointSize: 9; font.bold: true; color: parent.enabled ? "#ecf0f1" : "#7f8c8d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
-                    onClicked: { topRoot.speedChanged(2.0) }
+                // Camera controls
+                RowLayout {
+                    spacing: 8
+                    Layout.alignment: Qt.AlignVCenter
+
+                    Label {
+                        text: "Camera:"
+                        visible: !topRoot.compact
+                        color: "#ecf0f1"
+                        font.pixelSize: 14
+                        font.bold: true
+                        verticalAlignment: Text.AlignVCenter
+                    }
+
+                    Button {
+                        id: followBtn
+                        Layout.preferredWidth: topRoot.compact ? 44 : 80
+                        Layout.preferredHeight: Math.min(34, topPanel.height - 16)
+                        checkable: true
+                        text: topRoot.compact ? "\u2609" : "Follow"
+                        font.pixelSize: 13
+                        focusPolicy: Qt.NoFocus
+                        background: Rectangle {
+                            color: parent.checked ? "#3498db"
+                                  : parent.hovered ? "#34495e" : "#2c3e50"
+                            radius: 6
+                            border.color: parent.checked ? "#2980b9" : "#1a252f"
+                            border.width: 1
+                        }
+                        contentItem: Text { text: parent.text; font: parent.font; color: "#ecf0f1"
+                            horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter }
+                        onToggled: { if (typeof game !== 'undefined' && game.cameraFollowSelection) game.cameraFollowSelection(checked) }
+                    }
+
+                    Button {
+                        id: resetBtn
+                        Layout.preferredWidth: topRoot.compact ? 44 : 80
+                        Layout.preferredHeight: Math.min(34, topPanel.height - 16)
+                        text: topRoot.compact ? "\u21BA" : "Reset" // ↺
+                        font.pixelSize: 13
+                        focusPolicy: Qt.NoFocus
+                        background: Rectangle {
+                            color: parent.hovered ? "#34495e" : "#2c3e50"
+                            radius: 6
+                            border.color: "#1a252f"
+                            border.width: 1
+                        }
+                        contentItem: Text { text: parent.text; font: parent.font; 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
-                gradient: Gradient { GradientStop { position: 0.0; color: "transparent" } GradientStop { position: 0.5; color: "#34495e" } GradientStop { position: 1.0; color: "transparent" } }
-            }
+            // Spacer creates "space-between"
+            Item { Layout.fillWidth: true }
 
-            
-            Row { spacing: 8
-                Text { text: "Camera:"; color: "#ecf0f1"; font.pointSize: 11; font.bold: true; anchors.verticalCenter: parent.verticalCenter }
-                Button { width: 70; height: 32; text: "Follow"; checkable: true; checked: false; focusPolicy: Qt.NoFocus
-                    background: Rectangle { color: parent.checked ? "#3498db" : (parent.hovered ? "#34495e" : "#2c3e50"); radius: 4; border.color: parent.checked ? "#2980b9" : "#1a252f"; border.width: 1 }
-                    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() }
+            // ---------- RIGHT GROUP ----------
+            RowLayout {
+                id: rightGroup
+                spacing: 12
+                Layout.alignment: Qt.AlignVCenter
+
+                // Stats side-by-side
+                Row {
+                    id: statsRow
+                    spacing: 10
+                    Layout.alignment: Qt.AlignVCenter
+
+                    Label {
+                        id: playerLbl
+                        text: "🗡️ " + (typeof game !== 'undefined' ? game.playerTroopCount : 0)
+                              + " / " + (typeof game !== 'undefined' ? game.maxTroopsPerPlayer : 0)
+                        color: {
+                            if (typeof game === 'undefined') return "#95a5a6"
+                            var count = game.playerTroopCount
+                            var max = game.maxTroopsPerPlayer
+                            if (count >= max) return "#e74c3c"
+                            if (count >= max * 0.8) return "#f39c12"
+                            return "#2ecc71"
+                        }
+                        font.pixelSize: 14
+                        font.bold: true
+                        elide: Text.ElideRight
+                        verticalAlignment: Text.AlignVCenter
+                    }
+
+                    Label {
+                        id: enemyLbl
+                        text: "💀 " + (typeof game !== 'undefined' ? game.enemyTroopsDefeated : 0)
+                        color: "#ecf0f1"
+                        font.pixelSize: 14
+                        elide: Text.ElideRight
+                        verticalAlignment: Text.AlignVCenter
+                    }
                 }
-            }
 
-            Rectangle { width: 2; Layout.fillHeight: true; Layout.topMargin: 8; Layout.bottomMargin: 8
-                gradient: Gradient { GradientStop { position: 0.0; color: "transparent" } GradientStop { position: 0.5; color: "#34495e" } GradientStop { position: 1.0; color: "transparent" } }
-            }
+                // Minimap (shrinks/hides on very small widths to prevent overflow)
+                Item {
+                    id: miniWrap
+                    visible: !topRoot.ultraCompact
+                    Layout.preferredWidth: Math.round( topPanel.height * 2.2 )
+                    Layout.minimumWidth: Math.round( topPanel.height * 1.6 )
+                    Layout.preferredHeight: topPanel.height - 8
 
-            
-            Text { text: "🗡️ " + (typeof game !== 'undefined' ? game.playerTroopCount : 0) + " / " + (typeof game !== 'undefined' ? game.maxTroopsPerPlayer : 0)
-                color: { if (typeof game === 'undefined') return "#95a5a6"; var count = game.playerTroopCount; var max = game.maxTroopsPerPlayer; if (count >= max) return "#e74c3c"; if (count >= max * 0.8) return "#f39c12"; return "#2ecc71" }
-                font.pointSize: 11; font.bold: true }
+                    Rectangle {
+                        anchors.fill: parent
+                        color: "#0f1a22"
+                        radius: 8
+                        border.width: 2
+                        border.color: "#3498db"
 
-            Item { Layout.fillWidth: true }
+                        Rectangle {
+                            anchors.fill: parent
+                            anchors.margins: 3
+                            radius: 6
+                            color: "#0a0f14"
 
-            
-            Rectangle { Layout.preferredWidth: Math.min(140, parent.width * 0.12); Layout.fillHeight: true; Layout.topMargin: 4; Layout.bottomMargin: 4; color: "#1a252f"; border.width: 2; border.color: "#3498db"; radius: 4
-                Rectangle { anchors.fill: parent; anchors.margins: 2; color: "#0a0f14"
-                    Text { anchors.centerIn: parent; text: "MINIMAP"; color: "#34495e"; font.pointSize: 9; font.bold: true }
+                            // placeholder content; replace with live minimap
+                            Label {
+                                anchors.centerIn: parent
+                                text: "MINIMAP"
+                                color: "#3f5362"
+                                font.pixelSize: 12
+                                font.bold: true
+                            }
+                        }
+                    }
                 }
             }
         }
     }
-}
+}