Преглед на файлове

Extend make format to include QML and shader files, add CONTRIBUTING.md

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] преди 2 месеца
родител
ревизия
7d5928bff0
променени са 65 файла, в които са добавени 3158 реда и са изтрити 2252 реда
  1. 175 0
      CONTRIBUTING.md
  2. 53 8
      Makefile
  3. 3 3
      app/controllers/action_vfx.cpp
  4. 3 3
      app/controllers/action_vfx.h
  5. 19 17
      app/controllers/command_controller.cpp
  6. 12 12
      app/controllers/command_controller.h
  7. 44 49
      app/game_engine.cpp
  8. 9 9
      app/game_engine.h
  9. 10 7
      app/hover_tracker.h
  10. 2 2
      app/utils/engine_view_helpers.h
  11. 2 2
      app/utils/movement_utils.h
  12. 2 2
      app/utils/selection_utils.h
  13. 9 9
      assets/shaders/basic.frag
  14. 7 7
      assets/shaders/basic.vert
  15. 5 5
      assets/shaders/cylinder_instanced.frag
  16. 33 32
      assets/shaders/cylinder_instanced.vert
  17. 5 5
      assets/shaders/fog_instanced.frag
  18. 8 8
      assets/shaders/fog_instanced.vert
  19. 3 3
      assets/shaders/grass_instanced.frag
  20. 33 35
      assets/shaders/grass_instanced.vert
  21. 28 25
      assets/shaders/grid.frag
  22. 66 49
      assets/shaders/ground_plane.frag
  23. 41 20
      assets/shaders/ground_plane.vert
  24. 13 13
      assets/shaders/stone_instanced.frag
  25. 24 24
      assets/shaders/stone_instanced.vert
  26. 237 210
      assets/shaders/terrain_chunk.frag
  27. 48 46
      assets/shaders/terrain_chunk.vert
  28. 4 4
      game/map/environment.h
  29. 2 2
      game/map/level_loader.cpp
  30. 19 23
      game/map/map_catalog.cpp
  31. 9 10
      game/map/map_catalog.h
  32. 15 15
      game/map/skirmish_loader.cpp
  33. 17 15
      game/map/skirmish_loader.h
  34. 1 1
      game/map/terrain_service.cpp
  35. 1 1
      game/map/terrain_service.h
  36. 7 7
      game/map/world_bootstrap.cpp
  37. 5 5
      game/map/world_bootstrap.h
  38. 1 1
      game/systems/ai_system/ai_reasoner.cpp
  39. 2 4
      game/systems/ai_system/behaviors/gather_behavior.cpp
  40. 3 3
      game/systems/camera_service.cpp
  41. 1 1
      game/systems/camera_service.h
  42. 3 4
      game/systems/formation_system.cpp
  43. 8 8
      game/systems/formation_system.h
  44. 2 2
      game/systems/production_service.cpp
  45. 13 14
      game/systems/selection_system.cpp
  46. 3 2
      game/systems/selection_system.h
  47. 1 2
      game/systems/troop_count_registry.cpp
  48. 1 1
      game/systems/troop_count_registry.h
  49. 1 1
      game/visuals/team_colors.h
  50. 1 1
      game/visuals/visual_catalog.cpp
  51. 2 2
      game/visuals/visual_catalog.h
  52. 116 151
      ui/qml/CursorManager.qml
  53. 338 397
      ui/qml/GameView.qml
  54. 60 54
      ui/qml/HUD.qml
  55. 319 48
      ui/qml/HUDBottom.qml
  56. 200 82
      ui/qml/HUDTop.qml
  57. 6 3
      ui/qml/HUDVictory.qml
  58. 172 158
      ui/qml/Main.qml
  59. 131 63
      ui/qml/MainMenu.qml
  60. 68 38
      ui/qml/MapListPanel.qml
  61. 399 248
      ui/qml/MapSelect.qml
  62. 124 70
      ui/qml/PlayerConfigPanel.qml
  63. 30 18
      ui/qml/PlayerListItem.qml
  64. 106 150
      ui/qml/StyleGuide.qml
  65. 73 38
      ui/qml/StyledButton.qml

+ 175 - 0
CONTRIBUTING.md

@@ -0,0 +1,175 @@
+# Contributing to Standard of Iron
+
+Thank you for your interest in contributing to Standard of Iron! This document provides guidelines and information to help you contribute effectively.
+
+## Development Setup
+
+### Prerequisites
+
+To build and develop Standard of Iron, you'll need:
+
+- **CMake** >= 3.21.0
+- **GCC/G++** >= 10.0.0 or equivalent C++20 compiler
+- **Qt5** or **Qt6** (Qt6 is preferred)
+  - Qt Core, Widgets, OpenGL, Quick, Qml, QuickControls2
+- **OpenGL** 3.3+ support
+
+### Installation
+
+Run the automated setup:
+```bash
+make install
+```
+
+This will install all required dependencies on Ubuntu/Debian-based systems.
+
+## Code Formatting
+
+We maintain consistent code style across the entire codebase using automated formatting tools.
+
+### Required Tools
+
+1. **clang-format** (required)
+   - Formats C/C++ source files (`.cpp`, `.h`, `.hpp`)
+   - Formats GLSL shader files (`.frag`, `.vert`)
+   - Installed automatically with `make install`
+
+2. **qmlformat** (optional but recommended)
+   - Formats QML files (`.qml`)
+   - Part of Qt development tools
+   - Installed with `qtdeclarative5-dev-tools` (Qt5) or `qt6-declarative-dev-tools` (Qt6)
+
+### Formatting Commands
+
+Format all code before committing:
+```bash
+make format
+```
+
+This will:
+1. Strip comments from C/C++ files
+2. Format C/C++ files with clang-format
+3. Format QML files with qmlformat (if available)
+4. Format shader files with clang-format
+
+Check if code is properly formatted (CI-friendly):
+```bash
+make format-check
+```
+
+### File Types Covered
+
+- **C/C++ files**: `.cpp`, `.c`, `.h`, `.hpp`
+- **QML files**: `.qml`
+- **Shader files**: `.frag`, `.vert`
+
+### Installing qmlformat
+
+#### Ubuntu/Debian (Qt5)
+```bash
+sudo apt-get install qtdeclarative5-dev-tools
+```
+
+#### Ubuntu/Debian (Qt6)
+```bash
+sudo apt-get install qt6-declarative-dev-tools
+```
+
+The `qmlformat` binary will be installed to:
+- Qt5: `/usr/lib/qt5/bin/qmlformat`
+- Qt6: `/usr/lib/qt6/bin/qmlformat`
+
+The Makefile automatically detects qmlformat in these locations.
+
+## Building the Project
+
+### Standard Build
+```bash
+make build
+```
+
+### Debug Build
+```bash
+make debug
+```
+
+### Release Build
+```bash
+make release
+```
+
+### Clean Build
+```bash
+make rebuild
+```
+
+## Running the Application
+
+### Run the Game
+```bash
+make run
+```
+
+### Run the Map Editor
+```bash
+make editor
+```
+
+### Run in Headless Mode (for CI)
+```bash
+make run-headless
+```
+
+## Testing
+
+Run tests (when implemented):
+```bash
+make test
+```
+
+## Code Style Guidelines
+
+### C++ Style
+- Follow the `.clang-format` configuration
+- Use C++20 features appropriately
+- 4-space indentation (no tabs)
+- 88 character line limit
+- Place braces on the same line (`Attach` style)
+
+### QML Style
+- Use qmlformat's default style
+- Consistent property ordering
+- Proper indentation for nested elements
+
+### Shader Style
+- Use clang-format for consistent indentation
+- Follow GLSL naming conventions
+- Comment complex shader operations
+
+## Commit Guidelines
+
+1. **Format your code**: Always run `make format` before committing
+2. **Build successfully**: Ensure `make build` completes without errors
+3. **Test your changes**: Run relevant tests
+4. **Write clear commit messages**: Describe what and why, not just how
+
+## Pull Request Process
+
+1. Fork the repository
+2. Create a feature branch (`git checkout -b feature/amazing-feature`)
+3. Make your changes
+4. Format your code (`make format`)
+5. Commit your changes (`git commit -m 'Add amazing feature'`)
+6. Push to the branch (`git push origin feature/amazing-feature`)
+7. Open a Pull Request
+
+## Questions or Issues?
+
+If you have questions or encounter issues:
+- Open an issue on GitHub
+- Check existing issues and discussions
+- Review the README.md for additional information
+
+## License
+
+By contributing to Standard of Iron, you agree that your contributions will be licensed under the same license as the project.

+ 53 - 8
Makefile

@@ -11,7 +11,11 @@ MAP_EDITOR_BINARY := map_editor
 
 # Formatting config
 CLANG_FORMAT ?= clang-format
+# Try to find qmlformat in common Qt installation paths if not in PATH
+QMLFORMAT ?= $(shell command -v qmlformat 2>/dev/null || echo /usr/lib/qt5/bin/qmlformat)
 FMT_GLOBS := -name "*.cpp" -o -name "*.c" -o -name "*.h" -o -name "*.hpp"
+SHADER_GLOBS := -name "*.frag" -o -name "*.vert"
+QML_GLOBS := -name "*.qml"
 
 # Colors for output
 BOLD := \033[1m
@@ -35,7 +39,7 @@ help:
 	@echo "  $(GREEN)clean$(RESET)         - Clean build directory"
 	@echo "  $(GREEN)rebuild$(RESET)       - Clean and build"
 	@echo "  $(GREEN)test$(RESET)          - Run tests (if any)"
-	@echo "  $(GREEN)format$(RESET)        - Strip comments then clang-format (strict)"
+	@echo "  $(GREEN)format$(RESET)        - Format all code (C++, QML, shaders)"
 	@echo "  $(GREEN)format-check$(RESET)  - Verify formatting (CI-friendly, no changes)"
 	@echo "  $(GREEN)check-deps$(RESET)    - Check if dependencies are installed"
 	@echo "  $(GREEN)dev$(RESET)           - Set up development environment (install + configure + build)"
@@ -138,7 +142,7 @@ test: build
 		echo "$(YELLOW)No tests found. Test suite not yet implemented.$(RESET)"; \
 	fi
 
-# ---- Formatting: strip comments first, then clang-format (strict) ----
+# ---- Formatting: strip comments first, then format (strict) ----
 .PHONY: format format-check
 format:
 	@echo "$(BOLD)$(BLUE)Stripping comments in app/... game/... render/... tools/... ui/...$(RESET)"
@@ -149,24 +153,65 @@ format:
 	else \
 		echo "$(RED)scripts/remove-comments.sh not found$(RESET)"; exit 1; \
 	fi
-	@echo "$(BOLD)$(BLUE)Formatting with clang-format (strict)...$(RESET)"
+	@echo "$(BOLD)$(BLUE)Formatting C/C++ files with clang-format...$(RESET)"
 	@if command -v $(CLANG_FORMAT) >/dev/null 2>&1; then \
 		find . -type f \( $(FMT_GLOBS) \) -not -path "./$(BUILD_DIR)/*" -print0 \
 		| xargs -0 -r $(CLANG_FORMAT) -i --style=file; \
-		echo "$(GREEN)✓ Format + comment strip complete$(RESET)"; \
+		echo "$(GREEN)✓ C/C++ formatting complete$(RESET)"; \
 	else \
 		echo "$(RED)clang-format not found. Please install it.$(RESET)"; exit 1; \
 	fi
+	@echo "$(BOLD)$(BLUE)Formatting QML files...$(RESET)"
+	@if command -v $(QMLFORMAT) >/dev/null 2>&1 || [ -x "$(QMLFORMAT)" ]; then \
+		find . -type f \( $(QML_GLOBS) \) -not -path "./$(BUILD_DIR)/*" -print0 \
+		| xargs -0 -r $(QMLFORMAT) -i; \
+		echo "$(GREEN)✓ QML formatting complete$(RESET)"; \
+	else \
+		echo "$(YELLOW)⚠ qmlformat not found. Skipping QML formatting.$(RESET)"; \
+		echo "$(YELLOW)  Install qmlformat (from Qt dev tools) to format QML files.$(RESET)"; \
+	fi
+	@echo "$(BOLD)$(BLUE)Formatting shader files (.frag, .vert)...$(RESET)"
+	@if command -v $(CLANG_FORMAT) >/dev/null 2>&1; then \
+		find . -type f \( $(SHADER_GLOBS) \) -not -path "./$(BUILD_DIR)/*" -print0 \
+		| xargs -0 -r $(CLANG_FORMAT) -i --style=file; \
+		echo "$(GREEN)✓ Shader formatting complete$(RESET)"; \
+	else \
+		echo "$(YELLOW)⚠ clang-format not found. Shader files not formatted.$(RESET)"; \
+	fi
+	@echo "$(GREEN)✓ All formatting complete$(RESET)"
 
 # CI/verification: fail if anything would be reformatted
 format-check:
-	@echo "$(BOLD)$(BLUE)Checking clang-format compliance...$(RESET)"
-	@if command -v $(CLANG_FORMAT) >/dev/null 2>&1; then \
+	@echo "$(BOLD)$(BLUE)Checking formatting compliance...$(RESET)"
+	@FAILED=0; \
+	if command -v $(CLANG_FORMAT) >/dev/null 2>&1; then \
+		echo "$(BLUE)Checking C/C++ files...$(RESET)"; \
 		find . -type f \( $(FMT_GLOBS) \) -not -path "./$(BUILD_DIR)/*" -print0 \
-		| xargs -0 -r $(CLANG_FORMAT) --dry-run -Werror --style=file; \
-		echo "$(GREEN)✓ Formatting OK$(RESET)"; \
+		| xargs -0 -r $(CLANG_FORMAT) --dry-run -Werror --style=file || FAILED=1; \
+		echo "$(BLUE)Checking shader files...$(RESET)"; \
+		find . -type f \( $(SHADER_GLOBS) \) -not -path "./$(BUILD_DIR)/*" -print0 \
+		| xargs -0 -r $(CLANG_FORMAT) --dry-run -Werror --style=file || FAILED=1; \
 	else \
 		echo "$(RED)clang-format not found. Please install it.$(RESET)"; exit 1; \
+	fi; \
+	if command -v $(QMLFORMAT) >/dev/null 2>&1 || [ -x "$(QMLFORMAT)" ]; then \
+		echo "$(BLUE)Checking QML files...$(RESET)"; \
+		for file in $$(find . -type f \( $(QML_GLOBS) \) -not -path "./$(BUILD_DIR)/*"); do \
+			$(QMLFORMAT) "$$file" > /tmp/qmlformat_check.tmp 2>/dev/null; \
+			if ! diff -q "$$file" /tmp/qmlformat_check.tmp >/dev/null 2>&1; then \
+				echo "$(RED)QML file needs formatting: $$file$(RESET)"; \
+				FAILED=1; \
+			fi; \
+		done; \
+		rm -f /tmp/qmlformat_check.tmp; \
+	else \
+		echo "$(YELLOW)⚠ qmlformat not found. Skipping QML format check.$(RESET)"; \
+	fi; \
+	if [ $$FAILED -eq 0 ]; then \
+		echo "$(GREEN)✓ All formatting checks passed$(RESET)"; \
+	else \
+		echo "$(RED)✗ Formatting check failed. Run 'make format' to fix.$(RESET)"; \
+		exit 1; \
 	fi
 
 # Debug build

+ 3 - 3
app/controllers/action_vfx.cpp

@@ -8,7 +8,7 @@
 namespace App::Controllers {
 
 void ActionVFX::spawnAttackArrow(Engine::Core::World *world,
-                                Engine::Core::EntityID targetId) {
+                                 Engine::Core::EntityID targetId) {
   if (!world)
     return;
 
@@ -30,7 +30,7 @@ void ActionVFX::spawnAttackArrow(Engine::Core::World *world,
   QVector3D aboveTarget = targetPos + QVector3D(0, 2.0f, 0);
 
   arrowSystem->spawnArrow(aboveTarget, targetPos, QVector3D(1.0f, 0.2f, 0.2f),
-                         Game::GameConfig::instance().arrow().speedAttack);
+                          Game::GameConfig::instance().arrow().speedAttack);
 }
 
-} 
+} // namespace App::Controllers

+ 3 - 3
app/controllers/action_vfx.h

@@ -6,8 +6,8 @@ namespace Engine {
 namespace Core {
 class World;
 using EntityID = unsigned int;
-}
-} 
+} // namespace Core
+} // namespace Engine
 
 namespace App::Controllers {
 
@@ -17,4 +17,4 @@ public:
                                Engine::Core::EntityID targetId);
 };
 
-} 
+} // namespace App::Controllers

+ 19 - 17
app/controllers/command_controller.cpp

@@ -13,15 +13,15 @@
 namespace App::Controllers {
 
 CommandController::CommandController(
-    Engine::Core::World *world,
-    Game::Systems::SelectionSystem *selectionSystem,
+    Engine::Core::World *world, Game::Systems::SelectionSystem *selectionSystem,
     Game::Systems::PickingService *pickingService, QObject *parent)
     : QObject(parent), m_world(world), m_selectionSystem(selectionSystem),
       m_pickingService(pickingService) {}
 
 CommandResult CommandController::onAttackClick(qreal sx, qreal sy,
-                                              int viewportWidth,
-                                              int viewportHeight, void *camera) {
+                                               int viewportWidth,
+                                               int viewportHeight,
+                                               void *camera) {
   CommandResult result;
   if (!m_selectionSystem || !m_pickingService || !camera || !m_world) {
     result.resetCursorToNormal = true;
@@ -94,8 +94,9 @@ CommandResult CommandController::onStopCommand() {
 }
 
 CommandResult CommandController::onPatrolClick(qreal sx, qreal sy,
-                                              int viewportWidth,
-                                              int viewportHeight, void *camera) {
+                                               int viewportWidth,
+                                               int viewportHeight,
+                                               void *camera) {
   CommandResult result;
   if (!m_selectionSystem || !m_world || !m_pickingService || !camera) {
     if (m_hasPatrolFirstWaypoint) {
@@ -117,7 +118,7 @@ CommandResult CommandController::onPatrolClick(qreal sx, qreal sy,
   auto *cam = static_cast<Render::GL::Camera *>(camera);
   QVector3D hit;
   if (!m_pickingService->screenToGround(QPointF(sx, sy), *cam, viewportWidth,
-                                       viewportHeight, hit)) {
+                                        viewportHeight, hit)) {
     if (m_hasPatrolFirstWaypoint) {
       clearPatrolFirstWaypoint();
       result.resetCursorToNormal = true;
@@ -168,10 +169,10 @@ CommandResult CommandController::onPatrolClick(qreal sx, qreal sy,
 }
 
 CommandResult CommandController::setRallyAtScreen(qreal sx, qreal sy,
-                                                 int viewportWidth,
-                                                 int viewportHeight,
-                                                 void *camera,
-                                                 int localOwnerId) {
+                                                  int viewportWidth,
+                                                  int viewportHeight,
+                                                  void *camera,
+                                                  int localOwnerId) {
   CommandResult result;
   if (!m_world || !m_selectionSystem || !m_pickingService || !camera)
     return result;
@@ -179,7 +180,7 @@ CommandResult CommandController::setRallyAtScreen(qreal sx, qreal sy,
   auto *cam = static_cast<Render::GL::Camera *>(camera);
   QVector3D hit;
   if (!m_pickingService->screenToGround(QPointF(sx, sy), *cam, viewportWidth,
-                                       viewportHeight, hit))
+                                        viewportHeight, hit))
     return result;
 
   Game::Systems::ProductionService::setRallyForFirstSelectedBarracks(
@@ -191,7 +192,7 @@ CommandResult CommandController::setRallyAtScreen(qreal sx, qreal sy,
 }
 
 void CommandController::recruitNearSelected(const QString &unitType,
-                                           int localOwnerId) {
+                                            int localOwnerId) {
   if (!m_world || !m_selectionSystem)
     return;
 
@@ -199,9 +200,10 @@ void CommandController::recruitNearSelected(const QString &unitType,
   if (sel.empty())
     return;
 
-  auto result = Game::Systems::ProductionService::startProductionForFirstSelectedBarracks(
-      *m_world, sel, localOwnerId, unitType.toStdString());
-  
+  auto result =
+      Game::Systems::ProductionService::startProductionForFirstSelectedBarracks(
+          *m_world, sel, localOwnerId, unitType.toStdString());
+
   if (result == Game::Systems::ProductionResult::GlobalTroopLimitReached) {
     emit troopLimitReached();
   }
@@ -211,4 +213,4 @@ void CommandController::resetMovement(Engine::Core::Entity *entity) {
   App::Utils::resetMovement(entity);
 }
 
-} 
+} // namespace App::Controllers

+ 12 - 12
app/controllers/command_controller.h

@@ -1,8 +1,8 @@
 #pragma once
 
 #include <QObject>
-#include <QVector3D>
 #include <QString>
+#include <QVector3D>
 #include <vector>
 
 namespace Engine {
@@ -10,13 +10,13 @@ namespace Core {
 class World;
 class Entity;
 using EntityID = unsigned int;
-}
-} 
+} // namespace Core
+} // namespace Engine
 
 namespace Game::Systems {
 class SelectionSystem;
 class PickingService;
-}
+} // namespace Game::Systems
 
 namespace App::Controllers {
 
@@ -29,18 +29,18 @@ class CommandController : public QObject {
   Q_OBJECT
 public:
   CommandController(Engine::Core::World *world,
-                   Game::Systems::SelectionSystem *selectionSystem,
-                   Game::Systems::PickingService *pickingService,
-                   QObject *parent = nullptr);
+                    Game::Systems::SelectionSystem *selectionSystem,
+                    Game::Systems::PickingService *pickingService,
+                    QObject *parent = nullptr);
 
   CommandResult onAttackClick(qreal sx, qreal sy, int viewportWidth,
-                             int viewportHeight, void *camera);
+                              int viewportHeight, void *camera);
   CommandResult onStopCommand();
   CommandResult onPatrolClick(qreal sx, qreal sy, int viewportWidth,
-                             int viewportHeight, void *camera);
+                              int viewportHeight, void *camera);
   CommandResult setRallyAtScreen(qreal sx, qreal sy, int viewportWidth,
-                                int viewportHeight, void *camera,
-                                int localOwnerId);
+                                 int viewportHeight, void *camera,
+                                 int localOwnerId);
   void recruitNearSelected(const QString &unitType, int localOwnerId);
 
   bool hasPatrolFirstWaypoint() const { return m_hasPatrolFirstWaypoint; }
@@ -62,4 +62,4 @@ private:
   void resetMovement(Engine::Core::Entity *entity);
 };
 
-} 
+} // namespace App::Controllers

+ 44 - 49
app/game_engine.cpp

@@ -103,28 +103,27 @@ GameEngine::GameEngine() {
   m_cameraService = std::make_unique<Game::Systems::CameraService>();
 
   auto *selectionSystem = m_world->getSystem<Game::Systems::SelectionSystem>();
-  m_selectionController =
-      std::make_unique<Game::Systems::SelectionController>(
-          m_world.get(), selectionSystem, m_pickingService.get());
+  m_selectionController = std::make_unique<Game::Systems::SelectionController>(
+      m_world.get(), selectionSystem, m_pickingService.get());
   m_commandController = std::make_unique<App::Controllers::CommandController>(
       m_world.get(), selectionSystem, m_pickingService.get());
 
   m_cursorManager = std::make_unique<CursorManager>();
   m_hoverTracker = std::make_unique<HoverTracker>(m_pickingService.get());
-  
-  
+
   m_mapCatalog = std::make_unique<Game::Map::MapCatalog>();
-  connect(m_mapCatalog.get(), &Game::Map::MapCatalog::mapLoaded, this, [this](QVariantMap mapData) {
-    m_availableMaps.append(mapData);
-    emit availableMapsChanged();
-  });
-  connect(m_mapCatalog.get(), &Game::Map::MapCatalog::loadingChanged, this, [this](bool loading) {
-    m_mapsLoading = loading;
-    emit mapsLoadingChanged();
-  });
-  connect(m_mapCatalog.get(), &Game::Map::MapCatalog::allMapsLoaded, this, [this]() {
-    emit availableMapsChanged();
-  });
+  connect(m_mapCatalog.get(), &Game::Map::MapCatalog::mapLoaded, this,
+          [this](QVariantMap mapData) {
+            m_availableMaps.append(mapData);
+            emit availableMapsChanged();
+          });
+  connect(m_mapCatalog.get(), &Game::Map::MapCatalog::loadingChanged, this,
+          [this](bool loading) {
+            m_mapsLoading = loading;
+            emit mapsLoadingChanged();
+          });
+  connect(m_mapCatalog.get(), &Game::Map::MapCatalog::allMapsLoaded, this,
+          [this]() { emit availableMapsChanged(); });
 
   connect(m_cursorManager.get(), &CursorManager::modeChanged, this,
           &GameEngine::cursorModeChanged);
@@ -138,8 +137,7 @@ GameEngine::GameEngine() {
           &Game::Systems::SelectionController::selectionModelRefreshRequested,
           this, &GameEngine::selectedUnitsDataChanged);
   connect(m_commandController.get(),
-          &App::Controllers::CommandController::attackTargetSelected,
-          [this]() {
+          &App::Controllers::CommandController::attackTargetSelected, [this]() {
             if (auto *selSys =
                     m_world->getSystem<Game::Systems::SelectionSystem>()) {
               const auto &sel = selSys->getSelectedUnits();
@@ -152,16 +150,15 @@ GameEngine::GameEngine() {
                       m_viewport.height, 0);
                   if (targetId != 0) {
                     App::Controllers::ActionVFX::spawnAttackArrow(m_world.get(),
-                                                                 targetId);
+                                                                  targetId);
                   }
                 }
               }
             }
           });
-  
+
   connect(m_commandController.get(),
-          &App::Controllers::CommandController::troopLimitReached,
-          [this]() {
+          &App::Controllers::CommandController::troopLimitReached, [this]() {
             setError("Maximum troop limit reached. Cannot produce more units.");
           });
 
@@ -201,8 +198,8 @@ void GameEngine::onMapClicked(qreal sx, qreal sy) {
   ensureInitialized();
   if (m_selectionController && m_camera) {
     m_selectionController->onClickSelect(sx, sy, false, m_viewport.width,
-                                        m_viewport.height, m_camera.get(),
-                                        m_runtime.localOwnerId);
+                                         m_viewport.height, m_camera.get(),
+                                         m_runtime.localOwnerId);
   }
 }
 
@@ -257,7 +254,7 @@ void GameEngine::onAttackClick(qreal sx, qreal sy) {
             targetEntity->getComponent<Engine::Core::UnitComponent>();
         if (targetUnit && targetUnit->ownerId != m_runtime.localOwnerId) {
           App::Controllers::ActionVFX::spawnAttackArrow(m_world.get(),
-                                                       targetId);
+                                                        targetId);
         }
       }
     }
@@ -356,8 +353,8 @@ void GameEngine::onClickSelect(qreal sx, qreal sy, bool additive) {
   ensureInitialized();
   if (m_selectionController && m_camera) {
     m_selectionController->onClickSelect(sx, sy, additive, m_viewport.width,
-                                        m_viewport.height, m_camera.get(),
-                                        m_runtime.localOwnerId);
+                                         m_viewport.height, m_camera.get(),
+                                         m_runtime.localOwnerId);
   }
 }
 
@@ -367,9 +364,9 @@ void GameEngine::onAreaSelected(qreal x1, qreal y1, qreal x2, qreal y2,
     return;
   ensureInitialized();
   if (m_selectionController && m_camera) {
-    m_selectionController->onAreaSelected(x1, y1, x2, y2, additive,
-                                         m_viewport.width, m_viewport.height,
-                                         m_camera.get(), m_runtime.localOwnerId);
+    m_selectionController->onAreaSelected(
+        x1, y1, x2, y2, additive, m_viewport.width, m_viewport.height,
+        m_camera.get(), m_runtime.localOwnerId);
   }
 }
 
@@ -382,9 +379,8 @@ void GameEngine::selectAllTroops() {
 
 void GameEngine::ensureInitialized() {
   QString error;
-  Game::Map::WorldBootstrap::ensureInitialized(m_runtime.initialized,
-                                               *m_renderer, *m_camera,
-                                               m_ground.get(), &error);
+  Game::Map::WorldBootstrap::ensureInitialized(
+      m_runtime.initialized, *m_renderer, *m_camera, m_ground.get(), &error);
   if (!error.isEmpty()) {
     setError(error);
   }
@@ -451,7 +447,8 @@ void GameEngine::update(float dt) {
   }
 
   if (m_followSelectionEnabled && m_camera && m_world && m_cameraService) {
-    m_cameraService->updateFollow(*m_camera, *m_world, m_followSelectionEnabled);
+    m_cameraService->updateFollow(*m_camera, *m_world,
+                                  m_followSelectionEnabled);
   }
 
   if (m_selectedUnitsModel) {
@@ -539,8 +536,8 @@ bool GameEngine::screenToGround(const QPointF &screenPt, QVector3D &outWorld) {
 bool GameEngine::worldToScreen(const QVector3D &world,
                                QPointF &outScreen) const {
   return App::Utils::worldToScreen(m_pickingService.get(), m_camera.get(),
-                                   m_window, m_viewport.width, m_viewport.height,
-                                   world, outScreen);
+                                   m_window, m_viewport.width,
+                                   m_viewport.height, world, outScreen);
 }
 
 void GameEngine::syncSelectionFlags() {
@@ -747,8 +744,8 @@ void GameEngine::setRallyAtScreen(qreal sx, qreal sy) {
   if (!m_commandController || !m_camera)
     return;
   m_commandController->setRallyAtScreen(sx, sy, m_viewport.width,
-                                       m_viewport.height, m_camera.get(),
-                                       m_runtime.localOwnerId);
+                                        m_viewport.height, m_camera.get(),
+                                        m_runtime.localOwnerId);
 }
 
 void GameEngine::startLoadingMaps() {
@@ -758,9 +755,7 @@ void GameEngine::startLoadingMaps() {
   }
 }
 
-QVariantList GameEngine::availableMaps() const {
-  return m_availableMaps;
-}
+QVariantList GameEngine::availableMaps() const { return m_availableMaps; }
 
 void GameEngine::startSkirmish(const QString &mapPath,
                                const QVariantList &playerConfigs) {
@@ -793,17 +788,17 @@ void GameEngine::startSkirmish(const QString &mapPath,
     loader.setFogRenderer(m_fog.get());
     loader.setStoneRenderer(m_stone.get());
 
-    loader.setOnOwnersUpdated([this]() {
-      emit ownerInfoChanged();
-    });
+    loader.setOnOwnersUpdated([this]() { emit ownerInfoChanged(); });
 
     loader.setOnVisibilityMaskReady([this]() {
-      m_runtime.visibilityVersion = Game::Map::VisibilityService::instance().version();
+      m_runtime.visibilityVersion =
+          Game::Map::VisibilityService::instance().version();
       m_runtime.visibilityUpdateAccumulator = 0.0f;
     });
 
     int updatedPlayerId = m_selectedPlayerId;
-    auto result = loader.start(mapPath, playerConfigs, m_selectedPlayerId, updatedPlayerId);
+    auto result = loader.start(mapPath, playerConfigs, m_selectedPlayerId,
+                               updatedPlayerId);
 
     if (updatedPlayerId != m_selectedPlayerId) {
       m_selectedPlayerId = updatedPlayerId;
@@ -821,8 +816,9 @@ void GameEngine::startSkirmish(const QString &mapPath,
     m_level.camNear = result.camNear;
     m_level.camFar = result.camFar;
     m_level.maxTroopsPerPlayer = result.maxTroopsPerPlayer;
-    
-    Game::GameConfig::instance().setMaxTroopsPerPlayer(result.maxTroopsPerPlayer);
+
+    Game::GameConfig::instance().setMaxTroopsPerPlayer(
+        result.maxTroopsPerPlayer);
 
     if (m_victoryService) {
       m_victoryService->configure(result.victoryConfig, m_runtime.localOwnerId);
@@ -1007,4 +1003,3 @@ QVector3D GameEngine::getPatrolPreviewWaypoint() const {
     return QVector3D();
   return m_commandController->getPatrolFirstWaypoint();
 }
-

+ 9 - 9
app/game_engine.h

@@ -24,8 +24,8 @@ using EntityID = unsigned int;
 struct MovementComponent;
 struct TransformComponent;
 struct RenderableComponent;
-} 
-} 
+} // namespace Core
+} // namespace Engine
 
 namespace Render {
 namespace GL {
@@ -37,8 +37,8 @@ class TerrainRenderer;
 class BiomeRenderer;
 class FogRenderer;
 class StoneRenderer;
-} 
-} 
+} // namespace GL
+} // namespace Render
 
 namespace Game {
 namespace Systems {
@@ -48,17 +48,17 @@ class ArrowSystem;
 class PickingService;
 class VictoryService;
 class CameraService;
-} 
+} // namespace Systems
 namespace Map {
 class MapCatalog;
-} 
-} 
+}
+} // namespace Game
 
 namespace App {
 namespace Controllers {
 class CommandController;
 }
-} 
+} // namespace App
 
 class QQuickWindow;
 
@@ -139,7 +139,7 @@ public:
     }
   }
   QString lastError() const { return m_runtime.lastError; }
-  Q_INVOKABLE void clearError() { 
+  Q_INVOKABLE void clearError() {
     if (!m_runtime.lastError.isEmpty()) {
       m_runtime.lastError = "";
       emit lastErrorChanged();

+ 10 - 7
app/hover_tracker.h

@@ -7,24 +7,27 @@ namespace Engine {
 namespace Core {
 class World;
 using EntityID = unsigned int;
-} 
-} 
+} // namespace Core
+} // namespace Engine
 
 namespace Render {
 namespace GL {
 class Camera;
 }
-} 
+} // namespace Render
 
 class HoverTracker {
 public:
   HoverTracker(Game::Systems::PickingService *pickingService);
 
-  Engine::Core::EntityID updateHover(float sx, float sy, Engine::Core::World &world,
-                                      const Render::GL::Camera &camera,
-                                      int viewportWidth, int viewportHeight);
+  Engine::Core::EntityID updateHover(float sx, float sy,
+                                     Engine::Core::World &world,
+                                     const Render::GL::Camera &camera,
+                                     int viewportWidth, int viewportHeight);
 
-  Engine::Core::EntityID getLastHoveredEntity() const { return m_hoveredEntityId; }
+  Engine::Core::EntityID getLastHoveredEntity() const {
+    return m_hoveredEntityId;
+  }
 
 private:
   Game::Systems::PickingService *m_pickingService;

+ 2 - 2
app/utils/engine_view_helpers.h

@@ -33,5 +33,5 @@ inline bool worldToScreen(const Game::Systems::PickingService *pickingService,
   return pickingService->worldToScreen(*camera, w, h, world, outScreen);
 }
 
-} 
-} 
+} // namespace Utils
+} // namespace App

+ 2 - 2
app/utils/movement_utils.h

@@ -33,5 +33,5 @@ inline void resetMovement(Engine::Core::Entity *entity) {
   }
 }
 
-} 
-} 
+} // namespace Utils
+} // namespace App

+ 2 - 2
app/utils/selection_utils.h

@@ -33,5 +33,5 @@ inline void sanitizeSelection(Engine::Core::World *world,
   }
 }
 
-} 
-} 
+} // namespace Utils
+} // namespace App

+ 9 - 9
assets/shaders/basic.frag

@@ -12,13 +12,13 @@ uniform float u_alpha;
 out vec4 FragColor;
 
 void main() {
-    vec3 color = u_color;
-    if (u_useTexture) {
-        color *= texture(u_texture, v_texCoord).rgb;
-    }
-    vec3 normal = normalize(v_normal);
-    vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
-    float diff = max(dot(normal, lightDir), 0.2);
-    color *= diff;
-    FragColor = vec4(color, u_alpha);
+  vec3 color = u_color;
+  if (u_useTexture) {
+    color *= texture(u_texture, v_texCoord).rgb;
+  }
+  vec3 normal = normalize(v_normal);
+  vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
+  float diff = max(dot(normal, lightDir), 0.2);
+  color *= diff;
+  FragColor = vec4(color, u_alpha);
 }

+ 7 - 7
assets/shaders/basic.vert

@@ -1,8 +1,8 @@
 #version 330 core
 
-layout (location = 0) in vec3 a_position;
-layout (location = 1) in vec3 a_normal;
-layout (location = 2) in vec2 a_texCoord;
+layout(location = 0) in vec3 a_position;
+layout(location = 1) in vec3 a_normal;
+layout(location = 2) in vec2 a_texCoord;
 
 uniform mat4 u_mvp;
 uniform mat4 u_model;
@@ -12,8 +12,8 @@ out vec2 v_texCoord;
 out vec3 v_worldPos;
 
 void main() {
-    v_normal = mat3(transpose(inverse(u_model))) * a_normal;
-    v_texCoord = a_texCoord;
-    v_worldPos = vec3(u_model * vec4(a_position, 1.0));
-    gl_Position = u_mvp * vec4(a_position, 1.0);
+  v_normal = mat3(transpose(inverse(u_model))) * a_normal;
+  v_texCoord = a_texCoord;
+  v_worldPos = vec3(u_model * vec4(a_position, 1.0));
+  gl_Position = u_mvp * vec4(a_position, 1.0);
 }

+ 5 - 5
assets/shaders/cylinder_instanced.frag

@@ -8,9 +8,9 @@ in float v_alpha;
 out vec4 FragColor;
 
 void main() {
-    vec3 normal = normalize(v_normal);
-    vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
-    float diff = max(dot(normal, lightDir), 0.2);
-    vec3 color = v_color * diff;
-    FragColor = vec4(color, v_alpha);
+  vec3 normal = normalize(v_normal);
+  vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
+  float diff = max(dot(normal, lightDir), 0.2);
+  vec3 color = v_color * diff;
+  FragColor = vec4(color, v_alpha);
 }

+ 33 - 32
assets/shaders/cylinder_instanced.vert

@@ -20,36 +20,37 @@ out float v_alpha;
 const float EPSILON = 1e-5;
 
 void main() {
-    vec3 axis = i_end - i_start;
-    float len = length(axis);
-    vec3 dir = len > EPSILON ? axis / len : vec3(0.0, 1.0, 0.0);
-
-    vec3 up = abs(dir.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
-    vec3 tangent = normalize(cross(up, dir));
-    if (length(tangent) < EPSILON) {
-        tangent = vec3(1.0, 0.0, 0.0);
-    }
-    vec3 bitangent = cross(dir, tangent);
-    if (length(bitangent) < EPSILON) {
-        bitangent = vec3(0.0, 0.0, 1.0);
-    } else {
-        bitangent = normalize(bitangent);
-    }
-    tangent = normalize(cross(bitangent, dir));
-
-    vec3 localPos = a_position;
-    float along = (localPos.y + 0.5) * len;
-    vec3 radial = tangent * localPos.x + bitangent * localPos.z;
-
-    vec3 worldPos = i_start + dir * along + radial * i_radius;
-
-    vec3 localNormal = a_normal;
-    vec3 worldNormal = normalize(tangent * localNormal.x + dir * localNormal.y + bitangent * localNormal.z);
-
-    v_worldPos = worldPos;
-    v_normal = worldNormal;
-    v_color = i_color;
-    v_alpha = i_alpha;
-
-    gl_Position = u_viewProj * vec4(worldPos, 1.0);
+  vec3 axis = i_end - i_start;
+  float len = length(axis);
+  vec3 dir = len > EPSILON ? axis / len : vec3(0.0, 1.0, 0.0);
+
+  vec3 up = abs(dir.y) < 0.999 ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
+  vec3 tangent = normalize(cross(up, dir));
+  if (length(tangent) < EPSILON) {
+    tangent = vec3(1.0, 0.0, 0.0);
+  }
+  vec3 bitangent = cross(dir, tangent);
+  if (length(bitangent) < EPSILON) {
+    bitangent = vec3(0.0, 0.0, 1.0);
+  } else {
+    bitangent = normalize(bitangent);
+  }
+  tangent = normalize(cross(bitangent, dir));
+
+  vec3 localPos = a_position;
+  float along = (localPos.y + 0.5) * len;
+  vec3 radial = tangent * localPos.x + bitangent * localPos.z;
+
+  vec3 worldPos = i_start + dir * along + radial * i_radius;
+
+  vec3 localNormal = a_normal;
+  vec3 worldNormal = normalize(tangent * localNormal.x + dir * localNormal.y +
+                               bitangent * localNormal.z);
+
+  v_worldPos = worldPos;
+  v_normal = worldNormal;
+  v_color = i_color;
+  v_alpha = i_alpha;
+
+  gl_Position = u_viewProj * vec4(worldPos, 1.0);
 }

+ 5 - 5
assets/shaders/fog_instanced.frag

@@ -8,9 +8,9 @@ in float v_alpha;
 out vec4 FragColor;
 
 void main() {
-    vec3 normal = normalize(v_normal);
-    vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
-    float diff = max(dot(normal, lightDir), 0.2);
-    vec3 color = v_color * diff;
-    FragColor = vec4(color, v_alpha);
+  vec3 normal = normalize(v_normal);
+  vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0));
+  float diff = max(dot(normal, lightDir), 0.2);
+  vec3 color = v_color * diff;
+  FragColor = vec4(color, v_alpha);
 }

+ 8 - 8
assets/shaders/fog_instanced.vert

@@ -17,14 +17,14 @@ out vec3 v_color;
 out float v_alpha;
 
 void main() {
-    vec3 worldPos = vec3(i_center.x + a_position.x * i_size,
-                         i_center.y + a_position.y,
-                         i_center.z + a_position.z * i_size);
+  vec3 worldPos =
+      vec3(i_center.x + a_position.x * i_size, i_center.y + a_position.y,
+           i_center.z + a_position.z * i_size);
 
-    v_worldPos = worldPos;
-    v_normal = vec3(0.0, 1.0, 0.0);
-    v_color = i_color;
-    v_alpha = i_alpha;
+  v_worldPos = worldPos;
+  v_normal = vec3(0.0, 1.0, 0.0);
+  v_color = i_color;
+  v_alpha = i_alpha;
 
-    gl_Position = u_viewProj * vec4(worldPos, 1.0);
+  gl_Position = u_viewProj * vec4(worldPos, 1.0);
 }

+ 3 - 3
assets/shaders/grass_instanced.frag

@@ -6,7 +6,7 @@ in float v_alpha;
 out vec4 fragColor;
 
 void main() {
-    if (v_alpha <= 0.02)
-        discard;
-    fragColor = vec4(v_color, v_alpha);
+  if (v_alpha <= 0.02)
+    discard;
+  fragColor = vec4(v_color, v_alpha);
 }

+ 33 - 35
assets/shaders/grass_instanced.vert

@@ -1,10 +1,10 @@
 #version 330 core
 
-layout (location = 0) in vec3 a_position;
-layout (location = 1) in vec2 a_uv;
-layout (location = 2) in vec4 a_posHeight;
-layout (location = 3) in vec4 a_colorWidth;
-layout (location = 4) in vec4 a_swayParams;
+layout(location = 0) in vec3 a_position;
+layout(location = 1) in vec2 a_uv;
+layout(location = 2) in vec4 a_posHeight;
+layout(location = 3) in vec4 a_colorWidth;
+layout(location = 4) in vec4 a_swayParams;
 
 uniform mat4 u_viewProj;
 uniform float u_time;
@@ -17,43 +17,41 @@ out vec3 v_color;
 out float v_alpha;
 
 void main() {
-    vec3 basePos = a_posHeight.xyz;
-    float bladeHeight = a_posHeight.w;
+  vec3 basePos = a_posHeight.xyz;
+  float bladeHeight = a_posHeight.w;
 
-    vec3 bladeColor = a_colorWidth.xyz;
-    float bladeWidth = a_colorWidth.w;
+  vec3 bladeColor = a_colorWidth.xyz;
+  float bladeWidth = a_colorWidth.w;
 
-    float swayStrength = a_swayParams.x * u_windStrength;
-    float swaySpeed = a_swayParams.y * u_windSpeed;
-    float swayPhase = a_swayParams.z;
-    float orientation = a_swayParams.w;
+  float swayStrength = a_swayParams.x * u_windStrength;
+  float swaySpeed = a_swayParams.y * u_windSpeed;
+  float swayPhase = a_swayParams.z;
+  float orientation = a_swayParams.w;
 
-    float tip = clamp(a_uv.y, 0.0, 1.0);
-    float sway = sin(u_time * swaySpeed + swayPhase) * swayStrength;
-    float bend = smoothstep(0.0, 1.0, tip);
-    float swayOffset = sway * bend;
+  float tip = clamp(a_uv.y, 0.0, 1.0);
+  float sway = sin(u_time * swaySpeed + swayPhase) * swayStrength;
+  float bend = smoothstep(0.0, 1.0, tip);
+  float swayOffset = sway * bend;
 
-    vec3 localPos = vec3(a_position.x * bladeWidth + swayOffset,
-                         a_position.y * bladeHeight,
-                         0.0);
+  vec3 localPos = vec3(a_position.x * bladeWidth + swayOffset,
+                       a_position.y * bladeHeight, 0.0);
 
-    float sinO = sin(orientation);
-    float cosO = cos(orientation);
-    vec3 rotated = vec3(localPos.x * cosO - localPos.z * sinO,
-                        localPos.y,
-                        localPos.x * sinO + localPos.z * cosO);
+  float sinO = sin(orientation);
+  float cosO = cos(orientation);
+  vec3 rotated = vec3(localPos.x * cosO - localPos.z * sinO, localPos.y,
+                      localPos.x * sinO + localPos.z * cosO);
 
-    vec3 worldPos = basePos + rotated;
+  vec3 worldPos = basePos + rotated;
 
-    vec3 lightDir = normalize(u_lightDir);
-    vec3 normal = normalize(vec3(sinO, 1.6, cosO));
-    float lightTerm = clamp(dot(normal, lightDir), 0.0, 1.0);
-    float tipHighlight = mix(0.7, 1.0, tip);
-    vec3 soilBlend = mix(u_soilColor, bladeColor, tip);
-    v_color = soilBlend * (0.7 + 0.3 * lightTerm) * tipHighlight;
+  vec3 lightDir = normalize(u_lightDir);
+  vec3 normal = normalize(vec3(sinO, 1.6, cosO));
+  float lightTerm = clamp(dot(normal, lightDir), 0.0, 1.0);
+  float tipHighlight = mix(0.7, 1.0, tip);
+  vec3 soilBlend = mix(u_soilColor, bladeColor, tip);
+  v_color = soilBlend * (0.7 + 0.3 * lightTerm) * tipHighlight;
 
-    float edgeFade = 1.0 - smoothstep(0.35, 0.5, abs(a_uv.x - 0.5));
-    v_alpha = clamp(0.35 + 0.45 * tip, 0.25, 0.85) * edgeFade;
+  float edgeFade = 1.0 - smoothstep(0.35, 0.5, abs(a_uv.x - 0.5));
+  v_alpha = clamp(0.35 + 0.45 * tip, 0.25, 0.85) * edgeFade;
 
-    gl_Position = u_viewProj * vec4(worldPos, 1.0);
+  gl_Position = u_viewProj * vec4(worldPos, 1.0);
 }

+ 28 - 25
assets/shaders/grid.frag

@@ -13,32 +13,35 @@ out vec4 FragColor;
 
 // Hash for subtle per-cell variation
 float hash12(vec2 p) {
-    vec3 p3 = fract(vec3(p.xyx) * 0.1031);
-    p3 += dot(p3, p3.yzx + 33.33);
-    return fract((p3.x + p3.y) * p3.z);
+  vec3 p3 = fract(vec3(p.xyx) * 0.1031);
+  p3 += dot(p3, p3.yzx + 33.33);
+  return fract((p3.x + p3.y) * p3.z);
 }
 
 void main() {
-    vec2 coord = v_worldPos.xz / u_cellSize;
-    vec2 f = fract(coord) - 0.5;
-    vec2 af = abs(f);
-
-    // Anti-aliased lines using fwidth
-    float fw = fwidth(af.x);
-    float lineX = 1.0 - smoothstep(0.5 - u_thickness - fw, 0.5 - u_thickness + fw, af.x);
-    fw = fwidth(af.y);
-    float lineY = 1.0 - smoothstep(0.5 - u_thickness - fw, 0.5 - u_thickness + fw, af.y);
-    float lineMask = max(lineX, lineY);
-
-    // Emphasize major lines every 5 cells
-    vec2 cell = floor(coord);
-    float major = (abs(mod(cell.x, 5.0)) < 0.5 || abs(mod(cell.y, 5.0)) < 0.5) ? 1.0 : 0.0;
-    vec3 lineCol = mix(u_lineColor, u_lineColor * 1.2, major);
-
-    // Subtle per-cell brightness jitter
-    float jitter = (hash12(cell) - 0.5) * 0.06;
-    vec3 base = u_gridColor * (1.0 + jitter);
-
-    vec3 col = mix(base, lineCol, lineMask);
-    FragColor = vec4(col, 1.0);
+  vec2 coord = v_worldPos.xz / u_cellSize;
+  vec2 f = fract(coord) - 0.5;
+  vec2 af = abs(f);
+
+  // Anti-aliased lines using fwidth
+  float fw = fwidth(af.x);
+  float lineX =
+      1.0 - smoothstep(0.5 - u_thickness - fw, 0.5 - u_thickness + fw, af.x);
+  fw = fwidth(af.y);
+  float lineY =
+      1.0 - smoothstep(0.5 - u_thickness - fw, 0.5 - u_thickness + fw, af.y);
+  float lineMask = max(lineX, lineY);
+
+  // Emphasize major lines every 5 cells
+  vec2 cell = floor(coord);
+  float major =
+      (abs(mod(cell.x, 5.0)) < 0.5 || abs(mod(cell.y, 5.0)) < 0.5) ? 1.0 : 0.0;
+  vec3 lineCol = mix(u_lineColor, u_lineColor * 1.2, major);
+
+  // Subtle per-cell brightness jitter
+  float jitter = (hash12(cell) - 0.5) * 0.06;
+  vec3 base = u_gridColor * (1.0 + jitter);
+
+  vec3 col = mix(base, lineCol, lineMask);
+  FragColor = vec4(col, 1.0);
 }

+ 66 - 49
assets/shaders/ground_plane.frag

@@ -2,7 +2,7 @@
 in vec3 v_worldPos;
 in vec3 v_normal;
 in vec2 v_uv;
-layout (location = 0) out vec4 FragColor;
+layout(location = 0) out vec4 FragColor;
 uniform vec3 u_grassPrimary;
 uniform vec3 u_grassSecondary;
 uniform vec3 u_grassDry;
@@ -17,53 +17,70 @@ uniform float u_soilBlendSharpness;
 uniform float u_ambientBoost;
 uniform vec3 u_lightDir;
 
-float hash21(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);}
-float noise21(vec2 p){vec2 i=floor(p),f=fract(p);float a=hash21(i),b=hash21(i+vec2(1,0)),c=hash21(i+vec2(0,1)),d=hash21(i+vec2(1,1));vec2 u=f*f*(3.0-2.0*f);return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);}
-float fbm(vec2 p){float v=0.0,a=0.5;for(int i=0;i<3;++i){v+=noise21(p)*a;p=p*2.07+13.17;a*=0.5;}return v;}
+float hash21(vec2 p) {
+  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
+}
+float noise21(vec2 p) {
+  vec2 i = floor(p), f = fract(p);
+  float a = hash21(i), b = hash21(i + vec2(1, 0)), c = hash21(i + vec2(0, 1)),
+        d = hash21(i + vec2(1, 1));
+  vec2 u = f * f * (3.0 - 2.0 * f);
+  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
+}
+float fbm(vec2 p) {
+  float v = 0.0, a = 0.5;
+  for (int i = 0; i < 3; ++i) {
+    v += noise21(p) * a;
+    p = p * 2.07 + 13.17;
+    a *= 0.5;
+  }
+  return v;
+}
 
-void main(){
-    vec3 n=normalize(v_normal);
-    float ts=max(u_tileSize,1e-4);
-    vec2 wuv=(v_worldPos.xz/ts)+u_noiseOffset;
-    float macro=fbm(wuv*u_macroNoiseScale);
-    float detail=noise21(wuv*(u_detailNoiseScale*2.0));
-    float patchNoise=fbm(wuv*u_macroNoiseScale*0.4);
-    float moistureVar=smoothstep(0.3,0.7,patchNoise);
-    float lush=smoothstep(0.2,0.8,macro);
-    lush=mix(lush,moistureVar,0.3);
-    vec3 lushGrass=mix(u_grassPrimary,u_grassSecondary,lush);
-    float dryness=clamp(0.3*detail,0.0,0.4);
-    dryness+=moistureVar*0.15;
-    vec3 grassCol=mix(lushGrass,u_grassDry,dryness);
-    float sw=max(0.01,1.0/max(u_soilBlendSharpness,1e-3));
-    float sN=(noise21(wuv*4.0+9.7)-0.5)*sw*0.8;
-    float soilMix=1.0-smoothstep(u_soilBlendHeight-sw+sN,u_soilBlendHeight+sw+sN,v_worldPos.y);
-    soilMix=clamp(soilMix,0.0,1.0);
-    float mudPatch=fbm(wuv*0.08+vec2(7.3,11.2));
-    mudPatch=smoothstep(0.65,0.75,mudPatch);
-    soilMix=max(soilMix,mudPatch*0.85);
-    vec3 baseCol=mix(grassCol,u_soilColor,soilMix);
-    vec3 dx=dFdx(v_worldPos),dy=dFdy(v_worldPos);
-    float mScale=u_detailNoiseScale*8.0/ts;
-    float h0=noise21(wuv*mScale);
-    float hx=noise21((wuv+dx.xz*mScale));
-    float hy=noise21((wuv+dy.xz*mScale));
-    vec2 g=vec2(hx-h0,hy-h0);
-    vec3 t=normalize(dx - n*dot(n,dx));
-    vec3 b=normalize(cross(n,t));
-    float microAmp=0.12;
-    vec3 nMicro=normalize(n-(t*g.x + b*g.y)*microAmp);
-    float jitter=(hash21(wuv*0.27+vec2(17.0,9.0))-0.5)*0.06;
-    float brightnessVar=(moistureVar-0.5)*0.08;
-    vec3 col=baseCol*(1.0+jitter+brightnessVar);
-    col*=u_tint;
-    vec3 L=normalize(u_lightDir);
-    float ndl=max(dot(nMicro,L),0.0);
-    float ambient=0.40;
-    float fres=pow(1.0-max(dot(nMicro,vec3(0,1,0)),0.0),2.0);
-    float roughnessVar=mix(0.65,0.95,1.0-moistureVar);
-    float specContrib=fres*0.08*roughnessVar;
-    float shade=ambient+ndl*0.65+specContrib;
-    vec3 lit=col*shade*u_ambientBoost;
-    FragColor=vec4(clamp(lit,0.0,1.0),1.0);
+void main() {
+  vec3 n = normalize(v_normal);
+  float ts = max(u_tileSize, 1e-4);
+  vec2 wuv = (v_worldPos.xz / ts) + u_noiseOffset;
+  float macro = fbm(wuv * u_macroNoiseScale);
+  float detail = noise21(wuv * (u_detailNoiseScale * 2.0));
+  float patchNoise = fbm(wuv * u_macroNoiseScale * 0.4);
+  float moistureVar = smoothstep(0.3, 0.7, patchNoise);
+  float lush = smoothstep(0.2, 0.8, macro);
+  lush = mix(lush, moistureVar, 0.3);
+  vec3 lushGrass = mix(u_grassPrimary, u_grassSecondary, lush);
+  float dryness = clamp(0.3 * detail, 0.0, 0.4);
+  dryness += moistureVar * 0.15;
+  vec3 grassCol = mix(lushGrass, u_grassDry, dryness);
+  float sw = max(0.01, 1.0 / max(u_soilBlendSharpness, 1e-3));
+  float sN = (noise21(wuv * 4.0 + 9.7) - 0.5) * sw * 0.8;
+  float soilMix = 1.0 - smoothstep(u_soilBlendHeight - sw + sN,
+                                   u_soilBlendHeight + sw + sN, v_worldPos.y);
+  soilMix = clamp(soilMix, 0.0, 1.0);
+  float mudPatch = fbm(wuv * 0.08 + vec2(7.3, 11.2));
+  mudPatch = smoothstep(0.65, 0.75, mudPatch);
+  soilMix = max(soilMix, mudPatch * 0.85);
+  vec3 baseCol = mix(grassCol, u_soilColor, soilMix);
+  vec3 dx = dFdx(v_worldPos), dy = dFdy(v_worldPos);
+  float mScale = u_detailNoiseScale * 8.0 / ts;
+  float h0 = noise21(wuv * mScale);
+  float hx = noise21((wuv + dx.xz * mScale));
+  float hy = noise21((wuv + dy.xz * mScale));
+  vec2 g = vec2(hx - h0, hy - h0);
+  vec3 t = normalize(dx - n * dot(n, dx));
+  vec3 b = normalize(cross(n, t));
+  float microAmp = 0.12;
+  vec3 nMicro = normalize(n - (t * g.x + b * g.y) * microAmp);
+  float jitter = (hash21(wuv * 0.27 + vec2(17.0, 9.0)) - 0.5) * 0.06;
+  float brightnessVar = (moistureVar - 0.5) * 0.08;
+  vec3 col = baseCol * (1.0 + jitter + brightnessVar);
+  col *= u_tint;
+  vec3 L = normalize(u_lightDir);
+  float ndl = max(dot(nMicro, L), 0.0);
+  float ambient = 0.40;
+  float fres = pow(1.0 - max(dot(nMicro, vec3(0, 1, 0)), 0.0), 2.0);
+  float roughnessVar = mix(0.65, 0.95, 1.0 - moistureVar);
+  float specContrib = fres * 0.08 * roughnessVar;
+  float shade = ambient + ndl * 0.65 + specContrib;
+  vec3 lit = col * shade * u_ambientBoost;
+  FragColor = vec4(clamp(lit, 0.0, 1.0), 1.0);
 }

+ 41 - 20
assets/shaders/ground_plane.vert

@@ -1,7 +1,7 @@
 #version 330 core
-layout (location = 0) in vec3 a_position;
-layout (location = 1) in vec3 a_normal;
-layout (location = 2) in vec2 a_uv;
+layout(location = 0) in vec3 a_position;
+layout(location = 1) in vec3 a_normal;
+layout(location = 2) in vec2 a_uv;
 uniform mat4 u_mvp;
 uniform mat4 u_model;
 uniform vec2 u_noiseOffset;
@@ -11,22 +11,43 @@ out vec3 v_worldPos;
 out vec3 v_normal;
 out vec2 v_uv;
 
-float hash21(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123);}
-float noise21(vec2 p){vec2 i=floor(p),f=fract(p);float a=hash21(i),b=hash21(i+vec2(1,0)),c=hash21(i+vec2(0,1)),d=hash21(i+vec2(1,1));vec2 u=f*f*(3.0-2.0*f);return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);}
-float fbm2(vec2 p){float v=0.0,a=0.5;for(int i=0;i<2;++i){v+=noise21(p)*a;p=p*2.07+13.17;a*=0.5;}return v;}
-mat2 rot2(float a){float c=cos(a),s=sin(a);return mat2(c,-s,s,c);}
+float hash21(vec2 p) {
+  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
+}
+float noise21(vec2 p) {
+  vec2 i = floor(p), f = fract(p);
+  float a = hash21(i), b = hash21(i + vec2(1, 0)), c = hash21(i + vec2(0, 1)),
+        d = hash21(i + vec2(1, 1));
+  vec2 u = f * f * (3.0 - 2.0 * f);
+  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
+}
+float fbm2(vec2 p) {
+  float v = 0.0, a = 0.5;
+  for (int i = 0; i < 2; ++i) {
+    v += noise21(p) * a;
+    p = p * 2.07 + 13.17;
+    a *= 0.5;
+  }
+  return v;
+}
+mat2 rot2(float a) {
+  float c = cos(a), s = sin(a);
+  return mat2(c, -s, s, c);
+}
 
-void main(){
-    vec3 wp=(u_model*vec4(a_position,1.0)).xyz;
-    vec3 wn=normalize(mat3(u_model)*a_normal);
-    float ang=fract(sin(dot(u_noiseOffset,vec2(12.9898,78.233)))*43758.5453)*6.2831853;
-    vec2 uv=rot2(ang)*(wp.xz+u_noiseOffset);
-    float h=fbm2(uv*u_heightNoiseFrequency)*2.0-1.0;
-    float amp=clamp(u_heightNoiseStrength,0.0,0.20);
-    float disp=h*amp;
-    wp.y+=disp;
-    v_worldPos=wp;
-    v_normal=wn;
-    v_uv=a_uv;
-    gl_Position=u_mvp*vec4(wp,1.0);
+void main() {
+  vec3 wp = (u_model * vec4(a_position, 1.0)).xyz;
+  vec3 wn = normalize(mat3(u_model) * a_normal);
+  float ang =
+      fract(sin(dot(u_noiseOffset, vec2(12.9898, 78.233))) * 43758.5453) *
+      6.2831853;
+  vec2 uv = rot2(ang) * (wp.xz + u_noiseOffset);
+  float h = fbm2(uv * u_heightNoiseFrequency) * 2.0 - 1.0;
+  float amp = clamp(u_heightNoiseStrength, 0.0, 0.20);
+  float disp = h * amp;
+  wp.y += disp;
+  v_worldPos = wp;
+  v_normal = wn;
+  v_uv = a_uv;
+  gl_Position = u_mvp * vec4(wp, 1.0);
 }

+ 13 - 13
assets/shaders/stone_instanced.frag

@@ -9,17 +9,17 @@ uniform vec3 uLightDirection;
 out vec4 FragColor;
 
 void main() {
-    vec3 normal = normalize(vNormal);
-    vec3 lightDir = normalize(uLightDirection);
-    
-    // Diffuse lighting
-    float diffuse = max(dot(normal, lightDir), 0.0);
-    
-    // Ambient + diffuse
-    float ambient = 0.4;
-    float lighting = ambient + diffuse * 0.6;
-    
-    vec3 color = vColor * lighting;
-    
-    FragColor = vec4(color, 1.0);
+  vec3 normal = normalize(vNormal);
+  vec3 lightDir = normalize(uLightDirection);
+
+  // Diffuse lighting
+  float diffuse = max(dot(normal, lightDir), 0.0);
+
+  // Ambient + diffuse
+  float ambient = 0.4;
+  float lighting = ambient + diffuse * 0.6;
+
+  vec3 color = vColor * lighting;
+
+  FragColor = vec4(color, 1.0);
 }

+ 24 - 24
assets/shaders/stone_instanced.vert

@@ -2,8 +2,8 @@
 
 layout(location = 0) in vec3 aPos;
 layout(location = 1) in vec3 aNormal;
-layout(location = 2) in vec4 aPosScale;    // instance: xyz=world pos, w=scale
-layout(location = 3) in vec4 aColorRot;    // instance: rgb=color, a=rotation
+layout(location = 2) in vec4 aPosScale; // instance: xyz=world pos, w=scale
+layout(location = 3) in vec4 aColorRot; // instance: rgb=color, a=rotation
 
 uniform mat4 uViewProj;
 
@@ -12,26 +12,26 @@ out vec3 vNormal;
 out vec3 vColor;
 
 void main() {
-    float scale = aPosScale.w;
-    vec3 worldPos = aPosScale.xyz;
-    float rotation = aColorRot.a;
-    
-    // Rotate vertex around Y-axis
-    float cosR = cos(rotation);
-    float sinR = sin(rotation);
-    mat2 rot = mat2(cosR, -sinR, sinR, cosR);
-    
-    vec3 localPos = aPos * scale;
-    vec2 rotatedXZ = rot * localPos.xz;
-    localPos = vec3(rotatedXZ.x, localPos.y, rotatedXZ.y);
-    
-    vWorldPos = localPos + worldPos;
-    
-    // Rotate normal
-    vec2 rotatedNormalXZ = rot * aNormal.xz;
-    vNormal = normalize(vec3(rotatedNormalXZ.x, aNormal.y, rotatedNormalXZ.y));
-    
-    vColor = aColorRot.rgb;
-    
-    gl_Position = uViewProj * vec4(vWorldPos, 1.0);
+  float scale = aPosScale.w;
+  vec3 worldPos = aPosScale.xyz;
+  float rotation = aColorRot.a;
+
+  // Rotate vertex around Y-axis
+  float cosR = cos(rotation);
+  float sinR = sin(rotation);
+  mat2 rot = mat2(cosR, -sinR, sinR, cosR);
+
+  vec3 localPos = aPos * scale;
+  vec2 rotatedXZ = rot * localPos.xz;
+  localPos = vec3(rotatedXZ.x, localPos.y, rotatedXZ.y);
+
+  vWorldPos = localPos + worldPos;
+
+  // Rotate normal
+  vec2 rotatedNormalXZ = rot * aNormal.xz;
+  vNormal = normalize(vec3(rotatedNormalXZ.x, aNormal.y, rotatedNormalXZ.y));
+
+  vColor = aColorRot.rgb;
+
+  gl_Position = uViewProj * vec4(vWorldPos, 1.0);
 }

+ 237 - 210
assets/shaders/terrain_chunk.frag

@@ -5,11 +5,11 @@ in vec3 v_normal;
 in vec2 v_uv;
 in float v_vertexDisplacement;
 
-layout (location = 0) out vec4 FragColor;
+layout(location = 0) out vec4 FragColor;
 
-uniform vec3  u_grassPrimary, u_grassSecondary, u_grassDry, u_soilColor;
-uniform vec3  u_rockLow, u_rockHigh, u_tint, u_lightDir;
-uniform vec2  u_noiseOffset;
+uniform vec3 u_grassPrimary, u_grassSecondary, u_grassDry, u_soilColor;
+uniform vec3 u_rockLow, u_rockHigh, u_tint, u_lightDir;
+uniform vec2 u_noiseOffset;
 uniform float u_tileSize, u_macroNoiseScale, u_detailNoiseScale;
 uniform float u_slopeRockThreshold, u_slopeRockSharpness;
 uniform float u_soilBlendHeight, u_soilBlendSharpness;
@@ -17,243 +17,270 @@ uniform float u_heightNoiseStrength, u_heightNoiseFrequency;
 uniform float u_ambientBoost, u_rockDetailStrength;
 
 // lets soil “climb” up steep toes (world units)
-uniform float u_soilFootHeight;          // try 0.6–1.2
+uniform float u_soilFootHeight; // try 0.6–1.2
 
 // -------- OPTIONAL (leave at defaults if you don’t have a heightmap) --------
-uniform int   u_hasHeightTex;            // 0 = off (default), 1 = on
+uniform int u_hasHeightTex; // 0 = off (default), 1 = on
 uniform sampler2D u_heightTex;
-uniform vec2  u_heightTexelSize;
-uniform vec2  u_heightUVScale, u_heightUVOffset;
-uniform float u_heightTexToWorld;        // height normalization -> world units
-uniform int   u_toeTexRadius;            // 3–6
-uniform float u_toeHeightDelta;          // ~0.5–2.0 world units
-uniform float u_toeStrength;             // 0..1
+uniform vec2 u_heightTexelSize;
+uniform vec2 u_heightUVScale, u_heightUVOffset;
+uniform float u_heightTexToWorld; // height normalization -> world units
+uniform int u_toeTexRadius;       // 3–6
+uniform float u_toeHeightDelta;   // ~0.5–2.0 world units
+uniform float u_toeStrength;      // 0..1
 // ----------------------------------------------------------------------------
 
 // NEW: view-consistent, data-free toe smoothing (works even without heightmap)
-uniform float u_screenToeMul;            // try 12.0–30.0
-uniform float u_screenToeClamp;          // max extra width in world units (try 0.8)
-
-float hash21(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453123); }
-
-float noise21(vec2 p){
-    vec2 i = floor(p), f = fract(p);
-    float a = hash21(i);
-    float b = hash21(i + vec2(1.0,0.0));
-    float c = hash21(i + vec2(0.0,1.0));
-    float d = hash21(i + vec2(1.0,1.0));
-    vec2 u = f*f*(3.0 - 2.0*f);
-    return mix(mix(a,b,u.x), mix(c,d,u.x), u.y);
+uniform float u_screenToeMul;   // try 12.0–30.0
+uniform float u_screenToeClamp; // max extra width in world units (try 0.8)
+
+float hash21(vec2 p) {
+  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
+}
+
+float noise21(vec2 p) {
+  vec2 i = floor(p), f = fract(p);
+  float a = hash21(i);
+  float b = hash21(i + vec2(1.0, 0.0));
+  float c = hash21(i + vec2(0.0, 1.0));
+  float d = hash21(i + vec2(1.0, 1.0));
+  vec2 u = f * f * (3.0 - 2.0 * f);
+  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
 }
 
-float fbm(vec2 p){
-    float v=0.0, a=0.5;
-    for(int i=0;i<4;++i){ v += noise21(p)*a; p = p*2.07+13.17; a*=0.5; }
-    return v;
+float fbm(vec2 p) {
+  float v = 0.0, a = 0.5;
+  for (int i = 0; i < 4; ++i) {
+    v += noise21(p) * a;
+    p = p * 2.07 + 13.17;
+    a *= 0.5;
+  }
+  return v;
 }
 
-vec3 triplanarWeights(vec3 n){
-    vec3 b = abs(n); b = pow(b, vec3(4.0));
-    return b / (b.x + b.y + b.z + 1e-5);
+vec3 triplanarWeights(vec3 n) {
+  vec3 b = abs(n);
+  b = pow(b, vec3(4.0));
+  return b / (b.x + b.y + b.z + 1e-5);
 }
 
-float triplanarNoise(vec3 wp, float s){
-    vec3 w = triplanarWeights(normalize(v_normal));
-    float xy = noise21(wp.xy * s);
-    float xz = noise21(wp.xz * s);
-    float yz = noise21(wp.yz * s);
-    return xy*w.z + xz*w.y + yz*w.x;
+float triplanarNoise(vec3 wp, float s) {
+  vec3 w = triplanarWeights(normalize(v_normal));
+  float xy = noise21(wp.xy * s);
+  float xz = noise21(wp.xz * s);
+  float yz = noise21(wp.yz * s);
+  return xy * w.z + xz * w.y + yz * w.x;
 }
 
-float computeCurvature(){
-    float hx = dFdx(v_worldPos.y);
-    float hy = dFdy(v_worldPos.y);
-    return 0.5 * (dFdx(hx) + dFdy(hy));
+float computeCurvature() {
+  float hx = dFdx(v_worldPos.y);
+  float hy = dFdy(v_worldPos.y);
+  return 0.5 * (dFdx(hx) + dFdy(hy));
 }
 
-vec3 geomNormal(){
-    vec3 dx = dFdx(v_worldPos);
-    vec3 dy = dFdy(v_worldPos);
-    vec3 n  = normalize(cross(dx,dy));
-    return (dot(n, v_normal) < 0.0) ? -n : n;
+vec3 geomNormal() {
+  vec3 dx = dFdx(v_worldPos);
+  vec3 dy = dFdy(v_worldPos);
+  vec3 n = normalize(cross(dx, dy));
+  return (dot(n, v_normal) < 0.0) ? -n : n;
 }
 
 // ---- Optional heightmap helpers --------------------------------------------
-float sampleHeight(vec2 uv){ return texture(u_heightTex, uv).r * u_heightTexToWorld; }
+float sampleHeight(vec2 uv) {
+  return texture(u_heightTex, uv).r * u_heightTexToWorld;
+}
 
 // (kept for reference; no longer used by the fix)
 // float maxRiseNearby(vec2 uv, int r){ ... }  // removed to avoid confusion
 
 // --- world <-> UV helpers for height texture sampling (FIX ADD) ---
-vec2 uvToWorld(vec2 duv){
-    // uv = worldXZ * u_heightUVScale + u_heightUVOffset
-    // => worldXZ delta per uv delta = duv / u_heightUVScale (component-wise)
-    return duv / max(abs(u_heightUVScale), vec2(1e-6));
+vec2 uvToWorld(vec2 duv) {
+  // uv = worldXZ * u_heightUVScale + u_heightUVOffset
+  // => worldXZ delta per uv delta = duv / u_heightUVScale (component-wise)
+  return duv / max(abs(u_heightUVScale), vec2(1e-6));
 }
 
-float avgWorldPerTexel(){
-    vec2 wpt = abs(uvToWorld(u_heightTexelSize));
-    return 0.5 * (wpt.x + wpt.y);
+float avgWorldPerTexel() {
+  vec2 wpt = abs(uvToWorld(u_heightTexelSize));
+  return 0.5 * (wpt.x + wpt.y);
 }
 
-// Radial min-distance (in WORLD units) to a higher “cliff” neighborhood (FIX ADD)
-float minCliffDistanceRadial(vec2 uv, int r, float riseDelta){
-    const int MAX_R   = 12;      // max steps per ray (in texels)
-    const int NUM_DIR = 12;      // number of ray directions
-    r = clamp(r, 1, MAX_R);
-
-    float h0 = sampleHeight(uv);
-    float best = 1e9;
-
-    vec2 texStep = u_heightTexelSize;
-
-    for(int d = 0; d < NUM_DIR; ++d){
-        float ang = 6.2831853 * (float(d) + 0.5) / float(NUM_DIR);
-        vec2 dir = normalize(vec2(cos(ang), sin(ang))) * texStep;
-
-        vec2 p = uv;
-        for(int s = 1; s <= MAX_R; ++s){
-            if(s > r) break;
-            p += dir;
-
-            float rise = sampleHeight(p) - h0;
-            if(rise > riseDelta){
-                float stepWorld = length(uvToWorld(dir));
-                float distWorld = stepWorld * float(s);
-                best = min(best, distWorld);
-                break;
-            }
-        }
+// Radial min-distance (in WORLD units) to a higher “cliff” neighborhood (FIX
+// ADD)
+float minCliffDistanceRadial(vec2 uv, int r, float riseDelta) {
+  const int MAX_R = 12;   // max steps per ray (in texels)
+  const int NUM_DIR = 12; // number of ray directions
+  r = clamp(r, 1, MAX_R);
+
+  float h0 = sampleHeight(uv);
+  float best = 1e9;
+
+  vec2 texStep = u_heightTexelSize;
+
+  for (int d = 0; d < NUM_DIR; ++d) {
+    float ang = 6.2831853 * (float(d) + 0.5) / float(NUM_DIR);
+    vec2 dir = normalize(vec2(cos(ang), sin(ang))) * texStep;
+
+    vec2 p = uv;
+    for (int s = 1; s <= MAX_R; ++s) {
+      if (s > r)
+        break;
+      p += dir;
+
+      float rise = sampleHeight(p) - h0;
+      if (rise > riseDelta) {
+        float stepWorld = length(uvToWorld(dir));
+        float distWorld = stepWorld * float(s);
+        best = min(best, distWorld);
+        break;
+      }
     }
-    return best; // 1e9 = not found
+  }
+  return best; // 1e9 = not found
 }
 // ----------------------------------------------------------------------------
 
-void main(){
-    vec3 normal = geomNormal();
-    float slope = 1.0 - clamp(normal.y, 0.0, 1.0);
-    float curvature = computeCurvature();
-
-    float tileScale = max(u_tileSize, 0.0001);
-    vec2 worldCoord = (v_worldPos.xz / tileScale) + u_noiseOffset;
-
-    float macroNoise   = fbm(worldCoord * u_macroNoiseScale);
-    float detailNoise  = triplanarNoise(v_worldPos, u_detailNoiseScale * 2.5 / tileScale);
-    float erosionNoise = triplanarNoise(v_worldPos, u_detailNoiseScale * 4.0 / tileScale + 17.0);
-
-    float patchNoise = fbm(worldCoord * u_macroNoiseScale * 0.4);
-    float moistureVar = smoothstep(0.3, 0.7, patchNoise);
-    float lushFactor = smoothstep(0.2, 0.8, macroNoise);
-    lushFactor = mix(lushFactor, moistureVar, 0.3);
-    vec3  lushGrass  = mix(u_grassPrimary, u_grassSecondary, lushFactor);
-    float dryness    = clamp(0.55 * slope + 0.45 * detailNoise, 0.0, 1.0);
-    dryness += moistureVar * 0.15;
-    vec3  grassColor = mix(lushGrass, u_grassDry, dryness);
-
-    // ----- Soil band (height + toe expansion) -----
-    float soilWidth = max(0.01, 1.0 / max(u_soilBlendSharpness, 0.001));
-
-    // gentle noise to avoid tiled edges
-    float heightNoise = (triplanarNoise(v_worldPos, max(0.0001, u_heightNoiseFrequency)) - 0.5)
-                        * u_heightNoiseStrength;
-
-    // local toe from slope (small on flats)
-    float toeLocal = smoothstep(0.25, 0.9, slope);
-
-    // world-consistent “screen-space” dilation (FIX REPLACE)
-    vec2 dxxz = dFdx(v_worldPos.xz);
-    vec2 dyxz = dFdy(v_worldPos.xz);
-    float pxWorld = max(length(dxxz), length(dyxz)); // world meters per pixel (approx)
-    float dh = max(abs(dFdx(v_worldPos.y)), abs(dFdy(v_worldPos.y))); // height change per pixel
-    float toeSS = clamp((dh / max(pxWorld, 1e-6)) * u_screenToeMul, 0.0, u_screenToeClamp);
-
-    // heightmap-based toe: isotropic distance-to-cliff (FIX REPLACE)
-    float toeHM = 0.0;
-    if(u_hasHeightTex == 1){
-        vec2 huv = v_worldPos.xz * u_heightUVScale + u_heightUVOffset;
-        float dW = minCliffDistanceRadial(huv, u_toeTexRadius, max(1e-4, u_toeHeightDelta));
-        float maxSearchW = avgWorldPerTexel() * float(u_toeTexRadius);
-        if(dW < 1e8){
-            toeHM = smoothstep(maxSearchW, 0.0, dW) * clamp(u_toeStrength, 0.0, 1.0);
-        }
+void main() {
+  vec3 normal = geomNormal();
+  float slope = 1.0 - clamp(normal.y, 0.0, 1.0);
+  float curvature = computeCurvature();
+
+  float tileScale = max(u_tileSize, 0.0001);
+  vec2 worldCoord = (v_worldPos.xz / tileScale) + u_noiseOffset;
+
+  float macroNoise = fbm(worldCoord * u_macroNoiseScale);
+  float detailNoise =
+      triplanarNoise(v_worldPos, u_detailNoiseScale * 2.5 / tileScale);
+  float erosionNoise =
+      triplanarNoise(v_worldPos, u_detailNoiseScale * 4.0 / tileScale + 17.0);
+
+  float patchNoise = fbm(worldCoord * u_macroNoiseScale * 0.4);
+  float moistureVar = smoothstep(0.3, 0.7, patchNoise);
+  float lushFactor = smoothstep(0.2, 0.8, macroNoise);
+  lushFactor = mix(lushFactor, moistureVar, 0.3);
+  vec3 lushGrass = mix(u_grassPrimary, u_grassSecondary, lushFactor);
+  float dryness = clamp(0.55 * slope + 0.45 * detailNoise, 0.0, 1.0);
+  dryness += moistureVar * 0.15;
+  vec3 grassColor = mix(lushGrass, u_grassDry, dryness);
+
+  // ----- Soil band (height + toe expansion) -----
+  float soilWidth = max(0.01, 1.0 / max(u_soilBlendSharpness, 0.001));
+
+  // gentle noise to avoid tiled edges
+  float heightNoise =
+      (triplanarNoise(v_worldPos, max(0.0001, u_heightNoiseFrequency)) - 0.5) *
+      u_heightNoiseStrength;
+
+  // local toe from slope (small on flats)
+  float toeLocal = smoothstep(0.25, 0.9, slope);
+
+  // world-consistent “screen-space” dilation (FIX REPLACE)
+  vec2 dxxz = dFdx(v_worldPos.xz);
+  vec2 dyxz = dFdy(v_worldPos.xz);
+  float pxWorld =
+      max(length(dxxz), length(dyxz)); // world meters per pixel (approx)
+  float dh = max(abs(dFdx(v_worldPos.y)),
+                 abs(dFdy(v_worldPos.y))); // height change per pixel
+  float toeSS =
+      clamp((dh / max(pxWorld, 1e-6)) * u_screenToeMul, 0.0, u_screenToeClamp);
+
+  // heightmap-based toe: isotropic distance-to-cliff (FIX REPLACE)
+  float toeHM = 0.0;
+  if (u_hasHeightTex == 1) {
+    vec2 huv = v_worldPos.xz * u_heightUVScale + u_heightUVOffset;
+    float dW = minCliffDistanceRadial(huv, u_toeTexRadius,
+                                      max(1e-4, u_toeHeightDelta));
+    float maxSearchW = avgWorldPerTexel() * float(u_toeTexRadius);
+    if (dW < 1e8) {
+      toeHM = smoothstep(maxSearchW, 0.0, dW) * clamp(u_toeStrength, 0.0, 1.0);
     }
-
-    float toeProximity = max(toeLocal, max(toeHM, toeSS / max(1e-6, u_soilFootHeight)));
-
-    // concave bias
-    float concavityLift = smoothstep(0.0, 0.02, -curvature) * (0.25 * u_soilFootHeight);
-
-    float soilHeight = u_soilBlendHeight + heightNoise + concavityLift;
-    float bandWidth  = soilWidth + u_soilFootHeight * toeProximity;
-
-    float soilMix = 1.0 - smoothstep(soilHeight - bandWidth,
-                                     soilHeight + bandWidth,
-                                     v_worldPos.y);
-    soilMix = clamp(soilMix, 0.0, 1.0);
-    
-    float mudPatch = fbm(worldCoord * 0.08 + vec2(7.3, 11.2));
-    mudPatch = smoothstep(0.65, 0.75, mudPatch);
-    soilMix = max(soilMix, mudPatch * 0.85 * (1.0 - slope * 0.6));
-    
-    vec3 soilBlend = mix(grassColor, u_soilColor, soilMix);
-
-    // ----- Rocks -----
-    float slopeCut = smoothstep(u_slopeRockThreshold, u_slopeRockThreshold + 0.02, slope);
-    float rockMask = clamp(
-        pow(slopeCut, max(1.0, u_slopeRockSharpness)) +
-        (erosionNoise - 0.5) * u_rockDetailStrength,
-        0.0, 1.0
-    );
-    rockMask *= 1.0 - soilMix * 0.75;
-
-    float rockLerp = clamp(0.35 + detailNoise * 0.65, 0.0, 1.0);
-    vec3 rockColor = mix(u_rockLow, u_rockHigh, rockLerp);
-    rockColor = mix(rockColor, rockColor * 1.15, clamp(u_rockDetailStrength * 1.4, 0.0, 1.0));
-
-    // micro normal
-    vec3 microNormal = normal;
-    float microDetailScale = u_detailNoiseScale * 8.0 / tileScale;
-    vec2 microOffset = vec2(0.01, 0.0);
-    float h0 = triplanarNoise(v_worldPos, microDetailScale);
-    float hx = triplanarNoise(v_worldPos + vec3(microOffset.x, 0.0, 0.0), microDetailScale);
-    float hz = triplanarNoise(v_worldPos + vec3(0.0, 0.0, microOffset.x), microDetailScale);
-    vec3 microGrad = vec3((hx - h0) / microOffset.x, 0.0, (hz - h0) / microOffset.x);
-    float microAmp = 0.15 * u_rockDetailStrength * (0.2 + 0.8 * slope);
-    microNormal = normalize(normal + microGrad * microAmp);
-
-    // feature signals
-    float isFlat = 1.0 - smoothstep(0.10, 0.25, slope);
-    float isHigh = smoothstep(u_soilBlendHeight + 0.5, u_soilBlendHeight + 1.5, v_worldPos.y);
-    float plateauFactor = isFlat * isHigh;
-    float isGully = smoothstep(0.0, 0.02, -curvature) * (1.0 - smoothstep(0.25, 0.6, slope));
-    float isSteep = smoothstep(0.3, 0.5, slope);
-    float isRim   = smoothstep(0.0, 0.02, curvature);
-    float rimFactor = isSteep * isRim;
-
-    rockMask = clamp(rockMask + rimFactor * 0.10 - plateauFactor * 0.06 - isGully * 0.08, 0.0, 1.0);
-
-    vec3 terrainColor = mix(soilBlend, rockColor, rockMask);
-
-    // albedo jitter
-    float jitter = (hash21(worldCoord * 0.27 + vec2(17.0, 9.0)) - 0.5) * 0.06;
-    float brightnessVar = (moistureVar - 0.5) * 0.08 * (1.0 - rockMask);
-    terrainColor *= (1.0 + jitter + brightnessVar) * u_tint;
-
-    // lighting
-    vec3 L = normalize(u_lightDir);
-    float ndl = max(dot(microNormal, L), 0.0);
-    float ambient = 0.35;
-    float fresnel = pow(1.0 - max(dot(microNormal, vec3(0.0,1.0,0.0)), 0.0), 2.0);
-    float roughnessVar = mix(0.65, 0.95, 1.0 - moistureVar);
-    float specContrib = fresnel * 0.12 * roughnessVar * (1.0 - rockMask);
-    float shade = ambient + ndl * 0.75 + specContrib;
-
-    float plateauBrightness = 1.0 + plateauFactor * 0.05;
-    float gullyDarkness     = 1.0 - isGully * 0.04;
-    float rimContrast       = 1.0 + rimFactor * 0.03;
-
-    terrainColor *= plateauBrightness * gullyDarkness * rimContrast;
-    vec3 litColor = terrainColor * shade * u_ambientBoost;
-
-    FragColor = vec4(clamp(litColor, 0.0, 1.0), 1.0);
+  }
+
+  float toeProximity =
+      max(toeLocal, max(toeHM, toeSS / max(1e-6, u_soilFootHeight)));
+
+  // concave bias
+  float concavityLift =
+      smoothstep(0.0, 0.02, -curvature) * (0.25 * u_soilFootHeight);
+
+  float soilHeight = u_soilBlendHeight + heightNoise + concavityLift;
+  float bandWidth = soilWidth + u_soilFootHeight * toeProximity;
+
+  float soilMix = 1.0 - smoothstep(soilHeight - bandWidth,
+                                   soilHeight + bandWidth, v_worldPos.y);
+  soilMix = clamp(soilMix, 0.0, 1.0);
+
+  float mudPatch = fbm(worldCoord * 0.08 + vec2(7.3, 11.2));
+  mudPatch = smoothstep(0.65, 0.75, mudPatch);
+  soilMix = max(soilMix, mudPatch * 0.85 * (1.0 - slope * 0.6));
+
+  vec3 soilBlend = mix(grassColor, u_soilColor, soilMix);
+
+  // ----- Rocks -----
+  float slopeCut =
+      smoothstep(u_slopeRockThreshold, u_slopeRockThreshold + 0.02, slope);
+  float rockMask = clamp(pow(slopeCut, max(1.0, u_slopeRockSharpness)) +
+                             (erosionNoise - 0.5) * u_rockDetailStrength,
+                         0.0, 1.0);
+  rockMask *= 1.0 - soilMix * 0.75;
+
+  float rockLerp = clamp(0.35 + detailNoise * 0.65, 0.0, 1.0);
+  vec3 rockColor = mix(u_rockLow, u_rockHigh, rockLerp);
+  rockColor = mix(rockColor, rockColor * 1.15,
+                  clamp(u_rockDetailStrength * 1.4, 0.0, 1.0));
+
+  // micro normal
+  vec3 microNormal = normal;
+  float microDetailScale = u_detailNoiseScale * 8.0 / tileScale;
+  vec2 microOffset = vec2(0.01, 0.0);
+  float h0 = triplanarNoise(v_worldPos, microDetailScale);
+  float hx = triplanarNoise(v_worldPos + vec3(microOffset.x, 0.0, 0.0),
+                            microDetailScale);
+  float hz = triplanarNoise(v_worldPos + vec3(0.0, 0.0, microOffset.x),
+                            microDetailScale);
+  vec3 microGrad =
+      vec3((hx - h0) / microOffset.x, 0.0, (hz - h0) / microOffset.x);
+  float microAmp = 0.15 * u_rockDetailStrength * (0.2 + 0.8 * slope);
+  microNormal = normalize(normal + microGrad * microAmp);
+
+  // feature signals
+  float isFlat = 1.0 - smoothstep(0.10, 0.25, slope);
+  float isHigh = smoothstep(u_soilBlendHeight + 0.5, u_soilBlendHeight + 1.5,
+                            v_worldPos.y);
+  float plateauFactor = isFlat * isHigh;
+  float isGully =
+      smoothstep(0.0, 0.02, -curvature) * (1.0 - smoothstep(0.25, 0.6, slope));
+  float isSteep = smoothstep(0.3, 0.5, slope);
+  float isRim = smoothstep(0.0, 0.02, curvature);
+  float rimFactor = isSteep * isRim;
+
+  rockMask =
+      clamp(rockMask + rimFactor * 0.10 - plateauFactor * 0.06 - isGully * 0.08,
+            0.0, 1.0);
+
+  vec3 terrainColor = mix(soilBlend, rockColor, rockMask);
+
+  // albedo jitter
+  float jitter = (hash21(worldCoord * 0.27 + vec2(17.0, 9.0)) - 0.5) * 0.06;
+  float brightnessVar = (moistureVar - 0.5) * 0.08 * (1.0 - rockMask);
+  terrainColor *= (1.0 + jitter + brightnessVar) * u_tint;
+
+  // lighting
+  vec3 L = normalize(u_lightDir);
+  float ndl = max(dot(microNormal, L), 0.0);
+  float ambient = 0.35;
+  float fresnel =
+      pow(1.0 - max(dot(microNormal, vec3(0.0, 1.0, 0.0)), 0.0), 2.0);
+  float roughnessVar = mix(0.65, 0.95, 1.0 - moistureVar);
+  float specContrib = fresnel * 0.12 * roughnessVar * (1.0 - rockMask);
+  float shade = ambient + ndl * 0.75 + specContrib;
+
+  float plateauBrightness = 1.0 + plateauFactor * 0.05;
+  float gullyDarkness = 1.0 - isGully * 0.04;
+  float rimContrast = 1.0 + rimFactor * 0.03;
+
+  terrainColor *= plateauBrightness * gullyDarkness * rimContrast;
+  vec3 litColor = terrainColor * shade * u_ambientBoost;
+
+  FragColor = vec4(clamp(litColor, 0.0, 1.0), 1.0);
 }

+ 48 - 46
assets/shaders/terrain_chunk.vert

@@ -1,8 +1,8 @@
 #version 330 core
 
-layout (location = 0) in vec3 a_position;
-layout (location = 1) in vec3 a_normal;
-layout (location = 2) in vec2 a_uv;
+layout(location = 0) in vec3 a_position;
+layout(location = 1) in vec3 a_normal;
+layout(location = 2) in vec2 a_uv;
 
 uniform mat4 u_mvp;
 uniform mat4 u_model;
@@ -17,73 +17,75 @@ out float v_vertexDisplacement;
 
 // Simple hash function for noise
 float hash21(vec2 p) {
-    return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
+  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
 }
 
 // Value noise
 float noise21(vec2 p) {
-    vec2 i = floor(p);
-    vec2 f = fract(p);
+  vec2 i = floor(p);
+  vec2 f = fract(p);
 
-    float a = hash21(i);
-    float b = hash21(i + vec2(1.0, 0.0));
-    float c = hash21(i + vec2(0.0, 1.0));
-    float d = hash21(i + vec2(1.0, 1.0));
+  float a = hash21(i);
+  float b = hash21(i + vec2(1.0, 0.0));
+  float c = hash21(i + vec2(0.0, 1.0));
+  float d = hash21(i + vec2(1.0, 1.0));
 
-    vec2 u = f * f * (3.0 - 2.0 * f);
-    return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
+  vec2 u = f * f * (3.0 - 2.0 * f);
+  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
 }
 
 // Low-octave fBM (2 octaves for smooth, broad displacement)
 float fbm2(vec2 p) {
-    float value = 0.0;
-    float amplitude = 0.5;
-    for (int i = 0; i < 2; ++i) {
-        value += noise21(p) * amplitude;
-        p = p * 2.07 + 13.17;
-        amplitude *= 0.5;
-    }
-    return value;
+  float value = 0.0;
+  float amplitude = 0.5;
+  for (int i = 0; i < 2; ++i) {
+    value += noise21(p) * amplitude;
+    p = p * 2.07 + 13.17;
+    amplitude *= 0.5;
+  }
+  return value;
 }
 
 // 2D rotation matrix
 mat2 rot2(float angle) {
-    float c = cos(angle);
-    float s = sin(angle);
-    return mat2(c, -s, s, c);
+  float c = cos(angle);
+  float s = sin(angle);
+  return mat2(c, -s, s, c);
 }
 
 void main() {
-    // Transform to world space
-    vec3 wp = (u_model * vec4(a_position, 1.0)).xyz;
-    vec3 worldNormal = normalize(mat3(u_model) * a_normal);
+  // Transform to world space
+  vec3 wp = (u_model * vec4(a_position, 1.0)).xyz;
+  vec3 worldNormal = normalize(mat3(u_model) * a_normal);
 
-    // Generate stable rotation angle from noise offset
-    float angle = fract(sin(dot(u_noiseOffset, vec2(12.9898, 78.233))) * 43758.5453) * 6.2831853;
+  // Generate stable rotation angle from noise offset
+  float angle =
+      fract(sin(dot(u_noiseOffset, vec2(12.9898, 78.233))) * 43758.5453) *
+      6.2831853;
 
-    // Rotate noise coordinates for variation
-    vec2 uv = rot2(angle) * (wp.xz + u_noiseOffset);
+  // Rotate noise coordinates for variation
+  vec2 uv = rot2(angle) * (wp.xz + u_noiseOffset);
 
-    // Sample low-octave fBM for displacement (2 octaves)
-    float h = fbm2(uv * u_heightNoiseFrequency) * 2.0 - 1.0;
+  // Sample low-octave fBM for displacement (2 octaves)
+  float h = fbm2(uv * u_heightNoiseFrequency) * 2.0 - 1.0;
 
-    // Flatness factor: high on plateaus/flat areas, low on steep faces
-    float flatness = clamp(worldNormal.y, 0.0, 1.0);
+  // Flatness factor: high on plateaus/flat areas, low on steep faces
+  float flatness = clamp(worldNormal.y, 0.0, 1.0);
 
-    // More displacement on flat areas (plateaus), less on steep slopes
-    float displacementFactor = mix(0.4, 1.0, flatness);
+  // More displacement on flat areas (plateaus), less on steep slopes
+  float displacementFactor = mix(0.4, 1.0, flatness);
 
-    // Clamp height noise strength to reasonable range (0.10-0.20 world units)
-    float heightAmp = clamp(u_heightNoiseStrength, 0.0, 0.20);
+  // Clamp height noise strength to reasonable range (0.10-0.20 world units)
+  float heightAmp = clamp(u_heightNoiseStrength, 0.0, 0.20);
 
-    // Apply displacement
-    float displacement = h * heightAmp * displacementFactor;
-    wp.y += displacement;
+  // Apply displacement
+  float displacement = h * heightAmp * displacementFactor;
+  wp.y += displacement;
 
-    v_worldPos = wp;
-    v_normal = worldNormal;
-    v_uv = a_uv;
-    v_vertexDisplacement = displacement;
+  v_worldPos = wp;
+  v_normal = worldNormal;
+  v_uv = a_uv;
+  v_vertexDisplacement = displacement;
 
-    gl_Position = u_mvp * vec4(wp, 1.0);
+  gl_Position = u_mvp * vec4(wp, 1.0);
 }

+ 4 - 4
game/map/environment.h

@@ -6,8 +6,8 @@ namespace Render {
 namespace GL {
 class Renderer;
 class Camera;
-} 
-} 
+} // namespace GL
+} // namespace Render
 
 namespace Game {
 namespace Map {
@@ -19,5 +19,5 @@ struct Environment {
                            Render::GL::Camera &camera);
 };
 
-} 
-} 
+} // namespace Map
+} // namespace Game

+ 2 - 2
game/map/level_loader.cpp

@@ -131,5 +131,5 @@ LevelLoadResult LevelLoader::loadFromAssets(const QString &mapPath,
   return res;
 }
 
-} 
-} 
+} // namespace Map
+} // namespace Game

+ 19 - 23
game/map/map_catalog.cpp

@@ -7,8 +7,8 @@
 #include <QJsonObject>
 #include <QSet>
 #include <QStringList>
-#include <QVariantMap>
 #include <QTimer>
+#include <QVariantMap>
 #include <algorithm>
 
 namespace Game {
@@ -101,13 +101,14 @@ QVariantList MapCatalog::availableMaps() {
 }
 
 void MapCatalog::loadMapsAsync() {
-  if (m_loading) return;
-  
+  if (m_loading)
+    return;
+
   m_maps.clear();
   m_pendingFiles.clear();
   m_loading = true;
   emit loadingChanged(true);
-  
+
   QDir mapsDir(QStringLiteral("assets/maps"));
   if (!mapsDir.exists()) {
     m_loading = false;
@@ -115,18 +116,17 @@ void MapCatalog::loadMapsAsync() {
     emit allMapsLoaded();
     return;
   }
-  
-  m_pendingFiles = mapsDir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
-  
+
+  m_pendingFiles =
+      mapsDir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
+
   if (m_pendingFiles.isEmpty()) {
     m_loading = false;
     emit loadingChanged(false);
     emit allMapsLoaded();
     return;
   }
-  
-  
-  
+
   QTimer::singleShot(0, this, &MapCatalog::loadNextMap);
 }
 
@@ -137,20 +137,17 @@ void MapCatalog::loadNextMap() {
     emit allMapsLoaded();
     return;
   }
-  
+
   QString fileName = m_pendingFiles.takeFirst();
   QDir mapsDir(QStringLiteral("assets/maps"));
   QString path = mapsDir.filePath(fileName);
-  
+
   QVariantMap entry = loadSingleMap(path);
   if (!entry.isEmpty()) {
     m_maps.append(entry);
-    emit mapLoaded(entry);  
+    emit mapLoaded(entry);
   }
-  
-  
-  
-  
+
   if (!m_pendingFiles.isEmpty()) {
     QTimer::singleShot(10, this, &MapCatalog::loadNextMap);
   } else {
@@ -165,7 +162,7 @@ QVariantMap MapCatalog::loadSingleMap(const QString &path) {
   QString name = QFileInfo(path).fileName();
   QString desc;
   QSet<int> playerIds;
-  
+
   if (file.open(QIODevice::ReadOnly)) {
     QByteArray data = file.readAll();
     file.close();
@@ -194,13 +191,13 @@ QVariantMap MapCatalog::loadSingleMap(const QString &path) {
       }
     }
   }
-  
+
   QVariantMap entry;
   entry["name"] = name;
   entry["description"] = desc;
   entry["path"] = path;
   entry["playerCount"] = playerIds.size();
-  
+
   QVariantList playerIdList;
   QList<int> sortedIds = playerIds.values();
   std::sort(sortedIds.begin(), sortedIds.end());
@@ -209,7 +206,6 @@ QVariantMap MapCatalog::loadSingleMap(const QString &path) {
   }
   entry["playerIds"] = playerIdList;
 
-  
   QString thumbnail;
   if (file.open(QIODevice::ReadOnly)) {
     QByteArray data = file.readAll();
@@ -237,5 +233,5 @@ QVariantMap MapCatalog::loadSingleMap(const QString &path) {
   return entry;
 }
 
-}
-}
+} // namespace Map
+} // namespace Game

+ 9 - 10
game/map/map_catalog.h

@@ -11,28 +11,27 @@ class MapCatalog : public QObject {
   Q_OBJECT
 public:
   explicit MapCatalog(QObject *parent = nullptr);
-  
+
   static QVariantList availableMaps();
-  
-  
+
   Q_INVOKABLE void loadMapsAsync();
-  
+
   bool isLoading() const { return m_loading; }
-  const QVariantList& maps() const { return m_maps; }
-  
+  const QVariantList &maps() const { return m_maps; }
+
 signals:
   void mapLoaded(QVariantMap mapData);
   void allMapsLoaded();
   void loadingChanged(bool loading);
-  
+
 private:
   void loadNextMap();
   QVariantMap loadSingleMap(const QString &filePath);
-  
+
   QStringList m_pendingFiles;
   QVariantList m_maps;
   bool m_loading = false;
 };
 
-}
-}
+} // namespace Map
+} // namespace Game

+ 15 - 15
game/map/skirmish_loader.cpp

@@ -30,14 +30,14 @@ namespace Game {
 namespace Map {
 
 SkirmishLoader::SkirmishLoader(Engine::Core::World &world,
-                              Render::GL::Renderer &renderer,
-                              Render::GL::Camera &camera)
+                               Render::GL::Renderer &renderer,
+                               Render::GL::Camera &camera)
     : m_world(world), m_renderer(renderer), m_camera(camera) {}
 
 SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
-                                        const QVariantList &playerConfigs,
-                                        int selectedPlayerId,
-                                        int &outSelectedPlayerId) {
+                                         const QVariantList &playerConfigs,
+                                         int selectedPlayerId,
+                                         int &outSelectedPlayerId) {
   SkirmishLoadResult result;
 
   if (auto *selectionSystem =
@@ -140,8 +140,8 @@ SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
   Game::Map::MapTransformer::setLocalOwnerId(playerOwnerId);
   Game::Map::MapTransformer::setPlayerTeamOverrides(teamOverrides);
 
-  auto lr = Game::Map::LevelLoader::loadFromAssets(mapPath, m_world,
-                                                   m_renderer, m_camera);
+  auto lr = Game::Map::LevelLoader::loadFromAssets(mapPath, m_world, m_renderer,
+                                                   m_camera);
 
   if (!lr.ok && !lr.errorMessage.isEmpty()) {
     result.errorMessage = lr.errorMessage;
@@ -156,8 +156,7 @@ SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
       int playerId = config.value("playerId", -1).toInt();
       QString colorHex = config.value("colorHex", "#FFFFFF").toString();
 
-      if (playerId >= 0 && colorHex.startsWith("#") &&
-          colorHex.length() == 7) {
+      if (playerId >= 0 && colorHex.startsWith("#") && colorHex.length() == 7) {
         bool ok;
         int r = colorHex.mid(1, 2).toInt(&ok, 16);
         int g = colorHex.mid(3, 2).toInt(&ok, 16);
@@ -182,7 +181,7 @@ SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
       }
     }
   }
-  
+
   if (m_onOwnersUpdated) {
     m_onOwnersUpdated();
   }
@@ -226,12 +225,12 @@ SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
   auto &visibilityService = Game::Map::VisibilityService::instance();
   visibilityService.initialize(mapWidth, mapHeight, lr.tileSize);
   visibilityService.computeImmediate(m_world, playerOwnerId);
-  
+
   if (m_fog && visibilityService.isInitialized()) {
     m_fog->updateMask(
         visibilityService.getWidth(), visibilityService.getHeight(),
         visibilityService.getTileSize(), visibilityService.snapshotCells());
-    
+
     if (m_onVisibilityMaskReady) {
       m_onVisibilityMaskReady();
     }
@@ -267,7 +266,8 @@ SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
   if (focusEntity) {
     if (auto *t =
             focusEntity->getComponent<Engine::Core::TransformComponent>()) {
-      result.focusPosition = QVector3D(t->position.x, t->position.y, t->position.z);
+      result.focusPosition =
+          QVector3D(t->position.x, t->position.y, t->position.z);
       result.hasFocusPosition = true;
     }
   }
@@ -287,5 +287,5 @@ SkirmishLoadResult SkirmishLoader::start(const QString &mapPath,
   return result;
 }
 
-}
-}
+} // namespace Map
+} // namespace Game

+ 17 - 15
game/map/skirmish_loader.h

@@ -11,8 +11,8 @@ namespace Engine {
 namespace Core {
 class World;
 using EntityID = unsigned int;
-}
-}
+} // namespace Core
+} // namespace Engine
 
 namespace Render {
 namespace GL {
@@ -23,8 +23,8 @@ class TerrainRenderer;
 class BiomeRenderer;
 class FogRenderer;
 class StoneRenderer;
-}
-}
+} // namespace GL
+} // namespace Render
 
 namespace Game {
 namespace Map {
@@ -51,12 +51,15 @@ public:
   using OwnersUpdatedCallback = std::function<void()>;
   using VisibilityMaskReadyCallback = std::function<void()>;
 
-  SkirmishLoader(Engine::Core::World &world,
-                Render::GL::Renderer &renderer,
-                Render::GL::Camera &camera);
+  SkirmishLoader(Engine::Core::World &world, Render::GL::Renderer &renderer,
+                 Render::GL::Camera &camera);
 
-  void setGroundRenderer(Render::GL::GroundRenderer *ground) { m_ground = ground; }
-  void setTerrainRenderer(Render::GL::TerrainRenderer *terrain) { m_terrain = terrain; }
+  void setGroundRenderer(Render::GL::GroundRenderer *ground) {
+    m_ground = ground;
+  }
+  void setTerrainRenderer(Render::GL::TerrainRenderer *terrain) {
+    m_terrain = terrain;
+  }
   void setBiomeRenderer(Render::GL::BiomeRenderer *biome) { m_biome = biome; }
   void setFogRenderer(Render::GL::FogRenderer *fog) { m_fog = fog; }
   void setStoneRenderer(Render::GL::StoneRenderer *stone) { m_stone = stone; }
@@ -64,15 +67,14 @@ public:
   void setOnOwnersUpdated(OwnersUpdatedCallback callback) {
     m_onOwnersUpdated = callback;
   }
-  
+
   void setOnVisibilityMaskReady(VisibilityMaskReadyCallback callback) {
     m_onVisibilityMaskReady = callback;
   }
 
   SkirmishLoadResult start(const QString &mapPath,
-                          const QVariantList &playerConfigs,
-                          int selectedPlayerId,
-                          int &outSelectedPlayerId);
+                           const QVariantList &playerConfigs,
+                           int selectedPlayerId, int &outSelectedPlayerId);
 
 private:
   Engine::Core::World &m_world;
@@ -87,5 +89,5 @@ private:
   VisibilityMaskReadyCallback m_onVisibilityMaskReady;
 };
 
-}
-}
+} // namespace Map
+} // namespace Game

+ 1 - 1
game/map/terrain_service.cpp

@@ -85,4 +85,4 @@ TerrainType TerrainService::getTerrainType(int gridX, int gridZ) const {
   return m_heightMap->getTerrainType(gridX, gridZ);
 }
 
-} 
+} // namespace Game::Map

+ 1 - 1
game/map/terrain_service.h

@@ -44,4 +44,4 @@ private:
   BiomeSettings m_biomeSettings;
 };
 
-} 
+} // namespace Game::Map

+ 7 - 7
game/map/world_bootstrap.cpp

@@ -19,19 +19,19 @@ bool WorldBootstrap::initialize(Render::GL::Renderer &renderer,
   if (ground) {
     ground->configureExtent(50.0f);
   }
-  
+
   return true;
 }
 
 void WorldBootstrap::ensureInitialized(bool &initialized,
-                                      Render::GL::Renderer &renderer,
-                                      Render::GL::Camera &camera,
-                                      Render::GL::GroundRenderer *ground,
-                                      QString *outError) {
+                                       Render::GL::Renderer &renderer,
+                                       Render::GL::Camera &camera,
+                                       Render::GL::GroundRenderer *ground,
+                                       QString *outError) {
   if (!initialized) {
     initialized = initialize(renderer, camera, ground, outError);
   }
 }
 
-}
-}
+} // namespace Map
+} // namespace Game

+ 5 - 5
game/map/world_bootstrap.h

@@ -7,8 +7,8 @@ namespace GL {
 class Renderer;
 class Camera;
 class GroundRenderer;
-}
-}
+} // namespace GL
+} // namespace Render
 
 namespace Game {
 namespace Map {
@@ -19,7 +19,7 @@ public:
                          Render::GL::Camera &camera,
                          Render::GL::GroundRenderer *ground = nullptr,
                          QString *outError = nullptr);
-  
+
   static void ensureInitialized(bool &initialized,
                                 Render::GL::Renderer &renderer,
                                 Render::GL::Camera &camera,
@@ -27,5 +27,5 @@ public:
                                 QString *outError = nullptr);
 };
 
-}
-}
+} // namespace Map
+} // namespace Game

+ 1 - 1
game/systems/ai_system/ai_reasoner.cpp

@@ -1,6 +1,6 @@
 #include "ai_reasoner.h"
-#include "ai_utils.h"
 #include "../../game_config.h"
+#include "ai_utils.h"
 #include <algorithm>
 #include <cmath>
 #include <limits>

+ 2 - 4
game/systems/ai_system/behaviors/gather_behavior.cpp

@@ -51,10 +51,8 @@ void GatherBehavior::execute(const AISnapshot &snapshot, AIContext &context,
     formationType = nation->formationType;
   }
 
-  auto formationTargets =
-      FormationSystem::instance().getFormationPositions(
-          formationType, static_cast<int>(unitsToGather.size()), rallyPoint,
-          1.4f);
+  auto formationTargets = FormationSystem::instance().getFormationPositions(
+      formationType, static_cast<int>(unitsToGather.size()), rallyPoint, 1.4f);
 
   std::vector<Engine::Core::EntityID> unitsToMove;
   std::vector<float> targetX, targetY, targetZ;

+ 3 - 3
game/systems/camera_service.cpp

@@ -45,7 +45,7 @@ void CameraService::yaw(Render::GL::Camera &camera, float degrees) {
 }
 
 void CameraService::orbit(Render::GL::Camera &camera, float yawDeg,
-                           float pitchDeg) {
+                          float pitchDeg) {
   if (!std::isfinite(yawDeg) || !std::isfinite(pitchDeg)) {
     return;
   }
@@ -109,8 +109,8 @@ void CameraService::snapToEntity(Render::GL::Camera &camera,
   if (auto *t = entity.getComponent<Engine::Core::TransformComponent>()) {
     QVector3D center(t->position.x, t->position.y, t->position.z);
     const auto &camConfig = Game::GameConfig::instance().camera();
-    camera.setRTSView(center, camConfig.defaultDistance,
-                      camConfig.defaultPitch, camConfig.defaultYaw);
+    camera.setRTSView(center, camConfig.defaultDistance, camConfig.defaultPitch,
+                      camConfig.defaultYaw);
   }
 }
 

+ 1 - 1
game/systems/camera_service.h

@@ -12,7 +12,7 @@ class Entity;
 namespace Render {
 namespace GL {
 class Camera;
-} // namespace GL
+}
 } // namespace Render
 
 namespace Game {

+ 3 - 4
game/systems/formation_system.cpp

@@ -16,7 +16,7 @@ RomanFormation::calculatePositions(int unitCount, const QVector3D &center,
     return positions;
 
   float spacing = baseSpacing * 1.2f;
-  
+
   if (unitCount > 100) {
     spacing *= 2.0f;
   } else if (unitCount > 50) {
@@ -50,7 +50,7 @@ BarbarianFormation::calculatePositions(int unitCount, const QVector3D &center,
     return positions;
 
   float spacing = baseSpacing * 1.8f;
-  
+
   if (unitCount > 100) {
     spacing *= 2.0f;
   } else if (unitCount > 50) {
@@ -87,8 +87,7 @@ FormationSystem &FormationSystem::instance() {
 FormationSystem::FormationSystem() { initializeDefaults(); }
 
 void FormationSystem::initializeDefaults() {
-  registerFormation(FormationType::Roman,
-                    std::make_unique<RomanFormation>());
+  registerFormation(FormationType::Roman, std::make_unique<RomanFormation>());
   registerFormation(FormationType::Barbarian,
                     std::make_unique<BarbarianFormation>());
 }

+ 8 - 8
game/systems/formation_system.h

@@ -11,7 +11,7 @@ namespace Systems {
 
 enum class FormationType { Roman, Barbarian };
 
-} // namespace Systems
+}
 } // namespace Game
 
 namespace std {
@@ -20,7 +20,7 @@ template <> struct hash<Game::Systems::FormationType> {
     return std::hash<int>()(static_cast<int>(ft));
   }
 };
-}
+} // namespace std
 
 namespace Game {
 namespace Systems {
@@ -38,18 +38,18 @@ public:
 
 class RomanFormation : public IFormation {
 public:
-  std::vector<QVector3D> calculatePositions(int unitCount,
-                                            const QVector3D &center,
-                                            float baseSpacing = 1.0f) const override;
+  std::vector<QVector3D>
+  calculatePositions(int unitCount, const QVector3D &center,
+                     float baseSpacing = 1.0f) const override;
 
   FormationType getType() const override { return FormationType::Roman; }
 };
 
 class BarbarianFormation : public IFormation {
 public:
-  std::vector<QVector3D> calculatePositions(int unitCount,
-                                            const QVector3D &center,
-                                            float baseSpacing = 1.0f) const override;
+  std::vector<QVector3D>
+  calculatePositions(int unitCount, const QVector3D &center,
+                     float baseSpacing = 1.0f) const override;
 
   FormationType getType() const override { return FormationType::Barbarian; }
 };

+ 2 - 2
game/systems/production_service.cpp

@@ -38,12 +38,12 @@ ProductionResult ProductionService::startProductionForFirstSelectedBarracks(
     return ProductionResult::PerBarracksLimitReached;
   if (p->inProgress)
     return ProductionResult::AlreadyInProgress;
-  
+
   int currentTroops = world.countTroopsForPlayer(ownerId);
   int maxTroops = Game::GameConfig::instance().getMaxTroopsPerPlayer();
   if (currentTroops >= maxTroops)
     return ProductionResult::GlobalTroopLimitReached;
-  
+
   p->productType = unitType;
   p->timeRemaining = p->buildTime;
   p->inProgress = true;

+ 13 - 14
game/systems/selection_system.cpp

@@ -1,12 +1,12 @@
 #include "selection_system.h"
+#include "../../app/utils/selection_utils.h"
+#include "../../render/gl/camera.h"
 #include "../core/component.h"
 #include "../core/world.h"
+#include "../game_config.h"
 #include "command_service.h"
 #include "formation_planner.h"
 #include "picking_service.h"
-#include "../game_config.h"
-#include "../../app/utils/selection_utils.h"
-#include "../../render/gl/camera.h"
 #include <QPointF>
 #include <QVector3D>
 #include <algorithm>
@@ -78,9 +78,9 @@ void SelectionController::onClickSelect(qreal sx, qreal sy, bool additive,
   const auto &selected = m_selectionSystem->getSelectedUnits();
   if (!selected.empty()) {
     QVector3D hit;
-    if (!m_pickingService || !m_pickingService->screenToGround(
-                                 QPointF(sx, sy), *cam, viewportWidth,
-                                 viewportHeight, hit)) {
+    if (!m_pickingService ||
+        !m_pickingService->screenToGround(QPointF(sx, sy), *cam, viewportWidth,
+                                          viewportHeight, hit)) {
       return;
     }
     auto targets = Game::Systems::FormationPlanner::spreadFormation(
@@ -94,10 +94,10 @@ void SelectionController::onClickSelect(qreal sx, qreal sy, bool additive,
   }
 }
 
-void SelectionController::onAreaSelected(qreal x1, qreal y1, qreal x2,
-                                         qreal y2, bool additive,
-                                         int viewportWidth, int viewportHeight,
-                                         void *camera, int localOwnerId) {
+void SelectionController::onAreaSelected(qreal x1, qreal y1, qreal x2, qreal y2,
+                                         bool additive, int viewportWidth,
+                                         int viewportHeight, void *camera,
+                                         int localOwnerId) {
   if (!m_selectionSystem || !m_pickingService || !camera || !m_world)
     return;
 
@@ -105,10 +105,9 @@ void SelectionController::onAreaSelected(qreal x1, qreal y1, qreal x2,
     m_selectionSystem->clearSelection();
 
   auto *cam = static_cast<Render::GL::Camera *>(camera);
-  auto picked = m_pickingService->pickInRect(float(x1), float(y1), float(x2),
-                                             float(y2), *m_world, *cam,
-                                             viewportWidth, viewportHeight,
-                                             localOwnerId);
+  auto picked = m_pickingService->pickInRect(
+      float(x1), float(y1), float(x2), float(y2), *m_world, *cam, viewportWidth,
+      viewportHeight, localOwnerId);
   for (auto id : picked)
     m_selectionSystem->selectUnit(id);
   syncSelectionFlags();

+ 3 - 2
game/systems/selection_system.h

@@ -10,7 +10,7 @@ namespace Engine {
 namespace Core {
 class Entity;
 class World;
-}
+} // namespace Core
 } // namespace Engine
 
 namespace Game::Systems {
@@ -41,7 +41,8 @@ class SelectionController : public QObject {
 public:
   SelectionController(Engine::Core::World *world,
                       SelectionSystem *selectionSystem,
-                      PickingService *pickingService, QObject *parent = nullptr);
+                      PickingService *pickingService,
+                      QObject *parent = nullptr);
 
   void onClickSelect(qreal sx, qreal sy, bool additive, int viewportWidth,
                      int viewportHeight, void *camera, int localOwnerId);

+ 1 - 2
game/systems/troop_count_registry.cpp

@@ -43,8 +43,7 @@ void TroopCountRegistry::onUnitSpawned(
   m_troopCounts[event.ownerId] += individualsPerUnit;
 }
 
-void TroopCountRegistry::onUnitDied(
-    const Engine::Core::UnitDiedEvent &event) {
+void TroopCountRegistry::onUnitDied(const Engine::Core::UnitDiedEvent &event) {
   if (event.unitType == "barracks")
     return;
 

+ 1 - 1
game/systems/troop_count_registry.h

@@ -30,7 +30,7 @@ private:
   TroopCountRegistry &operator=(const TroopCountRegistry &) = delete;
 
   std::unordered_map<int, int> m_troopCounts;
-  
+
   Engine::Core::ScopedEventSubscription<Engine::Core::UnitSpawnedEvent>
       m_unitSpawnedSubscription;
   Engine::Core::ScopedEventSubscription<Engine::Core::UnitDiedEvent>

+ 1 - 1
game/visuals/team_colors.h

@@ -9,4 +9,4 @@ inline QVector3D teamColorForOwner(int ownerId) {
   auto color = registry.getOwnerColor(ownerId);
   return QVector3D(color[0], color[1], color[2]);
 }
-} 
+} // namespace Game::Visuals

+ 1 - 1
game/visuals/visual_catalog.cpp

@@ -95,4 +95,4 @@ void applyToRenderable(const VisualDef &def,
   }
 }
 
-} 
+} // namespace Game::Visuals

+ 2 - 2
game/visuals/visual_catalog.h

@@ -9,7 +9,7 @@ namespace Engine {
 namespace Core {
 class RenderableComponent;
 }
-} 
+} // namespace Engine
 
 namespace Game::Visuals {
 
@@ -35,4 +35,4 @@ VisualDef::MeshKind meshKindFromString(const QString &s);
 void applyToRenderable(const VisualDef &def,
                        Engine::Core::RenderableComponent &r);
 
-} 
+} // namespace Game::Visuals

+ 116 - 151
ui/qml/CursorManager.qml

@@ -2,184 +2,149 @@ import QtQuick 2.15
 
 Item {
     id: cursorManager
-    
+
     property string currentMode: "normal"
     property var cursorItem: null
-    
-    
+
+    function updateCursor(mode) {
+        currentMode = mode;
+        if (cursorItem) {
+            cursorItem.destroy();
+            cursorItem = null;
+        }
+        switch (mode) {
+        case "attack":
+            cursorItem = attackCursorComponent.createObject(cursorManager);
+            break;
+        case "guard":
+            cursorItem = guardCursorComponent.createObject(cursorManager);
+            break;
+        case "patrol":
+            cursorItem = patrolCursorComponent.createObject(cursorManager);
+            break;
+        default:
+            break;
+        }
+    }
+
     Component {
         id: attackCursorComponent
+
         Canvas {
             width: 32
             height: 32
             onPaint: {
-                var ctx = getContext("2d")
-                ctx.clearRect(0, 0, width, height)
-                
-                
-                ctx.strokeStyle = "#e74c3c"
-                ctx.lineWidth = 2
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(16, 4)
-                ctx.lineTo(16, 28)
-                ctx.stroke()
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(4, 16)
-                ctx.lineTo(28, 16)
-                ctx.stroke()
-                
-                
-                ctx.fillStyle = "#e74c3c"
-                ctx.beginPath()
-                ctx.arc(16, 16, 3, 0, Math.PI * 2)
-                ctx.fill()
-                
-                
-                ctx.strokeStyle = "#c0392b"
-                ctx.lineWidth = 2
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(8, 12)
-                ctx.lineTo(8, 8)
-                ctx.lineTo(12, 8)
-                ctx.stroke()
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(20, 8)
-                ctx.lineTo(24, 8)
-                ctx.lineTo(24, 12)
-                ctx.stroke()
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(8, 20)
-                ctx.lineTo(8, 24)
-                ctx.lineTo(12, 24)
-                ctx.stroke()
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(20, 24)
-                ctx.lineTo(24, 24)
-                ctx.lineTo(24, 20)
-                ctx.stroke()
+                var ctx = getContext("2d");
+                ctx.clearRect(0, 0, width, height);
+                ctx.strokeStyle = "#e74c3c";
+                ctx.lineWidth = 2;
+                ctx.beginPath();
+                ctx.moveTo(16, 4);
+                ctx.lineTo(16, 28);
+                ctx.stroke();
+                ctx.beginPath();
+                ctx.moveTo(4, 16);
+                ctx.lineTo(28, 16);
+                ctx.stroke();
+                ctx.fillStyle = "#e74c3c";
+                ctx.beginPath();
+                ctx.arc(16, 16, 3, 0, Math.PI * 2);
+                ctx.fill();
+                ctx.strokeStyle = "#c0392b";
+                ctx.lineWidth = 2;
+                ctx.beginPath();
+                ctx.moveTo(8, 12);
+                ctx.lineTo(8, 8);
+                ctx.lineTo(12, 8);
+                ctx.stroke();
+                ctx.beginPath();
+                ctx.moveTo(20, 8);
+                ctx.lineTo(24, 8);
+                ctx.lineTo(24, 12);
+                ctx.stroke();
+                ctx.beginPath();
+                ctx.moveTo(8, 20);
+                ctx.lineTo(8, 24);
+                ctx.lineTo(12, 24);
+                ctx.stroke();
+                ctx.beginPath();
+                ctx.moveTo(20, 24);
+                ctx.lineTo(24, 24);
+                ctx.lineTo(24, 20);
+                ctx.stroke();
             }
         }
+
     }
-    
+
     Component {
         id: guardCursorComponent
+
         Canvas {
             width: 32
             height: 32
             onPaint: {
-                var ctx = getContext("2d")
-                ctx.clearRect(0, 0, width, height)
-                
-                
-                ctx.fillStyle = "#3498db"
-                ctx.strokeStyle = "#2980b9"
-                ctx.lineWidth = 2
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(16, 6)
-                ctx.lineTo(24, 10)
-                ctx.lineTo(24, 18)
-                ctx.lineTo(16, 26)
-                ctx.lineTo(8, 18)
-                ctx.lineTo(8, 10)
-                ctx.closePath()
-                ctx.fill()
-                ctx.stroke()
-                
-                
-                ctx.strokeStyle = "#ecf0f1"
-                ctx.lineWidth = 2
-                ctx.beginPath()
-                ctx.moveTo(13, 16)
-                ctx.lineTo(15, 18)
-                ctx.lineTo(19, 12)
-                ctx.stroke()
+                var ctx = getContext("2d");
+                ctx.clearRect(0, 0, width, height);
+                ctx.fillStyle = "#3498db";
+                ctx.strokeStyle = "#2980b9";
+                ctx.lineWidth = 2;
+                ctx.beginPath();
+                ctx.moveTo(16, 6);
+                ctx.lineTo(24, 10);
+                ctx.lineTo(24, 18);
+                ctx.lineTo(16, 26);
+                ctx.lineTo(8, 18);
+                ctx.lineTo(8, 10);
+                ctx.closePath();
+                ctx.fill();
+                ctx.stroke();
+                ctx.strokeStyle = "#ecf0f1";
+                ctx.lineWidth = 2;
+                ctx.beginPath();
+                ctx.moveTo(13, 16);
+                ctx.lineTo(15, 18);
+                ctx.lineTo(19, 12);
+                ctx.stroke();
             }
         }
+
     }
-    
+
     Component {
         id: patrolCursorComponent
+
         Canvas {
             width: 32
             height: 32
             onPaint: {
-                var ctx = getContext("2d")
-                ctx.clearRect(0, 0, width, height)
-                
-                
-                ctx.fillStyle = "#27ae60"
-                ctx.strokeStyle = "#229954"
-                ctx.lineWidth = 2
-                
-                
-                ctx.beginPath()
-                ctx.arc(16, 16, 10, 0, Math.PI * 2)
-                ctx.stroke()
-                
-                
-                ctx.fillStyle = "#27ae60"
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(26, 16)
-                ctx.lineTo(22, 13)
-                ctx.lineTo(22, 19)
-                ctx.closePath()
-                ctx.fill()
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(6, 16)
-                ctx.lineTo(10, 13)
-                ctx.lineTo(10, 19)
-                ctx.closePath()
-                ctx.fill()
-                
-                
-                ctx.beginPath()
-                ctx.arc(16, 16, 3, 0, Math.PI * 2)
-                ctx.fill()
+                var ctx = getContext("2d");
+                ctx.clearRect(0, 0, width, height);
+                ctx.fillStyle = "#27ae60";
+                ctx.strokeStyle = "#229954";
+                ctx.lineWidth = 2;
+                ctx.beginPath();
+                ctx.arc(16, 16, 10, 0, Math.PI * 2);
+                ctx.stroke();
+                ctx.fillStyle = "#27ae60";
+                ctx.beginPath();
+                ctx.moveTo(26, 16);
+                ctx.lineTo(22, 13);
+                ctx.lineTo(22, 19);
+                ctx.closePath();
+                ctx.fill();
+                ctx.beginPath();
+                ctx.moveTo(6, 16);
+                ctx.lineTo(10, 13);
+                ctx.lineTo(10, 19);
+                ctx.closePath();
+                ctx.fill();
+                ctx.beginPath();
+                ctx.arc(16, 16, 3, 0, Math.PI * 2);
+                ctx.fill();
             }
         }
+
     }
-    
-    function updateCursor(mode) {
-        currentMode = mode
-        
-        
-        if (cursorItem) {
-            cursorItem.destroy()
-            cursorItem = null
-        }
-        
-        
-        switch(mode) {
-            case "attack":
-                cursorItem = attackCursorComponent.createObject(cursorManager)
-                break
-            case "guard":
-                cursorItem = guardCursorComponent.createObject(cursorManager)
-                break
-            case "patrol":
-                cursorItem = patrolCursorComponent.createObject(cursorManager)
-                break
-            default:
-                
-                break
-        }
-    }
+
 }

+ 338 - 397
ui/qml/GameView.qml

@@ -4,511 +4,452 @@ import StandardOfIron 1.0
 
 Item {
     id: gameView
-    objectName: "GameView"
-    
+
     property bool isPaused: false
-    property real gameSpeed: 1.0
-    property bool setRallyMode: false  
-    
+    property real gameSpeed: 1
+    property bool setRallyMode: false
+    property string cursorMode: "normal"
+
     signal mapClicked(real x, real y)
     signal unitSelected(int unitId)
     signal areaSelected(real x1, real y1, real x2, real y2)
-    
-    property string cursorMode: "normal"  
-    
+
     function setPaused(paused) {
-        isPaused = paused
+        isPaused = paused;
         if (typeof game !== 'undefined' && game.setPaused)
-            game.setPaused(paused)
+            game.setPaused(paused);
+
     }
-    
+
     function setGameSpeed(speed) {
-        gameSpeed = speed
+        gameSpeed = speed;
         if (typeof game !== 'undefined' && game.setGameSpeed)
-            game.setGameSpeed(speed)
+            game.setGameSpeed(speed);
+
     }
-    
+
     function issueCommand(command) {
-        console.log("Command issued:", command)
-        
+        console.log("Command issued:", command);
     }
-    
-    
-        GLView {
+
+    objectName: "GameView"
+    Keys.onPressed: function(event) {
+        if (typeof game === 'undefined')
+            return ;
+
+        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) {
+                mainWindow.menuVisible = true;
+                event.accepted = true;
+            }
+            break;
+        case Qt.Key_Space:
+            if (typeof mainWindow !== 'undefined') {
+                mainWindow.gamePaused = !mainWindow.gamePaused;
+                gameView.setPaused(mainWindow.gamePaused);
+                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, 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;
+            var shiftHeld = (event.modifiers & Qt.ShiftModifier) !== 0;
+            var pitchStep = shiftHeld ? 8 : 4;
+        case Qt.Key_R:
+            game.cameraOrbitDirection(1, shiftHeld);
+            event.accepted = true;
+            break;
+        case Qt.Key_F:
+            game.cameraOrbitDirection(-1, shiftHeld);
+            event.accepted = true;
+            break;
+        case Qt.Key_X:
+            game.selectAllTroops();
+            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) {
+            if (renderArea.keyPanCount === 0 && !renderArea.mousePanActive)
+                mainWindow.edgeScrollDisabled = false;
+
+        }
+    }
+    focus: true
+
+    GLView {
         id: renderArea
+
+        property int keyPanCount: 0
+        property bool mousePanActive: false
+
         anchors.fill: parent
-        engine: game 
+        engine: game
         focus: false
-        
-        
+        Component.onCompleted: {
+            if (typeof game !== 'undefined' && game.cursorMode)
+                gameView.cursorMode = game.cursorMode;
+
+        }
+
         Connections {
-            target: game
             function onCursorModeChanged() {
-                if (typeof game !== 'undefined' && game.cursorMode) {
-                    gameView.cursorMode = game.cursorMode
-                }
-            }
-        }
-        
-        
-        Component.onCompleted: {
-            if (typeof game !== 'undefined' && game.cursorMode) {
-                gameView.cursorMode = game.cursorMode
+                if (typeof game !== 'undefined' && game.cursorMode)
+                    gameView.cursorMode = game.cursorMode;
+
             }
+
+            target: game
         }
 
-        
-        property int keyPanCount: 0
-        property bool mousePanActive: false
-        
-        
         MouseArea {
             id: mouseArea
+
+            property bool isSelecting: false
+            property real startX: 0
+            property real startY: 0
+
             anchors.fill: parent
             acceptedButtons: Qt.LeftButton | Qt.RightButton
             hoverEnabled: true
             propagateComposedEvents: true
             preventStealing: true
-            
-            
             cursorShape: (gameView.cursorMode === "normal") ? Qt.ArrowCursor : Qt.BlankCursor
-            
             enabled: gameView.visible
-            
             onEntered: {
-                
-                if (typeof game !== 'undefined' && game.setHoverAtScreen) {
-                    game.setHoverAtScreen(0, 0) 
-                }
+                if (typeof game !== 'undefined' && game.setHoverAtScreen)
+                    game.setHoverAtScreen(0, 0);
+
             }
-            
             onExited: {
-                
-                if (typeof game !== 'undefined' && game.setHoverAtScreen) {
-                    game.setHoverAtScreen(-1, -1)
-                }
+                if (typeof game !== 'undefined' && game.setHoverAtScreen)
+                    game.setHoverAtScreen(-1, -1);
+
             }
-            
             onPositionChanged: function(mouse) {
-                
-                
                 if (isSelecting) {
-                    var endX = mouse.x
-                    var endY = mouse.y
-                    
-                    selectionBox.x = Math.min(startX, endX)
-                    selectionBox.y = Math.min(startY, endY)
-                    selectionBox.width = Math.abs(endX - startX)
-                    selectionBox.height = Math.abs(endY - startY)
+                    var endX = mouse.x;
+                    var endY = mouse.y;
+                    selectionBox.x = Math.min(startX, endX);
+                    selectionBox.y = Math.min(startY, endY);
+                    selectionBox.width = Math.abs(endX - startX);
+                    selectionBox.height = Math.abs(endY - startY);
                 } else {
-                    if (typeof game !== 'undefined' && game.setHoverAtScreen) {
-                        game.setHoverAtScreen(mouse.x, mouse.y)
-                    }
+                    if (typeof game !== 'undefined' && game.setHoverAtScreen)
+                        game.setHoverAtScreen(mouse.x, mouse.y);
+
                 }
             }
             onWheel: function(w) {
-                var dy = (w.angleDelta ? w.angleDelta.y / 120 : w.delta / 120)
-                if (dy !== 0 && typeof game !== 'undefined' && game.cameraZoom) {
-                    
-                    game.cameraZoom(dy * 0.8)
-                }
-                w.accepted = true
+                var dy = (w.angleDelta ? w.angleDelta.y / 120 : w.delta / 120);
+                if (dy !== 0 && typeof game !== 'undefined' && game.cameraZoom)
+                    game.cameraZoom(dy * 0.8);
+
+                w.accepted = true;
             }
-            
-            property bool isSelecting: false
-            property real startX: 0
-            property real startY: 0
-            
             onPressed: function(mouse) {
                 if (mouse.button === Qt.LeftButton) {
-                    
                     if (gameView.setRallyMode) {
-                        if (typeof game !== 'undefined' && game.setRallyAtScreen) {
-                            game.setRallyAtScreen(mouse.x, mouse.y)
-                        }
-                        gameView.setRallyMode = false
-                        return
+                        if (typeof game !== 'undefined' && game.setRallyAtScreen)
+                            game.setRallyAtScreen(mouse.x, mouse.y);
+
+                        gameView.setRallyMode = false;
+                        return ;
                     }
-                    
-                    
                     if (gameView.cursorMode === "attack") {
-                        if (typeof game !== 'undefined' && game.onAttackClick) {
-                            game.onAttackClick(mouse.x, mouse.y)
-                        }
-                        return
-                    }
-                    
-                    
-                    if (gameView.cursorMode === "guard") {
-                        
-                        return
+                        if (typeof game !== 'undefined' && game.onAttackClick)
+                            game.onAttackClick(mouse.x, mouse.y);
+
+                        return ;
                     }
-                    
-                    
+                    if (gameView.cursorMode === "guard")
+                        return ;
+
                     if (gameView.cursorMode === "patrol") {
-                        if (typeof game !== 'undefined' && game.onPatrolClick) {
-                            game.onPatrolClick(mouse.x, mouse.y)
-                        }
-                        return
+                        if (typeof game !== 'undefined' && game.onPatrolClick)
+                            game.onPatrolClick(mouse.x, mouse.y);
+
+                        return ;
                     }
-                    
-                    
-                    isSelecting = true
-                    startX = mouse.x
-                    startY = mouse.y
-                    selectionBox.x = startX
-                    selectionBox.y = startY
-                    selectionBox.width = 0
-                    selectionBox.height = 0
-                    selectionBox.visible = true
+                    isSelecting = true;
+                    startX = mouse.x;
+                    startY = mouse.y;
+                    selectionBox.x = startX;
+                    selectionBox.y = startY;
+                    selectionBox.width = 0;
+                    selectionBox.height = 0;
+                    selectionBox.visible = true;
                 } else if (mouse.button === Qt.RightButton) {
-                    
-                    renderArea.mousePanActive = true
-                    mainWindow.edgeScrollDisabled = true
+                    renderArea.mousePanActive = true;
+                    mainWindow.edgeScrollDisabled = true;
+                    if (gameView.setRallyMode)
+                        gameView.setRallyMode = false;
+
+                    if (typeof game !== 'undefined' && game.onRightClick)
+                        game.onRightClick(mouse.x, mouse.y);
 
-                    if (gameView.setRallyMode) {
-                        gameView.setRallyMode = false
-                    }
-                    if (typeof game !== 'undefined' && game.onRightClick) {
-                        game.onRightClick(mouse.x, mouse.y)
-                    }
                 }
             }
-            
             onReleased: function(mouse) {
                 if (mouse.button === Qt.LeftButton && isSelecting) {
-                    isSelecting = false
-                    selectionBox.visible = false
-                    
+                    isSelecting = false;
+                    selectionBox.visible = false;
                     if (selectionBox.width > 5 && selectionBox.height > 5) {
-                        
-                        areaSelected(selectionBox.x, selectionBox.y, 
-                                   selectionBox.x + selectionBox.width,
-                                   selectionBox.y + selectionBox.height)
-                        if (typeof game !== 'undefined' && game.onAreaSelected) {
-                            game.onAreaSelected(selectionBox.x, selectionBox.y,
-                                                selectionBox.x + selectionBox.width,
-                                                selectionBox.y + selectionBox.height,
-                                                false)
-                        }
+                        areaSelected(selectionBox.x, selectionBox.y, selectionBox.x + selectionBox.width, selectionBox.y + selectionBox.height);
+                        if (typeof game !== 'undefined' && game.onAreaSelected)
+                            game.onAreaSelected(selectionBox.x, selectionBox.y, selectionBox.x + selectionBox.width, selectionBox.y + selectionBox.height, false);
+
                     } else {
-                        
-                        mapClicked(mouse.x, mouse.y)
-                        if (typeof game !== 'undefined' && game.onClickSelect) {
-                            game.onClickSelect(mouse.x, mouse.y, false)
-                        }
+                        mapClicked(mouse.x, mouse.y);
+                        if (typeof game !== 'undefined' && game.onClickSelect)
+                            game.onClickSelect(mouse.x, mouse.y, false);
+
                     }
                 }
                 if (mouse.button === Qt.RightButton) {
-                    renderArea.mousePanActive = false
-
-                    mainWindow.edgeScrollDisabled = (renderArea.keyPanCount > 0) || renderArea.mousePanActive
+                    renderArea.mousePanActive = false;
+                    mainWindow.edgeScrollDisabled = (renderArea.keyPanCount > 0) || renderArea.mousePanActive;
                 }
             }
         }
 
-        
         Rectangle {
             id: selectionBox
+
             visible: false
             border.color: "white"
             border.width: 1
             color: "transparent"
         }
+
     }
-    
-    
+
     Item {
         id: customCursorContainer
+
         visible: gameView.cursorMode !== "normal"
         width: 32
         height: 32
         z: 999999
-        
-        
         x: (typeof game !== 'undefined' && game.globalCursorX) ? game.globalCursorX - 16 : 0
         y: (typeof game !== 'undefined' && game.globalCursorY) ? game.globalCursorY - 16 : 0
-        
-        
+
         Item {
             id: attackCursorContainer
+
+            property real pulseScale: 1
+
             visible: gameView.cursorMode === "attack"
             anchors.fill: parent
-            
-            property real pulseScale: 1.0
-            
-            SequentialAnimation on pulseScale {
-                running: attackCursorContainer.visible
-                loops: Animation.Infinite
-                NumberAnimation { from: 1.0; to: 1.2; duration: 400; easing.type: Easing.InOutQuad }
-                NumberAnimation { from: 1.2; to: 1.0; duration: 400; easing.type: Easing.InOutQuad }
-            }
-            
+
             Canvas {
                 id: attackCursor
+
                 anchors.fill: parent
                 scale: attackCursorContainer.pulseScale
                 transformOrigin: Item.Center
-                
                 onPaint: {
-                    var ctx = getContext("2d")
-                    ctx.clearRect(0, 0, width, height)
-                    
-                    
-                    ctx.strokeStyle = "#ff4444"
-                    ctx.lineWidth = 3
-                    
-                    
-                    ctx.beginPath()
-                    ctx.moveTo(16, 4)
-                    ctx.lineTo(16, 28)
-                    ctx.stroke()
-                    
-                    
-                    ctx.beginPath()
-                    ctx.moveTo(4, 16)
-                    ctx.lineTo(28, 16)
-                    ctx.stroke()
-                    
-                    
-                    ctx.fillStyle = "#ff2222"
-                    ctx.beginPath()
-                    ctx.arc(16, 16, 4, 0, Math.PI * 2)
-                    ctx.fill()
-                    
-                    
-                    ctx.strokeStyle = "rgba(255, 68, 68, 0.5)"
-                    ctx.lineWidth = 1
-                    ctx.beginPath()
-                    ctx.arc(16, 16, 7, 0, Math.PI * 2)
-                    ctx.stroke()
-                    
-                    
-                    ctx.strokeStyle = "#e74c3c"
-                    ctx.lineWidth = 2
-                    
-                    
-                    ctx.beginPath()
-                    ctx.moveTo(8, 12)
-                    ctx.lineTo(8, 8)
-                    ctx.lineTo(12, 8)
-                    ctx.stroke()
-                    
-                    
-                    ctx.beginPath()
-                    ctx.moveTo(20, 8)
-                    ctx.lineTo(24, 8)
-                    ctx.lineTo(24, 12)
-                    ctx.stroke()
-                    
-                    
-                    ctx.beginPath()
-                    ctx.moveTo(8, 20)
-                    ctx.lineTo(8, 24)
-                    ctx.lineTo(12, 24)
-                    ctx.stroke()
-                    
-                    
-                    ctx.beginPath()
-                    ctx.moveTo(20, 24)
-                    ctx.lineTo(24, 24)
-                    ctx.lineTo(24, 20)
-                    ctx.stroke()
+                    var ctx = getContext("2d");
+                    ctx.clearRect(0, 0, width, height);
+                    ctx.strokeStyle = "#ff4444";
+                    ctx.lineWidth = 3;
+                    ctx.beginPath();
+                    ctx.moveTo(16, 4);
+                    ctx.lineTo(16, 28);
+                    ctx.stroke();
+                    ctx.beginPath();
+                    ctx.moveTo(4, 16);
+                    ctx.lineTo(28, 16);
+                    ctx.stroke();
+                    ctx.fillStyle = "#ff2222";
+                    ctx.beginPath();
+                    ctx.arc(16, 16, 4, 0, Math.PI * 2);
+                    ctx.fill();
+                    ctx.strokeStyle = "rgba(255, 68, 68, 0.5)";
+                    ctx.lineWidth = 1;
+                    ctx.beginPath();
+                    ctx.arc(16, 16, 7, 0, Math.PI * 2);
+                    ctx.stroke();
+                    ctx.strokeStyle = "#e74c3c";
+                    ctx.lineWidth = 2;
+                    ctx.beginPath();
+                    ctx.moveTo(8, 12);
+                    ctx.lineTo(8, 8);
+                    ctx.lineTo(12, 8);
+                    ctx.stroke();
+                    ctx.beginPath();
+                    ctx.moveTo(20, 8);
+                    ctx.lineTo(24, 8);
+                    ctx.lineTo(24, 12);
+                    ctx.stroke();
+                    ctx.beginPath();
+                    ctx.moveTo(8, 20);
+                    ctx.lineTo(8, 24);
+                    ctx.lineTo(12, 24);
+                    ctx.stroke();
+                    ctx.beginPath();
+                    ctx.moveTo(20, 24);
+                    ctx.lineTo(24, 24);
+                    ctx.lineTo(24, 20);
+                    ctx.stroke();
                 }
-                
                 Component.onCompleted: requestPaint()
             }
+
+            SequentialAnimation on pulseScale {
+                running: attackCursorContainer.visible
+                loops: Animation.Infinite
+
+                NumberAnimation {
+                    from: 1
+                    to: 1.2
+                    duration: 400
+                    easing.type: Easing.InOutQuad
+                }
+
+                NumberAnimation {
+                    from: 1.2
+                    to: 1
+                    duration: 400
+                    easing.type: Easing.InOutQuad
+                }
+
+            }
+
         }
-        
-        
+
         Canvas {
             id: guardCursor
+
             visible: gameView.cursorMode === "guard"
             anchors.fill: parent
             onPaint: {
-                var ctx = getContext("2d")
-                ctx.clearRect(0, 0, width, height)
-                
-                
-                ctx.fillStyle = "#3498db"
-                ctx.strokeStyle = "#2980b9"
-                ctx.lineWidth = 2
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(16, 6)
-                ctx.lineTo(24, 10)
-                ctx.lineTo(24, 18)
-                ctx.lineTo(16, 26)
-                ctx.lineTo(8, 18)
-                ctx.lineTo(8, 10)
-                ctx.closePath()
-                ctx.fill()
-                ctx.stroke()
-                
-                
-                ctx.strokeStyle = "#ecf0f1"
-                ctx.lineWidth = 2
-                ctx.beginPath()
-                ctx.moveTo(13, 16)
-                ctx.lineTo(15, 18)
-                ctx.lineTo(19, 12)
-                ctx.stroke()
+                var ctx = getContext("2d");
+                ctx.clearRect(0, 0, width, height);
+                ctx.fillStyle = "#3498db";
+                ctx.strokeStyle = "#2980b9";
+                ctx.lineWidth = 2;
+                ctx.beginPath();
+                ctx.moveTo(16, 6);
+                ctx.lineTo(24, 10);
+                ctx.lineTo(24, 18);
+                ctx.lineTo(16, 26);
+                ctx.lineTo(8, 18);
+                ctx.lineTo(8, 10);
+                ctx.closePath();
+                ctx.fill();
+                ctx.stroke();
+                ctx.strokeStyle = "#ecf0f1";
+                ctx.lineWidth = 2;
+                ctx.beginPath();
+                ctx.moveTo(13, 16);
+                ctx.lineTo(15, 18);
+                ctx.lineTo(19, 12);
+                ctx.stroke();
             }
             Component.onCompleted: requestPaint()
         }
-        
-        
+
         Canvas {
             id: patrolCursor
+
             visible: gameView.cursorMode === "patrol"
             anchors.fill: parent
             onPaint: {
-                var ctx = getContext("2d")
-                ctx.clearRect(0, 0, width, height)
-                
-                
-                ctx.strokeStyle = "#27ae60"
-                ctx.lineWidth = 2
-                
-                
-                ctx.beginPath()
-                ctx.arc(16, 16, 10, 0, Math.PI * 2)
-                ctx.stroke()
-                
-                
-                ctx.fillStyle = "#27ae60"
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(26, 16)
-                ctx.lineTo(22, 13)
-                ctx.lineTo(22, 19)
-                ctx.closePath()
-                ctx.fill()
-                
-                
-                ctx.beginPath()
-                ctx.moveTo(6, 16)
-                ctx.lineTo(10, 13)
-                ctx.lineTo(10, 19)
-                ctx.closePath()
-                ctx.fill()
-                
-                
-                ctx.beginPath()
-                ctx.arc(16, 16, 3, 0, Math.PI * 2)
-                ctx.fill()
+                var ctx = getContext("2d");
+                ctx.clearRect(0, 0, width, height);
+                ctx.strokeStyle = "#27ae60";
+                ctx.lineWidth = 2;
+                ctx.beginPath();
+                ctx.arc(16, 16, 10, 0, Math.PI * 2);
+                ctx.stroke();
+                ctx.fillStyle = "#27ae60";
+                ctx.beginPath();
+                ctx.moveTo(26, 16);
+                ctx.lineTo(22, 13);
+                ctx.lineTo(22, 19);
+                ctx.closePath();
+                ctx.fill();
+                ctx.beginPath();
+                ctx.moveTo(6, 16);
+                ctx.lineTo(10, 13);
+                ctx.lineTo(10, 19);
+                ctx.closePath();
+                ctx.fill();
+                ctx.beginPath();
+                ctx.arc(16, 16, 3, 0, Math.PI * 2);
+                ctx.fill();
             }
             Component.onCompleted: requestPaint()
         }
-    }
-    
-    
-    
-    
-    Keys.onPressed: function(event) {
-        if (typeof game === 'undefined') return
-    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) {
-                    mainWindow.menuVisible = true
-                    event.accepted = true
-                }
-                break
-            
-            case Qt.Key_Space:
-                if (typeof mainWindow !== 'undefined') {
-                    mainWindow.gamePaused = !mainWindow.gamePaused
-                    gameView.setPaused(mainWindow.gamePaused)
-                    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, 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
-            
-            
-            
-            
-            
-            var shiftHeld = (event.modifiers & Qt.ShiftModifier) !== 0
-            var pitchStep = shiftHeld ? 8 : 4
-            
-            case Qt.Key_R: game.cameraOrbitDirection(1, shiftHeld);  event.accepted = true; break
-            case Qt.Key_F: game.cameraOrbitDirection(-1, shiftHeld); event.accepted = true; break
-            
-            case Qt.Key_X:
-                game.selectAllTroops()
-                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) {
-            
-            if (renderArea.keyPanCount === 0 && !renderArea.mousePanActive) {
-                mainWindow.edgeScrollDisabled = false
-            }
-        }
+
     }
 
-    focus: true
-}
+}

+ 60 - 54
ui/qml/HUD.qml

@@ -4,112 +4,118 @@ import QtQuick.Layouts 2.15
 
 Item {
     id: hud
-    
-    signal pauseToggled()
-    signal speedChanged(real speed)
-    signal commandModeChanged(string mode) 
-    signal recruit(string unitType)
-    
+
     property bool gameIsPaused: false
-    property real currentSpeed: 1.0
+    property real currentSpeed: 1
     property string currentCommandMode: "normal"
-    
-    
     property int topPanelHeight: topPanel.height
     property int bottomPanelHeight: bottomPanel.height
-    
     property int selectionTick: 0
     property bool hasMovableUnits: false
 
+    signal pauseToggled()
+    signal speedChanged(real speed)
+    signal commandModeChanged(string mode)
+    signal recruit(string unitType)
+
     Connections {
-        target: (typeof game !== 'undefined') ? game : null
-        function onSelectedUnitsChanged() { 
-            selectionTick += 1
-            
-            
-            var hasTroops = false
+        function onSelectedUnitsChanged() {
+            selectionTick += 1;
+            var hasTroops = false;
             if (typeof game !== 'undefined' && game.hasUnitsSelected && game.hasSelectedType) {
-                
-                var troopTypes = ["warrior", "archer"]
+                var troopTypes = ["warrior", "archer"];
                 for (var i = 0; i < troopTypes.length; i++) {
                     if (game.hasSelectedType(troopTypes[i])) {
-                        hasTroops = true
-                        break
+                        hasTroops = true;
+                        break;
                     }
                 }
             }
-            
-            
-            var actualMode = "normal"
-            if (hasTroops && typeof game !== 'undefined' && game.getSelectedUnitsCommandMode) {
-                actualMode = game.getSelectedUnitsCommandMode()
-            }
-            
-            
+            var actualMode = "normal";
+            if (hasTroops && typeof game !== 'undefined' && game.getSelectedUnitsCommandMode)
+                actualMode = game.getSelectedUnitsCommandMode();
+
             if (currentCommandMode !== actualMode) {
-                currentCommandMode = actualMode
-                commandModeChanged(actualMode)
+                currentCommandMode = actualMode;
+                commandModeChanged(actualMode);
             }
-            
-            hasMovableUnits = hasTroops
+            hasMovableUnits = hasTroops;
         }
+
+        target: (typeof game !== 'undefined') ? game : null
     }
 
-    
     Timer {
         id: productionRefresh
+
         interval: 100
         repeat: true
         running: true
         onTriggered: {
-            selectionTick += 1
-            
-            
+            selectionTick += 1;
             if (hasMovableUnits && typeof game !== 'undefined' && game.getSelectedUnitsCommandMode) {
-                var actualMode = game.getSelectedUnitsCommandMode()
-                if (currentCommandMode !== actualMode) {
-                    currentCommandMode = actualMode
-                    
-                }
+                var actualMode = game.getSelectedUnitsCommandMode();
+                if (currentCommandMode !== actualMode)
+                    currentCommandMode = actualMode;
+
             }
         }
     }
-    
-    
+
     Item {
         id: topPanel
+
         anchors.top: parent.top
         anchors.left: parent.left
         anchors.right: parent.right
         height: Math.max(50, parent.height * 0.08)
+
         HUDTop {
             id: hudTop
+
             anchors.fill: parent
             gameIsPaused: hud.gameIsPaused
             currentSpeed: hud.currentSpeed
-            onPauseToggled: { hud.gameIsPaused = !hud.gameIsPaused; hud.pauseToggled(); }
-            onSpeedChanged: function(s) { hud.currentSpeed = s; hud.speedChanged(s); }
+            onPauseToggled: {
+                hud.gameIsPaused = !hud.gameIsPaused;
+                hud.pauseToggled();
+            }
+            onSpeedChanged: function(s) {
+                hud.currentSpeed = s;
+                hud.speedChanged(s);
+            }
         }
+
     }
-    
-    
+
     Item {
         id: bottomPanel
+
         anchors.bottom: parent.bottom
         anchors.left: parent.left
         anchors.right: parent.right
-        height: Math.max(140, parent.height * 0.20)
+        height: Math.max(140, parent.height * 0.2)
+
         HUDBottom {
             id: hudBottom
+
             anchors.fill: parent
             currentCommandMode: hud.currentCommandMode
             selectionTick: hud.selectionTick
             hasMovableUnits: hud.hasMovableUnits
-            onCommandModeChanged: function(m) { hud.currentCommandMode = m; hud.commandModeChanged(m); }
-            onRecruit: function(t) { hud.recruit(t); }
+            onCommandModeChanged: function(m) {
+                hud.currentCommandMode = m;
+                hud.commandModeChanged(m);
+            }
+            onRecruit: function(t) {
+                hud.recruit(t);
+            }
         }
+
+    }
+
+    HUDVictory {
+        anchors.fill: parent
     }
-    
-    
-    HUDVictory { anchors.fill: parent }
-}
+
+}

+ 319 - 48
ui/qml/HUDBottom.qml

@@ -4,24 +4,21 @@ import QtQuick.Layouts 2.15
 
 RowLayout {
     id: bottomRoot
-    
-    
+
     property string currentCommandMode
     property int selectionTick
     property bool hasMovableUnits
-    
-    
+
     signal commandModeChanged(string mode)
     signal recruit(string unitType)
-    
+
     anchors.fill: parent
     anchors.margins: 10
     spacing: 12
 
-    
     Rectangle {
         Layout.fillWidth: true
-        Layout.preferredWidth: Math.max(240, bottomPanel.width * 0.30)
+        Layout.preferredWidth: Math.max(240, bottomPanel.width * 0.3)
         Layout.fillHeight: true
         Layout.alignment: Qt.AlignTop
         color: "#0f1419"
@@ -34,8 +31,20 @@ RowLayout {
             anchors.margins: 6
             spacing: 6
 
-            Rectangle { width: parent.width; height: 25; color: "#1a252f"; radius: 4
-                Text { anchors.centerIn: parent; text: "SELECTED UNITS"; color: "#3498db"; font.pointSize: 10; font.bold: true }
+            Rectangle {
+                width: parent.width
+                height: 25
+                color: "#1a252f"
+                radius: 4
+
+                Text {
+                    anchors.centerIn: parent
+                    text: "SELECTED UNITS"
+                    color: "#3498db"
+                    font.pointSize: 10
+                    font.bold: true
+                }
+
             }
 
             ScrollView {
@@ -49,6 +58,7 @@ RowLayout {
 
                 ListView {
                     id: selectedUnitsList
+
                     model: (typeof game !== 'undefined' && game.selectedUnitsModel) ? game.selectedUnitsModel : null
                     boundsBehavior: Flickable.StopAtBounds
                     flickableDirection: Flickable.VerticalFlick
@@ -88,25 +98,35 @@ RowLayout {
                                     width: parent.width * (typeof healthRatio !== 'undefined' ? healthRatio : 0)
                                     height: parent.height
                                     color: {
-                                        var ratio = (typeof healthRatio !== 'undefined' ? healthRatio : 0)
-                                        if (ratio > 0.6) return "#27ae60"
-                                        if (ratio > 0.3) return "#f39c12"
-                                        return "#e74c3c"
+                                        var ratio = (typeof healthRatio !== 'undefined' ? healthRatio : 0);
+                                        if (ratio > 0.6)
+                                            return "#27ae60";
+
+                                        if (ratio > 0.3)
+                                            return "#f39c12";
+
+                                        return "#e74c3c";
                                     }
                                     radius: 6
                                 }
+
                             }
+
                         }
+
                     }
+
                 }
+
             }
+
         }
+
     }
 
-    
     Column {
         Layout.fillWidth: true
-        Layout.preferredWidth: Math.max(320, bottomPanel.width * 0.40)
+        Layout.preferredWidth: Math.max(320, bottomPanel.width * 0.4)
         Layout.fillHeight: true
         Layout.alignment: Qt.AlignTop
         spacing: 8
@@ -118,50 +138,240 @@ RowLayout {
             border.color: bottomRoot.currentCommandMode === "normal" ? "#34495e" : (bottomRoot.currentCommandMode === "attack" ? "#e74c3c" : "#3498db")
             border.width: 2
             radius: 6
-            opacity: bottomRoot.hasMovableUnits ? 1.0 : 0.5
-
-            SequentialAnimation on opacity {
-                running: bottomRoot.currentCommandMode === "attack" && bottomRoot.hasMovableUnits
-                loops: Animation.Infinite
-                NumberAnimation { from: 0.8; to: 1.0; duration: 600 }
-                NumberAnimation { from: 1.0; to: 0.8; duration: 600 }
+            opacity: bottomRoot.hasMovableUnits ? 1 : 0.5
+
+            Rectangle {
+                anchors.fill: parent
+                anchors.margins: -4
+                color: "transparent"
+                border.color: bottomRoot.currentCommandMode === "attack" ? "#e74c3c" : "#3498db"
+                border.width: bottomRoot.currentCommandMode !== "normal" && bottomRoot.hasMovableUnits ? 1 : 0
+                radius: 8
+                opacity: 0.4
+                visible: bottomRoot.currentCommandMode !== "normal" && bottomRoot.hasMovableUnits
             }
 
-            Rectangle { anchors.fill: parent; anchors.margins: -4; color: "transparent"; border.color: bottomRoot.currentCommandMode === "attack" ? "#e74c3c" : "#3498db"; border.width: bottomRoot.currentCommandMode !== "normal" && bottomRoot.hasMovableUnits ? 1 : 0; radius: 8; opacity: 0.4; visible: bottomRoot.currentCommandMode !== "normal" && bottomRoot.hasMovableUnits }
-            Text { 
+            Text {
                 anchors.centerIn: parent
                 text: !bottomRoot.hasMovableUnits ? "◉ Select Troops for Commands" : (bottomRoot.currentCommandMode === "normal" ? "◉ Normal Mode" : bottomRoot.currentCommandMode === "attack" ? "⚔️ ATTACK MODE - Click Enemy" : bottomRoot.currentCommandMode === "guard" ? "🛡️ GUARD MODE - Click Position" : bottomRoot.currentCommandMode === "patrol" ? "🚶 PATROL MODE - Set Waypoints" : "⏹️ STOP COMMAND")
                 color: !bottomRoot.hasMovableUnits ? "#5a6c7d" : (bottomRoot.currentCommandMode === "normal" ? "#7f8c8d" : (bottomRoot.currentCommandMode === "attack" ? "#ff6b6b" : "#3498db"))
                 font.pointSize: bottomRoot.currentCommandMode === "normal" ? 10 : 11
                 font.bold: bottomRoot.currentCommandMode !== "normal" && bottomRoot.hasMovableUnits
             }
+
+            SequentialAnimation on opacity {
+                running: bottomRoot.currentCommandMode === "attack" && bottomRoot.hasMovableUnits
+                loops: Animation.Infinite
+
+                NumberAnimation {
+                    from: 0.8
+                    to: 1
+                    duration: 600
+                }
+
+                NumberAnimation {
+                    from: 1
+                    to: 0.8
+                    duration: 600
+                }
+
+            }
+
         }
 
         GridLayout {
+            function getButtonColor(btn, baseColor) {
+                if (btn.pressed)
+                    return Qt.darker(baseColor, 1.3);
+
+                if (btn.checked)
+                    return baseColor;
+
+                if (btn.hovered)
+                    return Qt.lighter(baseColor, 1.2);
+
+                return "#2c3e50";
+            }
+
             width: parent.width
             columns: 3
             rowSpacing: 6
             columnSpacing: 6
 
-            function getButtonColor(btn, baseColor) { if (btn.pressed) return Qt.darker(baseColor, 1.3); if (btn.checked) return baseColor; if (btn.hovered) return Qt.lighter(baseColor, 1.2); return "#2c3e50" }
+            Button {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 38
+                text: "Attack"
+                focusPolicy: Qt.NoFocus
+                enabled: bottomRoot.hasMovableUnits
+                checkable: true
+                checked: bottomRoot.currentCommandMode === "attack" && bottomRoot.hasMovableUnits
+                onClicked: {
+                    bottomRoot.commandModeChanged(checked ? "attack" : "normal");
+                }
+                ToolTip.visible: hovered
+                ToolTip.text: bottomRoot.hasMovableUnits ? "Attack enemy units or buildings.\nUnits will chase targets." : "Select troops first"
+                ToolTip.delay: 500
+
+                background: Rectangle {
+                    color: parent.enabled ? (parent.checked ? "#e74c3c" : (parent.hovered ? "#c0392b" : "#34495e")) : "#1a252f"
+                    radius: 6
+                    border.color: parent.checked ? "#c0392b" : "#1a252f"
+                    border.width: 2
+                }
 
-            
-            Button { Layout.fillWidth: true; Layout.preferredHeight: 38; text: "Attack"; focusPolicy: Qt.NoFocus; enabled: bottomRoot.hasMovableUnits; checkable: true; checked: bottomRoot.currentCommandMode === "attack" && bottomRoot.hasMovableUnits; background: Rectangle { color: parent.enabled ? (parent.checked ? "#e74c3c" : (parent.hovered ? "#c0392b" : "#34495e")) : "#1a252f"; radius: 6; border.color: parent.checked ? "#c0392b" : "#1a252f"; border.width: 2 } contentItem: Text { text: "⚔️\n" + parent.text; font.pointSize: 8; font.bold: true; color: parent.enabled ? "#ecf0f1" : "#7f8c8d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } onClicked: { bottomRoot.commandModeChanged(checked ? "attack" : "normal") } ToolTip.visible: hovered; ToolTip.text: bottomRoot.hasMovableUnits ? "Attack enemy units or buildings.\nUnits will chase targets." : "Select troops first"; ToolTip.delay: 500 }
+                contentItem: Text {
+                    text: "⚔️\n" + parent.text
+                    font.pointSize: 8
+                    font.bold: true
+                    color: parent.enabled ? "#ecf0f1" : "#7f8c8d"
+                    horizontalAlignment: Text.AlignHCenter
+                    verticalAlignment: Text.AlignVCenter
+                }
 
-            Button { Layout.fillWidth: true; Layout.preferredHeight: 38; text: "Guard"; focusPolicy: Qt.NoFocus; enabled: bottomRoot.hasMovableUnits; checkable: true; checked: bottomRoot.currentCommandMode === "guard" && bottomRoot.hasMovableUnits; background: Rectangle { color: parent.enabled ? (parent.checked ? "#3498db" : (parent.hovered ? "#2980b9" : "#34495e")) : "#1a252f"; radius: 6; border.color: parent.checked ? "#2980b9" : "#1a252f"; border.width: 2 } contentItem: Text { text: "🛡️\n" + parent.text; font.pointSize: 8; font.bold: true; color: parent.enabled ? "#ecf0f1" : "#7f8c8d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } onClicked: { bottomRoot.commandModeChanged(checked ? "guard" : "normal") } ToolTip.visible: hovered; ToolTip.text: bottomRoot.hasMovableUnits ? "Guard a position.\nUnits will defend from all sides." : "Select troops first"; ToolTip.delay: 500 }
+            }
 
-            Button { Layout.fillWidth: true; Layout.preferredHeight: 38; text: "Patrol"; focusPolicy: Qt.NoFocus; enabled: bottomRoot.hasMovableUnits; checkable: true; checked: bottomRoot.currentCommandMode === "patrol" && bottomRoot.hasMovableUnits; background: Rectangle { color: parent.enabled ? (parent.checked ? "#27ae60" : (parent.hovered ? "#229954" : "#34495e")) : "#1a252f"; radius: 6; border.color: parent.checked ? "#229954" : "#1a252f"; border.width: 2 } contentItem: Text { text: "🚶\n" + parent.text; font.pointSize: 8; font.bold: true; color: parent.enabled ? "#ecf0f1" : "#7f8c8d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } onClicked: { bottomRoot.commandModeChanged(checked ? "patrol" : "normal") } ToolTip.visible: hovered; ToolTip.text: bottomRoot.hasMovableUnits ? "Patrol between waypoints.\nClick start and end points." : "Select troops first"; ToolTip.delay: 500 }
+            Button {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 38
+                text: "Guard"
+                focusPolicy: Qt.NoFocus
+                enabled: bottomRoot.hasMovableUnits
+                checkable: true
+                checked: bottomRoot.currentCommandMode === "guard" && bottomRoot.hasMovableUnits
+                onClicked: {
+                    bottomRoot.commandModeChanged(checked ? "guard" : "normal");
+                }
+                ToolTip.visible: hovered
+                ToolTip.text: bottomRoot.hasMovableUnits ? "Guard a position.\nUnits will defend from all sides." : "Select troops first"
+                ToolTip.delay: 500
+
+                background: Rectangle {
+                    color: parent.enabled ? (parent.checked ? "#3498db" : (parent.hovered ? "#2980b9" : "#34495e")) : "#1a252f"
+                    radius: 6
+                    border.color: parent.checked ? "#2980b9" : "#1a252f"
+                    border.width: 2
+                }
+
+                contentItem: Text {
+                    text: "🛡️\n" + parent.text
+                    font.pointSize: 8
+                    font.bold: true
+                    color: parent.enabled ? "#ecf0f1" : "#7f8c8d"
+                    horizontalAlignment: Text.AlignHCenter
+                    verticalAlignment: Text.AlignVCenter
+                }
+
+            }
 
-            Button { Layout.fillWidth: true; Layout.preferredHeight: 38; text: "Stop"; focusPolicy: Qt.NoFocus; enabled: bottomRoot.hasMovableUnits; background: Rectangle { color: parent.enabled ? (parent.pressed ? "#d35400" : (parent.hovered ? "#e67e22" : "#34495e")) : "#1a252f"; radius: 6; border.color: parent.enabled ? "#d35400" : "#1a252f"; border.width: 2 } contentItem: Text { text: "⏹️\n" + parent.text; font.pointSize: 8; font.bold: true; color: parent.enabled ? "#ecf0f1" : "#7f8c8d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } onClicked: { if (typeof game !== 'undefined' && game.onStopCommand) { game.onStopCommand() } bottomRoot.commandModeChanged("normal") } ToolTip.visible: hovered; ToolTip.text: bottomRoot.hasMovableUnits ? "Stop all actions immediately" : "Select troops first"; ToolTip.delay: 500 }
+            Button {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 38
+                text: "Patrol"
+                focusPolicy: Qt.NoFocus
+                enabled: bottomRoot.hasMovableUnits
+                checkable: true
+                checked: bottomRoot.currentCommandMode === "patrol" && bottomRoot.hasMovableUnits
+                onClicked: {
+                    bottomRoot.commandModeChanged(checked ? "patrol" : "normal");
+                }
+                ToolTip.visible: hovered
+                ToolTip.text: bottomRoot.hasMovableUnits ? "Patrol between waypoints.\nClick start and end points." : "Select troops first"
+                ToolTip.delay: 500
+
+                background: Rectangle {
+                    color: parent.enabled ? (parent.checked ? "#27ae60" : (parent.hovered ? "#229954" : "#34495e")) : "#1a252f"
+                    radius: 6
+                    border.color: parent.checked ? "#229954" : "#1a252f"
+                    border.width: 2
+                }
+
+                contentItem: Text {
+                    text: "🚶\n" + parent.text
+                    font.pointSize: 8
+                    font.bold: true
+                    color: parent.enabled ? "#ecf0f1" : "#7f8c8d"
+                    horizontalAlignment: Text.AlignHCenter
+                    verticalAlignment: Text.AlignVCenter
+                }
+
+            }
+
+            Button {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 38
+                text: "Stop"
+                focusPolicy: Qt.NoFocus
+                enabled: bottomRoot.hasMovableUnits
+                onClicked: {
+                    if (typeof game !== 'undefined' && game.onStopCommand)
+                        game.onStopCommand();
+
+                    bottomRoot.commandModeChanged("normal");
+                }
+                ToolTip.visible: hovered
+                ToolTip.text: bottomRoot.hasMovableUnits ? "Stop all actions immediately" : "Select troops first"
+                ToolTip.delay: 500
+
+                background: Rectangle {
+                    color: parent.enabled ? (parent.pressed ? "#d35400" : (parent.hovered ? "#e67e22" : "#34495e")) : "#1a252f"
+                    radius: 6
+                    border.color: parent.enabled ? "#d35400" : "#1a252f"
+                    border.width: 2
+                }
+
+                contentItem: Text {
+                    text: "⏹️\n" + parent.text
+                    font.pointSize: 8
+                    font.bold: true
+                    color: parent.enabled ? "#ecf0f1" : "#7f8c8d"
+                    horizontalAlignment: Text.AlignHCenter
+                    verticalAlignment: Text.AlignVCenter
+                }
+
+            }
+
+            Button {
+                Layout.fillWidth: true
+                Layout.preferredHeight: 38
+                text: "Hold"
+                focusPolicy: Qt.NoFocus
+                enabled: bottomRoot.hasMovableUnits
+                onClicked: {
+                    bottomRoot.commandModeChanged("hold");
+                    Qt.callLater(function() {
+                        bottomRoot.commandModeChanged("normal");
+                    });
+                }
+                ToolTip.visible: hovered
+                ToolTip.text: bottomRoot.hasMovableUnits ? "Hold position and defend" : "Select troops first"
+                ToolTip.delay: 500
+
+                background: Rectangle {
+                    color: parent.enabled ? (parent.pressed ? "#8e44ad" : (parent.hovered ? "#9b59b6" : "#34495e")) : "#1a252f"
+                    radius: 6
+                    border.color: parent.enabled ? "#8e44ad" : "#1a252f"
+                    border.width: 2
+                }
+
+                contentItem: Text {
+                    text: "📍\n" + parent.text
+                    font.pointSize: 8
+                    font.bold: true
+                    color: parent.enabled ? "#ecf0f1" : "#7f8c8d"
+                    horizontalAlignment: Text.AlignHCenter
+                    verticalAlignment: Text.AlignVCenter
+                }
+
+            }
 
-            Button { Layout.fillWidth: true; Layout.preferredHeight: 38; text: "Hold"; focusPolicy: Qt.NoFocus; enabled: bottomRoot.hasMovableUnits; background: Rectangle { color: parent.enabled ? (parent.pressed ? "#8e44ad" : (parent.hovered ? "#9b59b6" : "#34495e")) : "#1a252f"; radius: 6; border.color: parent.enabled ? "#8e44ad" : "#1a252f"; border.width: 2 } contentItem: Text { text: "📍\n" + parent.text; font.pointSize: 8; font.bold: true; color: parent.enabled ? "#ecf0f1" : "#7f8c8d"; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter } onClicked: { bottomRoot.commandModeChanged("hold"); Qt.callLater(function() { bottomRoot.commandModeChanged("normal") }) } ToolTip.visible: hovered; ToolTip.text: bottomRoot.hasMovableUnits ? "Hold position and defend" : "Select troops first"; ToolTip.delay: 500 }
         }
+
     }
 
-    
     Rectangle {
         Layout.fillWidth: true
-        Layout.preferredWidth: Math.max(240, bottomPanel.width * 0.30)
+        Layout.preferredWidth: Math.max(240, bottomPanel.width * 0.3)
         Layout.fillHeight: true
         Layout.alignment: Qt.AlignTop
         color: "#34495e"
@@ -173,10 +383,18 @@ RowLayout {
             anchors.margins: 8
             spacing: 6
 
-            Text { id: prodHeader; text: "Production"; color: "white"; font.pointSize: 11; font.bold: true }
+            Text {
+                id: prodHeader
+
+                text: "Production"
+                color: "white"
+                font.pointSize: 11
+                font.bold: true
+            }
 
             ScrollView {
                 id: prodScroll
+
                 anchors.left: parent.left
                 anchors.right: parent.right
                 anchors.top: prodHeader.bottom
@@ -190,20 +408,32 @@ RowLayout {
 
                     Repeater {
                         model: (bottomRoot.selectionTick, (typeof game !== 'undefined' && game.hasSelectedType && game.hasSelectedType("barracks"))) ? 1 : 0
+
                         delegate: Column {
+                            property var prod: (bottomRoot.selectionTick, (typeof game !== 'undefined' && game.getSelectedProductionState) ? game.getSelectedProductionState() : ({
+                            }))
+
                             spacing: 6
-                            property var prod: (bottomRoot.selectionTick, (typeof game !== 'undefined' && game.getSelectedProductionState) ? game.getSelectedProductionState() : ({}))
 
                             Button {
                                 id: recruitBtn
+
                                 text: "Recruit Archer (" + (prod.villagerCost || 1) + ")"
                                 focusPolicy: Qt.NoFocus
-                                enabled: (function(){
-                                    if (typeof prod === 'undefined' || !prod) return false
-                                    if (!prod.hasBarracks) return false
-                                    if (prod.inProgress) return false
-                                    if (prod.producedCount >= prod.maxUnits) return false
-                                    return true
+                                enabled: (function() {
+                                    if (typeof prod === 'undefined' || !prod)
+                                        return false;
+
+                                    if (!prod.hasBarracks)
+                                        return false;
+
+                                    if (prod.inProgress)
+                                        return false;
+
+                                    if (prod.producedCount >= prod.maxUnits)
+                                        return false;
+
+                                    return true;
                                 })()
                                 onClicked: bottomRoot.recruit("archer")
                                 ToolTip.visible: hovered
@@ -224,41 +454,82 @@ RowLayout {
                                     anchors.left: parent.left
                                     anchors.verticalCenter: parent.verticalCenter
                                     height: parent.height
-                                    width: parent.width * (prod.buildTime > 0 ? (1.0 - Math.max(0, prod.timeRemaining) / prod.buildTime) : 0)
+                                    width: parent.width * (prod.buildTime > 0 ? (1 - Math.max(0, prod.timeRemaining) / prod.buildTime) : 0)
                                     color: "#27ae60"
                                     radius: 4
                                 }
+
                             }
 
                             Row {
                                 spacing: 8
-                                Text { text: prod.inProgress ? ("Time left: " + Math.max(0, prod.timeRemaining).toFixed(1) + "s") : ("Build time: " + (prod.buildTime || 0).toFixed(0) + "s"); color: "#bdc3c7"; font.pointSize: 9 }
-                                Text { text: (prod.producedCount || 0) + "/" + (prod.maxUnits || 0); color: "#bdc3c7"; font.pointSize: 9 }
+
+                                Text {
+                                    text: prod.inProgress ? ("Time left: " + Math.max(0, prod.timeRemaining).toFixed(1) + "s") : ("Build time: " + (prod.buildTime || 0).toFixed(0) + "s")
+                                    color: "#bdc3c7"
+                                    font.pointSize: 9
+                                }
+
+                                Text {
+                                    text: (prod.producedCount || 0) + "/" + (prod.maxUnits || 0)
+                                    color: "#bdc3c7"
+                                    font.pointSize: 9
+                                }
+
                             }
 
-                            Text { text: (prod.producedCount >= prod.maxUnits) ? "Cap reached" : ""; color: "#e67e22"; font.pointSize: 9 }
+                            Text {
+                                text: (prod.producedCount >= prod.maxUnits) ? "Cap reached" : ""
+                                color: "#e67e22"
+                                font.pointSize: 9
+                            }
 
                             Row {
                                 spacing: 6
+
                                 Button {
                                     text: (typeof gameView !== 'undefined' && gameView.setRallyMode) ? "Click map to set rally (right-click to cancel)" : "Set Rally"
                                     focusPolicy: Qt.NoFocus
                                     enabled: !!prod.hasBarracks
-                                    onClicked: if (typeof gameView !== 'undefined') gameView.setRallyMode = !gameView.setRallyMode
+                                    onClicked: {
+                                        if (typeof gameView !== 'undefined')
+                                            gameView.setRallyMode = !gameView.setRallyMode;
+
+                                    }
                                 }
-                                Text { text: (typeof gameView !== 'undefined' && gameView.setRallyMode) ? "Click on the map" : ""; color: "#bdc3c7"; font.pointSize: 9 }
+
+                                Text {
+                                    text: (typeof gameView !== 'undefined' && gameView.setRallyMode) ? "Click on the map" : ""
+                                    color: "#bdc3c7"
+                                    font.pointSize: 9
+                                }
+
                             }
+
                         }
+
                     }
 
                     Item {
                         visible: (bottomRoot.selectionTick, (typeof game === 'undefined' || !game.hasSelectedType || !game.hasSelectedType("barracks")))
                         width: parent.width
                         height: 30
-                        Text { text: "No production"; color: "#7f8c8d"; anchors.centerIn: parent; font.pointSize: 10 }
+
+                        Text {
+                            text: "No production"
+                            color: "#7f8c8d"
+                            anchors.centerIn: parent
+                            font.pointSize: 10
+                        }
+
                     }
+
                 }
+
             }
+
         }
+
     }
+
 }

+ 200 - 82
ui/qml/HUDTop.qml

@@ -4,18 +4,19 @@ import QtQuick.Layouts 2.15
 
 Item {
     id: topRoot
-    property bool gameIsPaused: false
-    property real currentSpeed: 1.0
-    signal pauseToggled()
-    signal speedChanged(real speed)
 
-    
+    property bool gameIsPaused: false
+    property real currentSpeed: 1
     readonly property int barMinHeight: 72
     readonly property bool compact: width < 800
     readonly property bool ultraCompact: width < 560
 
+    signal pauseToggled()
+    signal speedChanged(real speed)
+
     Rectangle {
         id: topPanel
+
         anchors.left: parent.left
         anchors.right: parent.right
         anchors.top: parent.top
@@ -24,58 +25,82 @@ Item {
         opacity: 0.98
         clip: true
 
-        
         Rectangle {
             anchors.fill: parent
+            opacity: 0.9
+
             gradient: Gradient {
-                GradientStop { position: 0.0; color: "#22303a" }
-                GradientStop { position: 1.0; color: "#0f1a22" }
+                GradientStop {
+                    position: 0
+                    color: "#22303a"
+                }
+
+                GradientStop {
+                    position: 1
+                    color: "#0f1a22"
+                }
+
             }
-            opacity: 0.9
+
         }
 
-        
         Rectangle {
             anchors.left: parent.left
             anchors.right: parent.right
             anchors.bottom: parent.bottom
             height: 2
+
             gradient: Gradient {
-                GradientStop { position: 0.0; color: "transparent" }
-                GradientStop { position: 0.5; color: "#3498db" }
-                GradientStop { position: 1.0; color: "transparent" }
+                GradientStop {
+                    position: 0
+                    color: "transparent"
+                }
+
+                GradientStop {
+                    position: 0.5
+                    color: "#3498db"
+                }
+
+                GradientStop {
+                    position: 1
+                    color: "transparent"
+                }
+
             }
+
         }
 
-        
         RowLayout {
             id: barRow
+
             anchors.fill: parent
             anchors.margins: 8
             spacing: 12
 
-            
             RowLayout {
                 id: leftGroup
+
                 spacing: 10
                 Layout.alignment: Qt.AlignVCenter
 
-                
                 Button {
                     id: pauseBtn
+
                     Layout.preferredWidth: topRoot.compact ? 48 : 56
                     Layout.preferredHeight: Math.min(40, topPanel.height - 12)
-                    text: topRoot.gameIsPaused ? "\u25B6" : "\u23F8" 
+                    text: topRoot.gameIsPaused ? "\u25B6" : "\u23F8"
                     font.pixelSize: 26
                     font.bold: true
                     focusPolicy: Qt.NoFocus
+                    onClicked: topRoot.pauseToggled()
+
                     background: Rectangle {
-                        color: parent.pressed ? "#e74c3c"
-                              : parent.hovered ? "#c0392b" : "#34495e"
+                        color: parent.pressed ? "#e74c3c" : parent.hovered ? "#c0392b" : "#34495e"
                         radius: 6
                         border.color: "#2c3e50"
                         border.width: 1
                     }
+
                     contentItem: Text {
                         text: parent.text
                         font: parent.font
@@ -83,21 +108,35 @@ Item {
                         horizontalAlignment: Text.AlignHCenter
                         verticalAlignment: Text.AlignVCenter
                     }
-                    onClicked: topRoot.pauseToggled()
+
                 }
 
-                
                 Rectangle {
-                    width: 2; Layout.fillHeight: true; radius: 1
+                    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" }
+                        GradientStop {
+                            position: 0
+                            color: "transparent"
+                        }
+
+                        GradientStop {
+                            position: 0.5
+                            color: "#34495e"
+                        }
+
+                        GradientStop {
+                            position: 1
+                            color: "transparent"
+                        }
+
                     }
+
                 }
 
-                
                 RowLayout {
                     spacing: 8
                     Layout.alignment: Qt.AlignVCenter
@@ -113,29 +152,38 @@ Item {
 
                     Row {
                         id: speedRow
+
+                        property var options: [0.5, 1, 2]
+
                         spacing: 8
                         visible: !topRoot.compact
 
-                        property var options: [0.5, 1.0, 2.0]
-                        ButtonGroup { id: speedGroup }
+                        ButtonGroup {
+                            id: speedGroup
+                        }
 
                         Repeater {
                             model: speedRow.options
+
                             delegate: Button {
                                 Layout.minimumWidth: 48
-                                width: 56; height: Math.min(34, topPanel.height - 16)
+                                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"
+                                ButtonGroup.group: speedGroup
+                                onClicked: topRoot.speedChanged(modelData)
+
                                 background: Rectangle {
-                                    color: parent.checked ? "#27ae60"
-                                          : parent.hovered ? "#34495e" : "#2c3e50"
+                                    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
@@ -144,40 +192,56 @@ Item {
                                     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
+                        currentIndex: topRoot.currentSpeed === 0.5 ? 0 : topRoot.currentSpeed === 1 ? 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)
+                            var v = i === 0 ? 0.5 : (i === 1 ? 1 : 2);
+                            topRoot.speedChanged(v);
                         }
                     }
+
                 }
 
-                
                 Rectangle {
-                    width: 2; Layout.fillHeight: true; radius: 1
+                    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" }
+                        GradientStop {
+                            position: 0
+                            color: "transparent"
+                        }
+
+                        GradientStop {
+                            position: 0.5
+                            color: "#34495e"
+                        }
+
+                        GradientStop {
+                            position: 1
+                            color: "transparent"
+                        }
+
                     }
+
                 }
 
-                
                 RowLayout {
                     spacing: 8
                     Layout.alignment: Qt.AlignVCenter
@@ -193,70 +257,104 @@ Item {
 
                     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
+                        onToggled: {
+                            if (typeof game !== 'undefined' && game.cameraFollowSelection)
+                                game.cameraFollowSelection(checked);
+
+                        }
+
                         background: Rectangle {
-                            color: parent.checked ? "#3498db"
-                                  : parent.hovered ? "#34495e" : "#2c3e50"
+                            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) }
+
+                        contentItem: Text {
+                            text: parent.text
+                            font: parent.font
+                            color: "#ecf0f1"
+                            horizontalAlignment: Text.AlignHCenter
+                            verticalAlignment: Text.AlignVCenter
+                        }
+
                     }
 
                     Button {
                         id: resetBtn
+
                         Layout.preferredWidth: topRoot.compact ? 44 : 80
                         Layout.preferredHeight: Math.min(34, topPanel.height - 16)
-                        text: topRoot.compact ? "\u21BA" : "Reset" 
+                        text: topRoot.compact ? "\u21BA" : "Reset"
                         font.pixelSize: 13
                         focusPolicy: Qt.NoFocus
+                        onClicked: {
+                            if (typeof game !== 'undefined' && game.resetCamera)
+                                game.resetCamera();
+
+                        }
+
                         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() }
+
+                        contentItem: Text {
+                            text: parent.text
+                            font: parent.font
+                            color: "#ecf0f1"
+                            horizontalAlignment: Text.AlignHCenter
+                            verticalAlignment: Text.AlignVCenter
+                        }
+
                     }
+
                 }
+
             }
 
-            
-            Item { Layout.fillWidth: true }
+            Item {
+                Layout.fillWidth: true
+            }
 
-            
             RowLayout {
                 id: rightGroup
+
                 spacing: 12
                 Layout.alignment: Qt.AlignVCenter
 
-                
                 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)
+
+                        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"
+                            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
@@ -274,16 +372,21 @@ Item {
 
                     Label {
                         id: ownersLbl
+
                         text: {
-                            if (typeof game === 'undefined') return "Players: 0"
-                            var owners = game.ownerInfo
-                            var playerCount = 0
-                            var aiCount = 0
+                            if (typeof game === 'undefined')
+                                return "Players: 0";
+
+                            var owners = game.ownerInfo;
+                            var playerCount = 0;
+                            var aiCount = 0;
                             for (var i = 0; i < owners.length; i++) {
-                                if (owners[i].type === "Player") playerCount++
-                                else if (owners[i].type === "AI") aiCount++
+                                if (owners[i].type === "Player")
+                                    playerCount++;
+                                else if (owners[i].type === "AI")
+                                    aiCount++;
                             }
-                            return "👥 " + playerCount + " | 🤖 " + aiCount
+                            return "👥 " + playerCount + " | 🤖 " + aiCount;
                         }
                         color: "#ecf0f1"
                         font.pixelSize: 13
@@ -293,39 +396,48 @@ Item {
                         ToolTip.visible: ma.containsMouse
                         ToolTip.delay: 500
                         ToolTip.text: {
-                            if (typeof game === 'undefined') return ""
-                            var owners = game.ownerInfo
-                            var tip = "Owner IDs:\n"
+                            if (typeof game === 'undefined')
+                                return "";
+
+                            var owners = game.ownerInfo;
+                            var tip = "Owner IDs:\n";
                             for (var i = 0; i < owners.length; i++) {
-                                tip += owners[i].id + ": " + owners[i].name + " (" + owners[i].type + ")"
-                                if (owners[i].isLocal) tip += " [You]"
-                                tip += "\n"
+                                tip += owners[i].id + ": " + owners[i].name + " (" + owners[i].type + ")";
+                                if (owners[i].isLocal)
+                                    tip += " [You]";
+
+                                tip += "\n";
                             }
-                            return tip
+                            return tip;
                         }
+
                         MouseArea {
                             id: ma
+
                             anchors.fill: parent
                             hoverEnabled: true
                         }
+
                     }
 
                     Label {
                         id: enemyLbl
+
                         text: "💀 " + (typeof game !== 'undefined' ? game.enemyTroopsDefeated : 0)
                         color: "#ecf0f1"
                         font.pixelSize: 14
                         elide: Text.ElideRight
                         verticalAlignment: Text.AlignVCenter
                     }
+
                 }
 
-                
                 Item {
                     id: miniWrap
+
                     visible: !topRoot.ultraCompact
-                    Layout.preferredWidth: Math.round( topPanel.height * 2.2 )
-                    Layout.minimumWidth: Math.round( topPanel.height * 1.6 )
+                    Layout.preferredWidth: Math.round(topPanel.height * 2.2)
+                    Layout.minimumWidth: Math.round(topPanel.height * 1.6)
                     Layout.preferredHeight: topPanel.height - 8
 
                     Rectangle {
@@ -341,7 +453,6 @@ Item {
                             radius: 6
                             color: "#0a0f14"
 
-                            
                             Label {
                                 anchors.centerIn: parent
                                 text: "MINIMAP"
@@ -349,10 +460,17 @@ Item {
                                 font.pixelSize: 12
                                 font.bold: true
                             }
+
                         }
+
                     }
+
                 }
+
             }
+
         }
+
     }
+
 }

+ 6 - 3
ui/qml/HUDVictory.qml

@@ -3,6 +3,7 @@ import QtQuick.Controls 2.15
 
 Rectangle {
     id: victoryOverlay
+
     anchors.fill: parent
     color: Qt.rgba(0, 0, 0, 0.7)
     visible: (typeof game !== 'undefined' && game.victoryState !== "")
@@ -14,6 +15,7 @@ Rectangle {
 
         Text {
             id: victoryText
+
             anchors.horizontalCenter: parent.horizontalCenter
             text: (typeof game !== 'undefined' && game.victoryState === "victory") ? "VICTORY!" : "DEFEAT"
             color: (typeof game !== 'undefined' && game.victoryState === "victory") ? "#27ae60" : "#e74c3c"
@@ -34,9 +36,10 @@ Rectangle {
             font.pointSize: 14
             focusPolicy: Qt.NoFocus
             onClicked: {
-                
-                victoryOverlay.visible = false
+                victoryOverlay.visible = false;
             }
         }
+
     }
-}
+
+}

+ 172 - 158
ui/qml/Main.qml

@@ -1,80 +1,77 @@
-
 import QtQuick 2.15
-import QtQuick.Window 2.15
 import QtQuick.Controls 2.15
+import QtQuick.Window 2.15
 
 ApplicationWindow {
     id: mainWindow
+
+    property alias gameView: gameViewItem
+    property bool menuVisible: true
+    property bool gameStarted: false
+    property bool gamePaused: false
+    property bool edgeScrollDisabled: false
+
     width: 1280
     height: 720
-    
     visibility: Window.FullScreen
     visible: true
     title: "Standard of Iron - RTS Game"
 
-    property alias gameView: gameViewItem
-    property bool menuVisible: true
-    property bool gameStarted: false  
-    property bool gamePaused: false   
-    
-    
-    property bool edgeScrollDisabled: false
-
-    
     GameView {
         id: gameViewItem
+
         anchors.fill: parent
         z: 0
         focus: !mainWindow.menuVisible
-        visible: gameStarted  
+        visible: gameStarted
     }
 
-    
     HUD {
         id: hud
+
         anchors.fill: parent
         z: 1
         visible: !mainWindow.menuVisible && gameStarted
-        
-        onActiveFocusChanged: if (activeFocus) gameViewItem.forceActiveFocus()
+        onActiveFocusChanged: {
+            if (activeFocus)
+                gameViewItem.forceActiveFocus();
 
+        }
         onPauseToggled: {
-            mainWindow.gamePaused = !mainWindow.gamePaused
-            gameViewItem.setPaused(mainWindow.gamePaused)
-            gameViewItem.forceActiveFocus()
+            mainWindow.gamePaused = !mainWindow.gamePaused;
+            gameViewItem.setPaused(mainWindow.gamePaused);
+            gameViewItem.forceActiveFocus();
         }
-
         onSpeedChanged: function(speed) {
-            gameViewItem.setGameSpeed(speed)
-            gameViewItem.forceActiveFocus()
+            gameViewItem.setGameSpeed(speed);
+            gameViewItem.forceActiveFocus();
         }
-
         onCommandModeChanged: function(mode) {
-            console.log("Main: Command mode changed to:", mode)
+            console.log("Main: Command mode changed to:", mode);
             if (typeof game !== 'undefined') {
-                console.log("Main: Setting game.cursorMode property to", mode)
-                game.cursorMode = mode
+                console.log("Main: Setting game.cursorMode property to", mode);
+                game.cursorMode = mode;
             } else {
-                console.log("Main: game is undefined")
+                console.log("Main: game is undefined");
             }
-            gameViewItem.forceActiveFocus()
+            gameViewItem.forceActiveFocus();
         }
-
         onRecruit: function(unitType) {
             if (typeof game !== 'undefined' && game.recruitNearSelected)
-                game.recruitNearSelected(unitType)
-            gameViewItem.forceActiveFocus()
+                game.recruitNearSelected(unitType);
+
+            gameViewItem.forceActiveFocus();
         }
     }
 
-    
     Rectangle {
         id: pauseOverlay
+
         anchors.fill: parent
         z: 10
         visible: mainWindow.gamePaused && gameStarted
-        color: "#80000000"  
-        
+        color: "#80000000"
+
         Rectangle {
             anchors.centerIn: parent
             width: 300
@@ -83,11 +80,11 @@ ApplicationWindow {
             radius: 8
             border.color: "#34495e"
             border.width: 2
-            
+
             Column {
                 anchors.centerIn: parent
                 spacing: 20
-                
+
                 Text {
                     text: "PAUSED"
                     color: "#ecf0f1"
@@ -95,117 +92,118 @@ ApplicationWindow {
                     font.bold: true
                     anchors.horizontalCenter: parent.horizontalCenter
                 }
-                
+
                 Text {
                     text: "Press Space to resume"
                     color: "#bdc3c7"
                     font.pixelSize: 14
                     anchors.horizontalCenter: parent.horizontalCenter
                 }
+
             }
+
         }
+
     }
 
-    
     MainMenu {
         id: mainMenu
+
         anchors.fill: parent
         z: 20
         visible: mainWindow.menuVisible
-
         Component.onCompleted: {
-            if (mainWindow.menuVisible) mainMenu.forceActiveFocus()
-        }
+            if (mainWindow.menuVisible)
+                mainMenu.forceActiveFocus();
 
+        }
         onVisibleChanged: {
             if (visible) {
-                mainMenu.forceActiveFocus()
-                gameViewItem.focus = false
+                mainMenu.forceActiveFocus();
+                gameViewItem.focus = false;
             } else if (gameStarted) {
-                
-                gameViewItem.forceActiveFocus()
+                gameViewItem.forceActiveFocus();
             }
         }
-
         onOpenSkirmish: function() {
-            mapSelect.visible = true
-            mainWindow.menuVisible = false  
+            mapSelect.visible = true;
+            mainWindow.menuVisible = false;
         }
         onOpenSettings: function() {
-            if (typeof game !== 'undefined' && game.openSettings) game.openSettings()
+            if (typeof game !== 'undefined' && game.openSettings)
+                game.openSettings();
+
         }
         onLoadSave: function() {
-            if (typeof game !== 'undefined' && game.loadSave) game.loadSave()
+            if (typeof game !== 'undefined' && game.loadSave)
+                game.loadSave();
+
         }
         onExitRequested: function() {
-            if (typeof game !== 'undefined' && game.exitGame) game.exitGame()
+            if (typeof game !== 'undefined' && game.exitGame)
+                game.exitGame();
+
         }
     }
 
     MapSelect {
         id: mapSelect
+
         anchors.fill: parent
         z: 21
         visible: false
-
         onVisibleChanged: {
             if (visible) {
-                mapSelect.forceActiveFocus()
-                gameViewItem.focus = false
+                mapSelect.forceActiveFocus();
+                gameViewItem.focus = false;
             }
         }
-
         onMapChosen: function(mapPath, playerConfigs) {
-            console.log("Main: onMapChosen received", mapPath, "with", playerConfigs.length, "player configs")
-            if (typeof game !== 'undefined' && game.startSkirmish) {
-                game.startSkirmish(mapPath, playerConfigs)
-            }
-            mapSelect.visible = false
-            mainWindow.menuVisible = false
-            mainWindow.gameStarted = true  
-            mainWindow.gamePaused = false  
-            gameViewItem.forceActiveFocus()
+            console.log("Main: onMapChosen received", mapPath, "with", playerConfigs.length, "player configs");
+            if (typeof game !== 'undefined' && game.startSkirmish)
+                game.startSkirmish(mapPath, playerConfigs);
+
+            mapSelect.visible = false;
+            mainWindow.menuVisible = false;
+            mainWindow.gameStarted = true;
+            mainWindow.gamePaused = false;
+            gameViewItem.forceActiveFocus();
         }
         onCancelled: function() {
-            mapSelect.visible = false
-            mainWindow.menuVisible = true
+            mapSelect.visible = false;
+            mainWindow.menuVisible = true;
         }
     }
 
-    
-    
-    
     Item {
         id: edgeScrollOverlay
-        anchors.fill: parent
-        z: 2
-        visible: !mainWindow.menuVisible && !mapSelect.visible
-        enabled: visible
 
-        
-    
-    
-    property real horzThreshold: 120
-    property real horzMaxSpeed: 0.15
-
-    property real vertThreshold: 100
-    property real verticalDeadZone: 45
-    property real vertMaxSpeed: 0.01
+        property real horzThreshold: 120
+        property real horzMaxSpeed: 0.15
+        property real vertThreshold: 100
+        property real verticalDeadZone: 45
+        property real vertMaxSpeed: 0.01
         property real xPos: -1
         property real yPos: -1
-        
         property int verticalShift: 6
 
-        
         function inHudZone(x, y) {
-            var topH = (typeof hud !== 'undefined' && hud && hud.topPanelHeight) ? hud.topPanelHeight : 0
-            var bottomH = (typeof hud !== 'undefined' && hud && hud.bottomPanelHeight) ? hud.bottomPanelHeight : 0
-            if (y < topH) return true
-            if (y > (height - bottomH)) return true
-            return false
+            var topH = (typeof hud !== 'undefined' && hud && hud.topPanelHeight) ? hud.topPanelHeight : 0;
+            var bottomH = (typeof hud !== 'undefined' && hud && hud.bottomPanelHeight) ? hud.bottomPanelHeight : 0;
+            if (y < topH)
+                return true;
+
+            if (y > (height - bottomH))
+                return true;
+
+            return false;
         }
 
-        
+        anchors.fill: parent
+        z: 2
+        visible: !mainWindow.menuVisible && !mapSelect.visible
+        enabled: visible
+
         MouseArea {
             anchors.fill: parent
             hoverEnabled: true
@@ -213,96 +211,111 @@ ApplicationWindow {
             propagateComposedEvents: true
             preventStealing: false
             onPositionChanged: function(mouse) {
-                edgeScrollOverlay.xPos = mouse.x
-                edgeScrollOverlay.yPos = mouse.y
+                edgeScrollOverlay.xPos = mouse.x;
+                edgeScrollOverlay.yPos = mouse.y;
                 if (typeof game !== 'undefined' && game.setHoverAtScreen) {
-                    if (!edgeScrollOverlay.inHudZone(mouse.x, mouse.y)) {
-                        game.setHoverAtScreen(mouse.x, mouse.y)
-                    } else {
-                        game.setHoverAtScreen(-1, -1)
-                    }
+                    if (!edgeScrollOverlay.inHudZone(mouse.x, mouse.y))
+                        game.setHoverAtScreen(mouse.x, mouse.y);
+                    else
+                        game.setHoverAtScreen(-1, -1);
                 }
             }
             onEntered: function() {
-                edgeScrollTimer.start()
+                edgeScrollTimer.start();
                 if (typeof game !== 'undefined' && game.setHoverAtScreen) {
-                    if (!edgeScrollOverlay.inHudZone(edgeScrollOverlay.xPos, edgeScrollOverlay.yPos)) {
-                        game.setHoverAtScreen(edgeScrollOverlay.xPos, edgeScrollOverlay.yPos)
-                    } else {
-                        game.setHoverAtScreen(-1, -1)
-                    }
+                    if (!edgeScrollOverlay.inHudZone(edgeScrollOverlay.xPos, edgeScrollOverlay.yPos))
+                        game.setHoverAtScreen(edgeScrollOverlay.xPos, edgeScrollOverlay.yPos);
+                    else
+                        game.setHoverAtScreen(-1, -1);
                 }
             }
             onExited: function() {
-                edgeScrollTimer.stop()
-                edgeScrollOverlay.xPos = -1
-                edgeScrollOverlay.yPos = -1
-                if (typeof game !== 'undefined' && game.setHoverAtScreen) {
-                    game.setHoverAtScreen(-1, -1)
-                }
+                edgeScrollTimer.stop();
+                edgeScrollOverlay.xPos = -1;
+                edgeScrollOverlay.yPos = -1;
+                if (typeof game !== 'undefined' && game.setHoverAtScreen)
+                    game.setHoverAtScreen(-1, -1);
+
             }
         }
 
         Timer {
             id: edgeScrollTimer
+
             interval: 16
             repeat: true
             onTriggered: {
-                if (typeof game === 'undefined') return
-                const w = edgeScrollOverlay.width
-                const h = edgeScrollOverlay.height
-                const x = edgeScrollOverlay.xPos
-                const y = edgeScrollOverlay.yPos
-                if (x < 0 || y < 0) return
-                
+                if (typeof game === 'undefined')
+                    return ;
+
+                const w = edgeScrollOverlay.width;
+                const h = edgeScrollOverlay.height;
+                const x = edgeScrollOverlay.xPos;
+                const y = edgeScrollOverlay.yPos;
+                if (x < 0 || y < 0)
+                    return ;
+
                 if (edgeScrollOverlay.inHudZone(x, y) || mainWindow.edgeScrollDisabled) {
-                    if (game.setHoverAtScreen) game.setHoverAtScreen(-1, -1)
-                    return
+                    if (game.setHoverAtScreen)
+                        game.setHoverAtScreen(-1, -1);
+
+                    return ;
                 }
-                
-                if (game.setHoverAtScreen) game.setHoverAtScreen(x, y)
-                const th = edgeScrollOverlay.horzThreshold
-                const tv = edgeScrollOverlay.vertThreshold
-                const vdz = edgeScrollOverlay.verticalDeadZone
-                const clamp = function(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
-                
-                const dl = x
-                const dr = w - x
-                
-                const topBar = (typeof hud !== 'undefined' && hud && hud.topPanelHeight) ? hud.topPanelHeight : 0
-                const bottomBar = (typeof hud !== 'undefined' && hud && hud.bottomPanelHeight) ? hud.bottomPanelHeight : 0
-                const topEdge = topBar + edgeScrollOverlay.verticalShift
-                const bottomEdge = h - bottomBar - edgeScrollOverlay.verticalShift
-                
-                const dt = Math.max(0, (y - topEdge) - vdz)
-                const db = Math.max(0, (bottomEdge - y) - vdz)
-                
-                const il = clamp(1.0 - dl / th, 0, 1)
-                const ir = clamp(1.0 - dr / th, 0, 1)
-                const iu = clamp(1.0 - dt / tv, 0, 1)
-                const id = clamp(1.0 - db / tv, 0, 1)
-                if (il===0 && ir===0 && iu===0 && id===0) return
-                
-                const curveH = function(a) { return a*a }
-                
-                const curveV = function(a) { return a*a*a }
-                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)
+                if (game.setHoverAtScreen)
+                    game.setHoverAtScreen(x, y);
+
+                const th = edgeScrollOverlay.horzThreshold;
+                const tv = edgeScrollOverlay.vertThreshold;
+                const vdz = edgeScrollOverlay.verticalDeadZone;
+                const clamp = function clamp(v, lo, hi) {
+                    return Math.max(lo, Math.min(hi, v));
+                };
+                const dl = x;
+                const dr = w - x;
+                const topBar = (typeof hud !== 'undefined' && hud && hud.topPanelHeight) ? hud.topPanelHeight : 0;
+                const bottomBar = (typeof hud !== 'undefined' && hud && hud.bottomPanelHeight) ? hud.bottomPanelHeight : 0;
+                const topEdge = topBar + edgeScrollOverlay.verticalShift;
+                const bottomEdge = h - bottomBar - edgeScrollOverlay.verticalShift;
+                const dt = Math.max(0, (y - topEdge) - vdz);
+                const db = Math.max(0, (bottomEdge - y) - vdz);
+                const il = clamp(1 - dl / th, 0, 1);
+                const ir = clamp(1 - dr / th, 0, 1);
+                const iu = clamp(1 - dt / tv, 0, 1);
+                const id = clamp(1 - db / tv, 0, 1);
+                if (il === 0 && ir === 0 && iu === 0 && id === 0)
+                    return ;
+
+                const curveH = function curveH(a) {
+                    return a * a;
+                };
+                const curveV = function curveV(a) {
+                    return a * a * a;
+                };
+                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);
+
             }
         }
+
     }
 
     Dialog {
         id: errorDialog
+
         anchors.centerIn: parent
         width: Math.min(parent.width * 0.6, 500)
         title: "Error"
         modal: true
         standardButtons: Dialog.Ok
+        onAccepted: {
+            if (game)
+                game.clearError();
+
+        }
 
         contentItem: Rectangle {
             color: "#2a2a2a"
@@ -310,6 +323,7 @@ ApplicationWindow {
 
             Text {
                 id: errorText
+
                 anchors.centerIn: parent
                 width: parent.width - 40
                 text: game ? game.lastError : ""
@@ -317,19 +331,19 @@ ApplicationWindow {
                 wrapMode: Text.WordWrap
                 font.pixelSize: 14
             }
-        }
 
-        onAccepted: {
-            if (game) game.clearError()
         }
+
     }
 
     Connections {
-        target: game
         function onLastErrorChanged() {
-            if (game.lastError !== "") {
-                errorDialog.open()
-            }
+            if (game.lastError !== "")
+                errorDialog.open();
+
         }
+
+        target: game
     }
+
 }

+ 131 - 63
ui/qml/MainMenu.qml

@@ -1,4 +1,3 @@
-
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
@@ -7,24 +6,51 @@ import StandardOfIron.UI 1.0
 
 Item {
     id: root
-    anchors.fill: parent
-    z: 10
-    focus: true
 
     signal openSkirmish()
     signal openSettings()
     signal loadSave()
     signal exitRequested()
 
-    
+    anchors.fill: parent
+    z: 10
+    focus: true
+    Keys.onPressed: function(event) {
+        if (event.key === Qt.Key_Down) {
+            container.selectedIndex = Math.min(container.selectedIndex + 1, menuModel.count - 1);
+            event.accepted = true;
+        } else if (event.key === Qt.Key_Up) {
+            container.selectedIndex = Math.max(container.selectedIndex - 1, 0);
+            event.accepted = true;
+        } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+            var m = menuModel.get(container.selectedIndex);
+            if (m.idStr === "skirmish")
+                root.openSkirmish();
+            else if (m.idStr === "load")
+                root.loadSave();
+            else if (m.idStr === "settings")
+                root.openSettings();
+            else if (m.idStr === "exit")
+                root.exitRequested();
+            event.accepted = true;
+        } else if (event.key === Qt.Key_Escape) {
+            if (typeof mainWindow !== 'undefined' && mainWindow.menuVisible && mainWindow.gameStarted) {
+                mainWindow.menuVisible = false;
+                event.accepted = true;
+            }
+        }
+    }
+
     Rectangle {
         anchors.fill: parent
         color: Theme.dim
     }
 
-    
     Rectangle {
         id: container
+
+        property int selectedIndex: 0
+
         width: Math.min(parent.width * 0.78, 1100)
         height: Math.min(parent.height * 0.78, 700)
         anchors.centerIn: parent
@@ -33,26 +59,24 @@ Item {
         border.color: Theme.panelBr
         border.width: 1
         opacity: 0.98
-        clip: true                         
-
-        property int selectedIndex: 0
+        clip: true
 
         GridLayout {
             id: grid
+
             anchors.fill: parent
             anchors.margins: Theme.spacingXLarge
             rowSpacing: Theme.spacingMedium
             columnSpacing: 18
             columns: parent.width > 900 ? 2 : 1
 
-            
             ColumnLayout {
                 Layout.preferredWidth: parent.width > 900 ? parent.width * 0.45 : parent.width
                 spacing: Theme.spacingLarge
 
-                
                 ColumnLayout {
                     spacing: Theme.spacingSmall
+
                     Label {
                         text: "STANDARD OF IRON"
                         color: Theme.textMain
@@ -62,6 +86,7 @@ Item {
                         Layout.fillWidth: true
                         elide: Label.ElideRight
                     }
+
                     Label {
                         text: "A tiny but ambitious RTS"
                         color: Theme.textSub
@@ -70,23 +95,46 @@ Item {
                         Layout.fillWidth: true
                         elide: Label.ElideRight
                     }
+
                 }
 
-                
                 ListModel {
                     id: menuModel
-                    ListElement { idStr: "skirmish"; title: "Play — Skirmish"; subtitle: "Select a map and start" }
-                    ListElement { idStr: "load";     title: "Load Save";       subtitle: "Resume a previous game" }
-                    ListElement { idStr: "settings"; title: "Settings";        subtitle: "Adjust graphics & controls" }
-                    ListElement { idStr: "exit";     title: "Exit";            subtitle: "Quit the game" }
+
+                    ListElement {
+                        idStr: "skirmish"
+                        title: "Play — Skirmish"
+                        subtitle: "Select a map and start"
+                    }
+
+                    ListElement {
+                        idStr: "load"
+                        title: "Load Save"
+                        subtitle: "Resume a previous game"
+                    }
+
+                    ListElement {
+                        idStr: "settings"
+                        title: "Settings"
+                        subtitle: "Adjust graphics & controls"
+                    }
+
+                    ListElement {
+                        idStr: "exit"
+                        title: "Exit"
+                        subtitle: "Quit the game"
+                    }
+
                 }
 
-                
                 Repeater {
                     model: menuModel
+
                     delegate: Item {
                         id: menuItem
+
                         property int idx: index
+
                         Layout.fillWidth: true
                         Layout.preferredHeight: container.width > 900 ? 64 : 56
 
@@ -94,25 +142,24 @@ Item {
                             anchors.fill: parent
                             radius: Theme.radiusLarge
                             clip: true
-                            color: container.selectedIndex === idx ? Theme.selectedBg
-                                : menuItemMouse.containsPress ? Theme.hoverBg : Qt.rgba(0, 0, 0, 0)
+                            color: container.selectedIndex === idx ? Theme.selectedBg : menuItemMouse.containsPress ? Theme.hoverBg : Qt.rgba(0, 0, 0, 0)
                             border.width: 1
                             border.color: container.selectedIndex === idx ? Theme.selectedBr : Theme.cardBorder
-                            Behavior on color { ColorAnimation { duration: Theme.animNormal } }
-                            Behavior on border.color { ColorAnimation { duration: Theme.animNormal } }
 
                             RowLayout {
                                 anchors.fill: parent
                                 anchors.margins: Theme.spacingSmall
                                 spacing: Theme.spacingMedium
 
-                                
-                                Item { Layout.fillWidth: true; Layout.preferredWidth: 1 }
+                                Item {
+                                    Layout.fillWidth: true
+                                    Layout.preferredWidth: 1
+                                }
 
-                                
                                 ColumnLayout {
                                     Layout.fillWidth: true
                                     spacing: Theme.spacingTiny
+
                                     Text {
                                         text: model.title
                                         Layout.fillWidth: true
@@ -121,6 +168,7 @@ Item {
                                         font.pointSize: Theme.fontSizeLarge
                                         font.bold: container.selectedIndex === idx
                                     }
+
                                     Text {
                                         text: model.subtitle
                                         Layout.fillWidth: true
@@ -128,53 +176,85 @@ Item {
                                         color: container.selectedIndex === idx ? Theme.accentBright : Theme.textSubLite
                                         font.pointSize: Theme.fontSizeSmall
                                     }
+
                                 }
 
-                                
                                 Text {
                                     text: "›"
                                     font.pointSize: Theme.fontSizeTitle
                                     color: container.selectedIndex === idx ? Theme.textMain : Theme.textHint
                                 }
+
                             }
+
+                            Behavior on color {
+                                ColorAnimation {
+                                    duration: Theme.animNormal
+                                }
+
+                            }
+
+                            Behavior on border.color {
+                                ColorAnimation {
+                                    duration: Theme.animNormal
+                                }
+
+                            }
+
                         }
 
-                        
                         MouseArea {
                             id: menuItemMouse
+
                             anchors.fill: parent
                             hoverEnabled: true
                             acceptedButtons: Qt.LeftButton
                             cursorShape: Qt.PointingHandCursor
                             onEntered: container.selectedIndex = idx
                             onClicked: {
-                                if (model.idStr === "skirmish")      root.openSkirmish();
-                                else if (model.idStr === "load")     root.loadSave();
-                                else if (model.idStr === "settings") root.openSettings();
-                                else if (model.idStr === "exit")     root.exitRequested();
+                                if (model.idStr === "skirmish")
+                                    root.openSkirmish();
+                                else if (model.idStr === "load")
+                                    root.loadSave();
+                                else if (model.idStr === "settings")
+                                    root.openSettings();
+                                else if (model.idStr === "exit")
+                                    root.exitRequested();
                             }
                         }
+
                     }
+
                 }
 
-                
-                Item { Layout.fillHeight: true }
+                Item {
+                    Layout.fillHeight: true
+                }
 
-                
                 RowLayout {
                     spacing: Theme.spacingSmall
-                    Label { text: "v0.9 — prototype"; color: Theme.textDim; font.pointSize: Theme.fontSizeSmall }
-                    Item { Layout.fillWidth: true }
+
+                    Label {
+                        text: "v0.9 — prototype"
+                        color: Theme.textDim
+                        font.pointSize: Theme.fontSizeSmall
+                    }
+
+                    Item {
+                        Layout.fillWidth: true
+                    }
+
                     Label {
                         text: Qt.formatDateTime(new Date(), "yyyy-MM-dd")
                         color: Theme.textHint
                         font.pointSize: Theme.fontSizeSmall
                         elide: Label.ElideRight
                     }
+
                 }
+
             }
 
-            
             Rectangle {
                 color: Qt.rgba(0, 0, 0, 0)
                 radius: Theme.radiusMedium
@@ -185,9 +265,9 @@ Item {
                     anchors.margins: Theme.spacingSmall
                     spacing: Theme.spacingMedium
 
-                    
                     Rectangle {
                         id: promo
+
                         color: Theme.cardBase
                         radius: Theme.radiusLarge
                         border.color: Theme.border
@@ -199,6 +279,7 @@ Item {
                             anchors.fill: parent
                             anchors.margins: Theme.spacingMedium
                             spacing: Theme.spacingSmall
+
                             Label {
                                 text: "Featured"
                                 color: Theme.accent
@@ -206,6 +287,7 @@ Item {
                                 Layout.fillWidth: true
                                 elide: Label.ElideRight
                             }
+
                             Label {
                                 text: "Skirmish Mode"
                                 color: Theme.textMain
@@ -214,18 +296,20 @@ Item {
                                 Layout.fillWidth: true
                                 elide: Label.ElideRight
                             }
+
                             Text {
                                 text: "Pick a map, adjust your forces and jump into battle. Modern controls and responsive UI."
                                 color: Theme.textSubLite
                                 wrapMode: Text.WordWrap
-                                maximumLineCount: 3           
+                                maximumLineCount: 3
                                 elide: Text.ElideRight
                                 Layout.fillWidth: true
                             }
+
                         }
+
                     }
 
-                    
                     Rectangle {
                         color: Theme.cardBase
                         radius: Theme.radiusLarge
@@ -238,6 +322,7 @@ Item {
                             anchors.fill: parent
                             anchors.margins: Theme.spacingSmall
                             spacing: Theme.spacingSmall
+
                             Label {
                                 text: "Tips"
                                 color: Theme.accent
@@ -245,6 +330,7 @@ Item {
                                 Layout.fillWidth: true
                                 elide: Label.ElideRight
                             }
+
                             Text {
                                 text: "Hover menu items or use Up/Down and Enter to navigate. Play opens map selection."
                                 color: Theme.textSubLite
@@ -253,35 +339,17 @@ Item {
                                 elide: Text.ElideRight
                                 Layout.fillWidth: true
                             }
+
                         }
+
                     }
+
                 }
-            }
-        }
-    }
 
-    
-    Keys.onPressed: function(event) {
-        if (event.key === Qt.Key_Down) {
-            container.selectedIndex = Math.min(container.selectedIndex + 1, menuModel.count - 1)
-            event.accepted = true
-        } else if (event.key === Qt.Key_Up) {
-            container.selectedIndex = Math.max(container.selectedIndex - 1, 0)
-            event.accepted = true
-        } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
-            var m = menuModel.get(container.selectedIndex)
-            if (m.idStr === "skirmish")      root.openSkirmish()
-            else if (m.idStr === "load")     root.loadSave()
-            else if (m.idStr === "settings") root.openSettings()
-            else if (m.idStr === "exit")     root.exitRequested()
-            event.accepted = true
-        } else if (event.key === Qt.Key_Escape) {
-            
-            
-            if (typeof mainWindow !== 'undefined' && mainWindow.menuVisible && mainWindow.gameStarted) {
-                mainWindow.menuVisible = false
-                event.accepted = true
             }
+
         }
+
     }
+
 }

+ 68 - 38
ui/qml/MapListPanel.qml

@@ -1,51 +1,63 @@
-
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 
 Item {
     id: root
-    anchors.fill: parent
 
     property var mapsModel: []
     property int currentIndex: 0
-    property var colors: ({})
+    property var colors: ({
+    })
 
     signal mapSelected(int index)
     signal mapDoubleClicked()
 
     function field(obj, key) {
-        return (obj && obj[key] !== undefined) ? String(obj[key]) : ""
+        return (obj && obj[key] !== undefined) ? String(obj[key]) : "";
     }
 
-    
+    anchors.fill: parent
+
     Text {
         id: title
+
         text: "Maps"
         color: colors.textMain
         font.pixelSize: 18
         font.bold: true
+
         anchors {
             top: parent.top
             left: parent.left
             right: parent.right
         }
+
     }
 
     Text {
         id: countLabel
+
         text: "(" + (list.count || 0) + ")"
         color: colors.textSubLite
         font.pixelSize: 12
+
         anchors {
             left: title.right
             leftMargin: 8
             verticalCenter: title.verticalCenter
         }
+
     }
 
-    
     Rectangle {
         id: listFrame
+
+        color: "transparent"
+        radius: 10
+        border.color: colors.panelBr
+        border.width: 1
+        clip: true
+
         anchors {
             top: title.bottom
             topMargin: 12
@@ -53,14 +65,10 @@ Item {
             right: parent.right
             bottom: parent.bottom
         }
-        color: "transparent"
-        radius: 10
-        border.color: colors.panelBr
-        border.width: 1
-        clip: true
 
         ListView {
             id: list
+
             anchors.fill: parent
             anchors.margins: 8
             model: root.mapsModel
@@ -69,15 +77,31 @@ Item {
             currentIndex: root.currentIndex
             keyNavigationWraps: false
             boundsBehavior: Flickable.StopAtBounds
-
             onCurrentIndexChanged: {
-                root.currentIndex = currentIndex
-                if (currentIndex >= 0) {
-                    root.mapSelected(currentIndex)
+                root.currentIndex = currentIndex;
+                if (currentIndex >= 0)
+                    root.mapSelected(currentIndex);
+
+            }
+            highlightMoveDuration: 120
+            highlightFollowsCurrentItem: true
+
+            Item {
+                anchors.fill: parent
+                visible: list.count === 0
+
+                Text {
+                    text: "No maps available"
+                    color: colors.textSub
+                    font.pixelSize: 14
+                    anchors.centerIn: parent
                 }
+
             }
 
-            ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
+            ScrollBar.vertical: ScrollBar {
+                policy: ScrollBar.AsNeeded
+            }
 
             highlight: Rectangle {
                 color: "transparent"
@@ -85,8 +109,6 @@ Item {
                 border.color: colors.selectedBr
                 border.width: 1
             }
-            highlightMoveDuration: 120
-            highlightFollowsCurrentItem: true
 
             delegate: Item {
                 width: list.width
@@ -94,6 +116,7 @@ Item {
 
                 MouseArea {
                     id: rowMouse
+
                     anchors.fill: parent
                     hoverEnabled: true
                     acceptedButtons: Qt.LeftButton
@@ -106,29 +129,26 @@ Item {
                     anchors.fill: parent
                     radius: 8
                     clip: true
-                    color: rowMouse.containsPress ? colors.hoverBg
-                            : (index === list.currentIndex ? colors.selectedBg 
-                            : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.03) : "transparent"))
+                    color: rowMouse.containsPress ? colors.hoverBg : (index === list.currentIndex ? colors.selectedBg : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.03) : "transparent"))
                     border.width: 1
-                    border.color: (index === list.currentIndex) ? colors.selectedBr 
-                            : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.15) : colors.thumbBr)
-                    Behavior on color { ColorAnimation { duration: 160 } }
-                    Behavior on border.color { ColorAnimation { duration: 160 } }
+                    border.color: (index === list.currentIndex) ? colors.selectedBr : (rowMouse.containsMouse ? Qt.rgba(1, 1, 1, 0.15) : colors.thumbBr)
 
                     Rectangle {
                         id: thumbWrap
+
                         width: 60
                         height: 42
                         radius: 6
                         color: "#031314"
                         border.color: colors.thumbBr
                         border.width: 1
+                        clip: true
+
                         anchors {
                             left: parent.left
                             leftMargin: 10
                             verticalCenter: parent.verticalCenter
                         }
-                        clip: true
 
                         Image {
                             anchors.fill: parent
@@ -137,9 +157,12 @@ Item {
                             fillMode: Image.PreserveAspectCrop
                             visible: status === Image.Ready
                         }
+
                     }
 
                     Column {
+                        spacing: 4
+
                         anchors {
                             left: thumbWrap.right
                             leftMargin: 10
@@ -147,7 +170,6 @@ Item {
                             rightMargin: 10
                             verticalCenter: parent.verticalCenter
                         }
-                        spacing: 4
 
                         Text {
                             text: (typeof name !== "undefined") ? String(name) : ""
@@ -165,21 +187,29 @@ Item {
                             elide: Text.ElideRight
                             width: parent.width
                         }
+
+                    }
+
+                    Behavior on color {
+                        ColorAnimation {
+                            duration: 160
+                        }
+
+                    }
+
+                    Behavior on border.color {
+                        ColorAnimation {
+                            duration: 160
+                        }
+
                     }
-                }
-            }
 
-            
-            Item {
-                anchors.fill: parent
-                visible: list.count === 0
-                Text {
-                    text: "No maps available"
-                    color: colors.textSub
-                    font.pixelSize: 14
-                    anchors.centerIn: parent
                 }
+
             }
+
         }
+
     }
+
 }

Файловите разлики са ограничени, защото са твърде много
+ 399 - 248
ui/qml/MapSelect.qml


+ 124 - 70
ui/qml/PlayerConfigPanel.qml

@@ -1,36 +1,11 @@
-
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 
 Item {
     id: root
-    anchors.fill: parent
-    
-    
-    Component {
-        id: playerListItemComponent
-        Loader {
-            source: "./PlayerListItem.qml"
-            property var itemColors: root.colors
-            property var itemPlayerData: model
-            property var itemTeamIcons: root.teamIcons
-            property bool itemCanRemove: !model.isHuman
-            
-            onLoaded: {
-                item.colors = Qt.binding(function() { return itemColors })
-                item.playerData = Qt.binding(function() { return itemPlayerData })
-                item.teamIcons = Qt.binding(function() { return itemTeamIcons })
-                item.canRemove = Qt.binding(function() { return itemCanRemove })
-                
-                item.removeClicked.connect(function() { root.removePlayerClicked(index) })
-                item.colorClicked.connect(function() { root.playerColorClicked(index) })
-                item.teamClicked.connect(function() { root.playerTeamClicked(index) })
-                item.factionClicked.connect(function() { root.playerFactionClicked(index) })
-            }
-        }
-    }
 
-    property var colors: ({})
+    property var colors: ({
+    })
     property var playersModel: null
     property var teamIcons: []
     property var currentMapData: null
@@ -43,34 +18,80 @@ Item {
     signal playerTeamClicked(int index)
     signal playerFactionClicked(int index)
 
-    
+    anchors.fill: parent
+
+    Component {
+        id: playerListItemComponent
+
+        Loader {
+            property var itemColors: root.colors
+            property var itemPlayerData: model
+            property var itemTeamIcons: root.teamIcons
+            property bool itemCanRemove: !model.isHuman
+
+            source: "./PlayerListItem.qml"
+            onLoaded: {
+                item.colors = Qt.binding(function() {
+                    return itemColors;
+                });
+                item.playerData = Qt.binding(function() {
+                    return itemPlayerData;
+                });
+                item.teamIcons = Qt.binding(function() {
+                    return itemTeamIcons;
+                });
+                item.canRemove = Qt.binding(function() {
+                    return itemCanRemove;
+                });
+                item.removeClicked.connect(function() {
+                    root.removePlayerClicked(index);
+                });
+                item.colorClicked.connect(function() {
+                    root.playerColorClicked(index);
+                });
+                item.teamClicked.connect(function() {
+                    root.playerTeamClicked(index);
+                });
+                item.factionClicked.connect(function() {
+                    root.playerFactionClicked(index);
+                });
+            }
+        }
+
+    }
+
     Text {
         id: title
+
         text: root.mapTitle
         color: colors.textMain
         font.pixelSize: 20
         font.bold: true
         elide: Text.ElideRight
+
         anchors {
             top: parent.top
             left: parent.left
             right: parent.right
         }
+
     }
 
-    
     Item {
         id: playerSection
+
+        height: Math.min(350, parent.height * 0.6)
+
         anchors {
             top: title.bottom
             topMargin: 16
             left: parent.left
             right: parent.right
         }
-        height: Math.min(350, parent.height * 0.6)
 
         Text {
             id: playerSectionTitle
+
             text: "Players (" + (playersModel ? playersModel.count : 0) + ")"
             color: colors.textMain
             font.pixelSize: 16
@@ -79,19 +100,29 @@ Item {
 
         Text {
             id: playerSectionHint
+
+            text: "Click color/team to cycle"
+            color: colors.textSubLite
+            font.pixelSize: 11
+            font.italic: true
+
             anchors {
                 left: playerSectionTitle.right
                 leftMargin: 10
                 verticalCenter: playerSectionTitle.verticalCenter
             }
-            text: "Click color/team to cycle"
-            color: colors.textSubLite
-            font.pixelSize: 11
-            font.italic: true
+
         }
 
         Rectangle {
             id: playerListFrame
+
+            radius: 8
+            color: colors.cardBaseA
+            border.color: colors.panelBr
+            border.width: 1
+            clip: true
+
             anchors {
                 top: playerSectionTitle.bottom
                 topMargin: 10
@@ -100,26 +131,18 @@ Item {
                 bottom: addCPUBtn.top
                 bottomMargin: 8
             }
-            radius: 8
-            color: colors.cardBaseA
-            border.color: colors.panelBr
-            border.width: 1
-            clip: true
 
             ListView {
                 id: playerListView
+
                 anchors.fill: parent
                 anchors.margins: 8
                 model: root.playersModel
                 spacing: 6
                 clip: true
                 boundsBehavior: Flickable.StopAtBounds
-
-                ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
-
                 delegate: playerListItemComponent
 
-                
                 Item {
                     anchors.fill: parent
                     visible: !playersModel || playersModel.count === 0
@@ -130,33 +153,53 @@ Item {
                         color: colors.textSub
                         font.pixelSize: 13
                     }
+
                 }
+
+                ScrollBar.vertical: ScrollBar {
+                    policy: ScrollBar.AsNeeded
+                }
+
             }
+
         }
 
-        
         Button {
             id: addCPUBtn
+
             text: "+ Add CPU"
+            enabled: {
+                if (!currentMapData || !currentMapData.playerIds)
+                    return false;
+
+                if (!playersModel)
+                    return false;
+
+                return playersModel.count < currentMapData.playerIds.length;
+            }
+            hoverEnabled: true
+            onClicked: root.addCPUClicked()
+
             anchors {
                 bottom: parent.bottom
                 left: parent.left
             }
-            enabled: {
-                if (!currentMapData || !currentMapData.playerIds) return false
-                if (!playersModel) return false
-                return playersModel.count < currentMapData.playerIds.length
-            }
-            hoverEnabled: true
 
             MouseArea {
                 id: addHover
+
                 anchors.fill: parent
                 hoverEnabled: true
                 acceptedButtons: Qt.NoButton
                 cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
             }
 
+            ToolTip {
+                visible: addHover.containsMouse && enabled
+                text: "Add AI player to the game"
+                delay: 500
+            }
+
             contentItem: Text {
                 text: addCPUBtn.text
                 font.pixelSize: 12
@@ -170,49 +213,57 @@ Item {
                 implicitWidth: 100
                 implicitHeight: 32
                 radius: 6
-                color: enabled ? (addCPUBtn.down ? Qt.darker(colors.addColor, 1.3)
-                        : (addHover.containsMouse ? Qt.darker(colors.addColor, 1.1) : colors.cardBaseA))
-                        : colors.cardBaseA
+                color: enabled ? (addCPUBtn.down ? Qt.darker(colors.addColor, 1.3) : (addHover.containsMouse ? Qt.darker(colors.addColor, 1.1) : colors.cardBaseA)) : colors.cardBaseA
                 border.width: 1
                 border.color: enabled ? colors.addColor : colors.thumbBr
-                Behavior on color { ColorAnimation { duration: 150 } }
-            }
 
-            onClicked: root.addCPUClicked()
+                Behavior on color {
+                    ColorAnimation {
+                        duration: 150
+                    }
+
+                }
 
-            ToolTip {
-                visible: addHover.containsMouse && enabled
-                text: "Add AI player to the game"
-                delay: 500
             }
+
         }
 
         Text {
+            text: {
+                if (!currentMapData || !currentMapData.playerIds)
+                    return "";
+
+                if (!playersModel)
+                    return "";
+
+                var available = currentMapData.playerIds.length - playersModel.count;
+                if (available <= 0)
+                    return "Max players reached";
+
+                return available + " slot" + (available > 1 ? "s" : "") + " available";
+            }
+            color: colors.textSubLite
+            font.pixelSize: 11
+
             anchors {
                 left: addCPUBtn.right
                 leftMargin: 10
                 verticalCenter: addCPUBtn.verticalCenter
             }
-            text: {
-                if (!currentMapData || !currentMapData.playerIds) return ""
-                if (!playersModel) return ""
-                var available = currentMapData.playerIds.length - playersModel.count
-                if (available <= 0) return "Max players reached"
-                return available + " slot" + (available > 1 ? "s" : "") + " available"
-            }
-            color: colors.textSubLite
-            font.pixelSize: 11
+
         }
+
     }
 
-    
     Rectangle {
         id: preview
+
         radius: 8
         color: "#031314"
         border.color: colors.thumbBr
         border.width: 1
         clip: true
+
         anchors {
             top: playerSection.bottom
             topMargin: 16
@@ -223,6 +274,7 @@ Item {
 
         Image {
             id: previewImage
+
             anchors.fill: parent
             source: root.mapPreview
             asynchronous: true
@@ -237,5 +289,7 @@ Item {
             color: colors.hint
             font.pixelSize: 14
         }
+
     }
+
 }

+ 30 - 18
ui/qml/PlayerListItem.qml

@@ -1,15 +1,13 @@
-
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 
 Rectangle {
     id: root
-    width: parent ? parent.width : 400
-    height: 48
-    radius: 6
 
-    property var colors: ({})
-    property var playerData: ({})
+    property var colors: ({
+    })
+    property var playerData: ({
+    })
     property var teamIcons: []
     property bool canRemove: true
 
@@ -18,6 +16,9 @@ Rectangle {
     signal teamClicked()
     signal factionClicked()
 
+    width: parent ? parent.width : 400
+    height: 48
+    radius: 6
     color: colors.cardBaseB
     border.color: colors.thumbBr
     border.width: 1
@@ -27,11 +28,10 @@ Rectangle {
         anchors.margins: 8
         spacing: 10
 
-        
         Item {
             width: 80
             height: parent.height
-            
+
             Text {
                 anchors.centerIn: parent
                 text: playerData.playerName || ""
@@ -39,9 +39,9 @@ Rectangle {
                 font.pixelSize: 14
                 font.bold: playerData.isHuman || false
             }
+
         }
 
-        
         Rectangle {
             width: 90
             height: parent.height
@@ -71,9 +71,9 @@ Rectangle {
                 text: "Click to change color"
                 delay: 500
             }
+
         }
 
-        
         Rectangle {
             width: 50
             height: parent.height
@@ -89,8 +89,10 @@ Rectangle {
                 Text {
                     anchors.horizontalCenter: parent.horizontalCenter
                     text: {
-                        if (!playerData.teamId || !teamIcons || teamIcons.length === 0) return "●"
-                        return teamIcons[(playerData.teamId - 1) % teamIcons.length]
+                        if (!playerData.teamId || !teamIcons || teamIcons.length === 0)
+                            return "●";
+
+                        return teamIcons[(playerData.teamId - 1) % teamIcons.length];
                     }
                     color: colors.textMain
                     font.pixelSize: 18
@@ -102,6 +104,7 @@ Rectangle {
                     color: colors.textSubLite
                     font.pixelSize: 9
                 }
+
             }
 
             MouseArea {
@@ -116,9 +119,9 @@ Rectangle {
                 text: "Click to change team"
                 delay: 500
             }
+
         }
 
-        
         Rectangle {
             width: 140
             height: parent.height
@@ -126,7 +129,7 @@ Rectangle {
             color: colors.cardBaseA
             border.color: colors.thumbBr
             border.width: 1
-            opacity: 0.7 
+            opacity: 0.7
 
             Text {
                 anchors.centerIn: parent
@@ -140,19 +143,18 @@ Rectangle {
 
             MouseArea {
                 anchors.fill: parent
-                cursorShape: Qt.ArrowCursor 
+                cursorShape: Qt.ArrowCursor
                 enabled: false
                 onClicked: root.factionClicked()
             }
+
         }
 
-        
         Item {
             width: Math.max(10, parent.parent.width - 432)
             height: parent.height
         }
 
-        
         Rectangle {
             width: 32
             height: parent.height
@@ -161,7 +163,6 @@ Rectangle {
             border.color: colors.dangerColor
             border.width: 1
             visible: root.canRemove && !playerData.isHuman
-            Behavior on color { ColorAnimation { duration: 150 } }
 
             Text {
                 anchors.centerIn: parent
@@ -173,6 +174,7 @@ Rectangle {
 
             MouseArea {
                 id: removeMouseArea
+
                 anchors.fill: parent
                 hoverEnabled: true
                 cursorShape: Qt.PointingHandCursor
@@ -184,6 +186,16 @@ Rectangle {
                 text: "Remove player"
                 delay: 300
             }
+
+            Behavior on color {
+                ColorAnimation {
+                    duration: 150
+                }
+
+            }
+
         }
+
     }
+
 }

+ 106 - 150
ui/qml/StyleGuide.qml

@@ -1,170 +1,126 @@
-
-pragma Singleton
 import QtQuick 2.15
+pragma Singleton
 
 QtObject {
     id: root
 
-    
     readonly property var palette: ({
-        
-        bg: "#071018",
-        bgShade: "#061214",
-        dim: Qt.rgba(0, 0, 0, 0.45),
-        
-        
-        panelBase: "#0E1C1E",
-        panelBr: "#0f2430",
-        
-        
-        cardBase: "#132526",
-        cardBaseA: "#132526AA",
-        cardBaseB: "#06141b",
-        cardBorder: "#12323a",
-        
-        
-        hover: "#184c7a",
-        hoverBg: "#184c7a",
-        selected: "#1f8bf5",
-        selectedBg: "#1f8bf5",
-        selectedBr: "#1b74d1",
-        
-        
-        thumbBr: "#2A4E56",
-        border: "#0f2b34",
-        
-        
-        textMain: "#eaf6ff",
-        textBright: "#dff0ff",
-        textSub: "#86a7b6",
-        textSubLite: "#79a6b7",
-        textDim: "#4f6a75",
-        textHint: "#2a5e6e",
-        
-        
-        accent: "#9fd9ff",
-        accentBright: "#d0e8ff",
-        
-        
-        addColor: "#3A9CA8",
-        removeColor: "#D04040",
-        dangerColor: "#D04040",
-        startColor: "#40D080"
+        "bg": "#071018",
+        "bgShade": "#061214",
+        "dim": Qt.rgba(0, 0, 0, 0.45),
+        "panelBase": "#0E1C1E",
+        "panelBr": "#0f2430",
+        "cardBase": "#132526",
+        "cardBaseA": "#132526AA",
+        "cardBaseB": "#06141b",
+        "cardBorder": "#12323a",
+        "hover": "#184c7a",
+        "hoverBg": "#184c7a",
+        "selected": "#1f8bf5",
+        "selectedBg": "#1f8bf5",
+        "selectedBr": "#1b74d1",
+        "thumbBr": "#2A4E56",
+        "border": "#0f2b34",
+        "textMain": "#eaf6ff",
+        "textBright": "#dff0ff",
+        "textSub": "#86a7b6",
+        "textSubLite": "#79a6b7",
+        "textDim": "#4f6a75",
+        "textHint": "#2a5e6e",
+        "accent": "#9fd9ff",
+        "accentBright": "#d0e8ff",
+        "addColor": "#3A9CA8",
+        "removeColor": "#D04040",
+        "dangerColor": "#D04040",
+        "startColor": "#40D080"
     })
-
-    
     readonly property var button: ({
-        
-        primary: {
-            normalBg: palette.selectedBg,
-            hoverBg: "#2a7fe0",
-            pressBg: palette.selectedBr,
-            disabledBg: "#0a1a24",
-            
-            normalBorder: palette.selectedBr,
-            hoverBorder: palette.selectedBr,
-            disabledBorder: palette.panelBr,
-            
-            textColor: "white",
-            disabledTextColor: "#6f8793",
-            
-            radius: 9,
-            height: 40,
-            minWidth: 120,
-            fontSize: 12,
-            hoverFontSize: 13
+        "primary": {
+            "normalBg": palette.selectedBg,
+            "hoverBg": "#2a7fe0",
+            "pressBg": palette.selectedBr,
+            "disabledBg": "#0a1a24",
+            "normalBorder": palette.selectedBr,
+            "hoverBorder": palette.selectedBr,
+            "disabledBorder": palette.panelBr,
+            "textColor": "white",
+            "disabledTextColor": "#6f8793",
+            "radius": 9,
+            "height": 40,
+            "minWidth": 120,
+            "fontSize": 12,
+            "hoverFontSize": 13
         },
-        
-        
-        secondary: {
-            normalBg: "transparent",
-            hoverBg: palette.cardBase,
-            pressBg: palette.hover,
-            disabledBg: "transparent",
-            
-            normalBorder: palette.cardBorder,
-            hoverBorder: palette.thumbBr,
-            disabledBorder: palette.panelBr,
-            
-            textColor: palette.textBright,
-            disabledTextColor: palette.textDim,
-            
-            radius: 8,
-            height: 38,
-            minWidth: 100,
-            fontSize: 11,
-            hoverFontSize: 12
+        "secondary": {
+            "normalBg": "transparent",
+            "hoverBg": palette.cardBase,
+            "pressBg": palette.hover,
+            "disabledBg": "transparent",
+            "normalBorder": palette.cardBorder,
+            "hoverBorder": palette.thumbBr,
+            "disabledBorder": palette.panelBr,
+            "textColor": palette.textBright,
+            "disabledTextColor": palette.textDim,
+            "radius": 8,
+            "height": 38,
+            "minWidth": 100,
+            "fontSize": 11,
+            "hoverFontSize": 12
         },
-        
-        
-        small: {
-            normalBg: palette.addColor,
-            hoverBg: Qt.lighter(palette.addColor, 1.2),
-            pressBg: Qt.darker(palette.addColor, 1.2),
-            disabledBg: palette.cardBase,
-            
-            normalBorder: Qt.lighter(palette.addColor, 1.1),
-            hoverBorder: Qt.lighter(palette.addColor, 1.3),
-            disabledBorder: palette.thumbBr,
-            
-            textColor: "white",
-            disabledTextColor: palette.textDim,
-            
-            radius: 6,
-            height: 32,
-            minWidth: 80,
-            fontSize: 11,
-            hoverFontSize: 11
+        "small": {
+            "normalBg": palette.addColor,
+            "hoverBg": Qt.lighter(palette.addColor, 1.2),
+            "pressBg": Qt.darker(palette.addColor, 1.2),
+            "disabledBg": palette.cardBase,
+            "normalBorder": Qt.lighter(palette.addColor, 1.1),
+            "hoverBorder": Qt.lighter(palette.addColor, 1.3),
+            "disabledBorder": palette.thumbBr,
+            "textColor": "white",
+            "disabledTextColor": palette.textDim,
+            "radius": 6,
+            "height": 32,
+            "minWidth": 80,
+            "fontSize": 11,
+            "hoverFontSize": 11
         },
-        
-        
-        danger: {
-            normalBg: "transparent",
-            hoverBg: palette.dangerColor,
-            pressBg: Qt.darker(palette.dangerColor, 1.2),
-            disabledBg: "transparent",
-            
-            normalBorder: palette.dangerColor,
-            hoverBorder: palette.dangerColor,
-            disabledBorder: palette.thumbBr,
-            
-            textColor: palette.dangerColor,
-            hoverTextColor: "white",
-            disabledTextColor: palette.textDim,
-            
-            radius: 4,
-            height: 32,
-            minWidth: 32,
-            fontSize: 14,
-            hoverFontSize: 14
+        "danger": {
+            "normalBg": "transparent",
+            "hoverBg": palette.dangerColor,
+            "pressBg": Qt.darker(palette.dangerColor, 1.2),
+            "disabledBg": "transparent",
+            "normalBorder": palette.dangerColor,
+            "hoverBorder": palette.dangerColor,
+            "disabledBorder": palette.thumbBr,
+            "textColor": palette.dangerColor,
+            "hoverTextColor": "white",
+            "disabledTextColor": palette.textDim,
+            "radius": 4,
+            "height": 32,
+            "minWidth": 32,
+            "fontSize": 14,
+            "hoverFontSize": 14
         }
     })
-
-    
     readonly property var card: ({
-        radius: 8,
-        borderWidth: 1,
-        bg: palette.cardBase,
-        border: palette.cardBorder,
-        hoverBg: palette.hover,
-        selectedBg: palette.selected,
-        selectedBorder: palette.selectedBr
+        "radius": 8,
+        "borderWidth": 1,
+        "bg": palette.cardBase,
+        "border": palette.cardBorder,
+        "hoverBg": palette.hover,
+        "selectedBg": palette.selected,
+        "selectedBorder": palette.selectedBr
     })
-
-    
     readonly property var listItem: ({
-        height: 48,
-        radius: 6,
-        spacing: 10,
-        bg: palette.cardBaseB,
-        border: palette.thumbBr,
-        borderWidth: 1
+        "height": 48,
+        "radius": 6,
+        "spacing": 10,
+        "bg": palette.cardBaseB,
+        "border": palette.thumbBr,
+        "borderWidth": 1
     })
-
-    
     readonly property var animation: ({
-        fast: 120,
-        normal: 160,
-        slow: 200
+        "fast": 120,
+        "normal": 160,
+        "slow": 200
     })
 }

+ 73 - 38
ui/qml/StyledButton.qml

@@ -1,80 +1,115 @@
-
 import QtQuick 2.15
 import QtQuick.Controls 2.15
 
 Button {
     id: control
-    
-    property string buttonStyle: "primary" 
+
+    property string buttonStyle: "primary"
     property var styleConfig: {
-        if (buttonStyle === "primary") return StyleGuide.button.primary
-        if (buttonStyle === "secondary") return StyleGuide.button.secondary
-        if (buttonStyle === "small") return StyleGuide.button.small
-        if (buttonStyle === "danger") return StyleGuide.button.danger
-        return StyleGuide.button.primary
+        if (buttonStyle === "primary")
+            return StyleGuide.button.primary;
+
+        if (buttonStyle === "secondary")
+            return StyleGuide.button.secondary;
+
+        if (buttonStyle === "small")
+            return StyleGuide.button.small;
+
+        if (buttonStyle === "danger")
+            return StyleGuide.button.danger;
+
+        return StyleGuide.button.primary;
     }
-    
+
     implicitHeight: styleConfig.height
     implicitWidth: styleConfig.minWidth
     hoverEnabled: true
-    
+    ToolTip.visible: control.ToolTip.text !== "" && hoverArea.containsMouse
+    ToolTip.delay: 500
+
     MouseArea {
         id: hoverArea
+
         anchors.fill: parent
         hoverEnabled: true
         acceptedButtons: Qt.NoButton
         cursorShape: control.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
     }
-    
+
     contentItem: Text {
         text: control.text
-        font.pointSize: control.enabled 
-            ? (hoverArea.containsMouse ? styleConfig.hoverFontSize : styleConfig.fontSize)
-            : styleConfig.fontSize
+        font.pointSize: control.enabled ? (hoverArea.containsMouse ? styleConfig.hoverFontSize : styleConfig.fontSize) : styleConfig.fontSize
         font.bold: true
         color: {
-            if (!control.enabled) return styleConfig.disabledTextColor
-            if (buttonStyle === "danger" && hoverArea.containsMouse) 
-                return styleConfig.hoverTextColor || styleConfig.textColor
-            return styleConfig.textColor
+            if (!control.enabled)
+                return styleConfig.disabledTextColor;
+
+            if (buttonStyle === "danger" && hoverArea.containsMouse)
+                return styleConfig.hoverTextColor || styleConfig.textColor;
+
+            return styleConfig.textColor;
         }
         horizontalAlignment: Text.AlignHCenter
         verticalAlignment: Text.AlignVCenter
         elide: Text.ElideRight
-        
-        Behavior on font.pointSize { 
-            NumberAnimation { duration: StyleGuide.animation.fast } 
+
+        Behavior on font.pointSize {
+            NumberAnimation {
+                duration: StyleGuide.animation.fast
+            }
+
         }
+
         Behavior on color {
-            ColorAnimation { duration: StyleGuide.animation.normal }
+            ColorAnimation {
+                duration: StyleGuide.animation.normal
+            }
+
         }
+
     }
-    
+
     background: Rectangle {
         implicitWidth: styleConfig.minWidth
         implicitHeight: styleConfig.height
         radius: styleConfig.radius
         color: {
-            if (!control.enabled) return styleConfig.disabledBg
-            if (control.down) return styleConfig.pressBg
-            if (hoverArea.containsMouse) return styleConfig.hoverBg
-            return styleConfig.normalBg
+            if (!control.enabled)
+                return styleConfig.disabledBg;
+
+            if (control.down)
+                return styleConfig.pressBg;
+
+            if (hoverArea.containsMouse)
+                return styleConfig.hoverBg;
+
+            return styleConfig.normalBg;
         }
         border.width: 1
         border.color: {
-            if (!control.enabled) return styleConfig.disabledBorder
-            if (hoverArea.containsMouse) return styleConfig.hoverBorder
-            return styleConfig.normalBorder
+            if (!control.enabled)
+                return styleConfig.disabledBorder;
+
+            if (hoverArea.containsMouse)
+                return styleConfig.hoverBorder;
+
+            return styleConfig.normalBorder;
         }
-        
-        Behavior on color { 
-            ColorAnimation { duration: StyleGuide.animation.normal } 
+
+        Behavior on color {
+            ColorAnimation {
+                duration: StyleGuide.animation.normal
+            }
+
         }
-        Behavior on border.color { 
-            ColorAnimation { duration: StyleGuide.animation.normal } 
+
+        Behavior on border.color {
+            ColorAnimation {
+                duration: StyleGuide.animation.normal
+            }
+
         }
+
     }
-    
-    ToolTip.visible: control.ToolTip.text !== "" && hoverArea.containsMouse
-    ToolTip.delay: 500
+
 }

Някои файлове не бяха показани, защото твърде много файлове са промени