Explorar el Código

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

Co-authored-by: djeada <[email protected]>
copilot-swe-agent[bot] hace 2 meses
padre
commit
7d5928bff0
Se han modificado 65 ficheros con 3158 adiciones y 2252 borrados
  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
 # Formatting config
 CLANG_FORMAT ?= clang-format
 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"
 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
 # Colors for output
 BOLD := \033[1m
 BOLD := \033[1m
@@ -35,7 +39,7 @@ help:
 	@echo "  $(GREEN)clean$(RESET)         - Clean build directory"
 	@echo "  $(GREEN)clean$(RESET)         - Clean build directory"
 	@echo "  $(GREEN)rebuild$(RESET)       - Clean and build"
 	@echo "  $(GREEN)rebuild$(RESET)       - Clean and build"
 	@echo "  $(GREEN)test$(RESET)          - Run tests (if any)"
 	@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)format-check$(RESET)  - Verify formatting (CI-friendly, no changes)"
 	@echo "  $(GREEN)check-deps$(RESET)    - Check if dependencies are installed"
 	@echo "  $(GREEN)check-deps$(RESET)    - Check if dependencies are installed"
 	@echo "  $(GREEN)dev$(RESET)           - Set up development environment (install + configure + build)"
 	@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)"; \
 		echo "$(YELLOW)No tests found. Test suite not yet implemented.$(RESET)"; \
 	fi
 	fi
 
 
-# ---- Formatting: strip comments first, then clang-format (strict) ----
+# ---- Formatting: strip comments first, then format (strict) ----
 .PHONY: format format-check
 .PHONY: format format-check
 format:
 format:
 	@echo "$(BOLD)$(BLUE)Stripping comments in app/... game/... render/... tools/... ui/...$(RESET)"
 	@echo "$(BOLD)$(BLUE)Stripping comments in app/... game/... render/... tools/... ui/...$(RESET)"
@@ -149,24 +153,65 @@ format:
 	else \
 	else \
 		echo "$(RED)scripts/remove-comments.sh not found$(RESET)"; exit 1; \
 		echo "$(RED)scripts/remove-comments.sh not found$(RESET)"; exit 1; \
 	fi
 	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 \
 	@if command -v $(CLANG_FORMAT) >/dev/null 2>&1; then \
 		find . -type f \( $(FMT_GLOBS) \) -not -path "./$(BUILD_DIR)/*" -print0 \
 		find . -type f \( $(FMT_GLOBS) \) -not -path "./$(BUILD_DIR)/*" -print0 \
 		| xargs -0 -r $(CLANG_FORMAT) -i --style=file; \
 		| xargs -0 -r $(CLANG_FORMAT) -i --style=file; \
-		echo "$(GREEN)✓ Format + comment strip complete$(RESET)"; \
+		echo "$(GREEN)✓ C/C++ formatting complete$(RESET)"; \
 	else \
 	else \
 		echo "$(RED)clang-format not found. Please install it.$(RESET)"; exit 1; \
 		echo "$(RED)clang-format not found. Please install it.$(RESET)"; exit 1; \
 	fi
 	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
 # CI/verification: fail if anything would be reformatted
 format-check:
 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 \
 		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 \
 	else \
 		echo "$(RED)clang-format not found. Please install it.$(RESET)"; exit 1; \
 		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
 	fi
 
 
 # Debug build
 # Debug build

+ 3 - 3
app/controllers/action_vfx.cpp

@@ -8,7 +8,7 @@
 namespace App::Controllers {
 namespace App::Controllers {
 
 
 void ActionVFX::spawnAttackArrow(Engine::Core::World *world,
 void ActionVFX::spawnAttackArrow(Engine::Core::World *world,
-                                Engine::Core::EntityID targetId) {
+                                 Engine::Core::EntityID targetId) {
   if (!world)
   if (!world)
     return;
     return;
 
 
@@ -30,7 +30,7 @@ void ActionVFX::spawnAttackArrow(Engine::Core::World *world,
   QVector3D aboveTarget = targetPos + QVector3D(0, 2.0f, 0);
   QVector3D aboveTarget = targetPos + QVector3D(0, 2.0f, 0);
 
 
   arrowSystem->spawnArrow(aboveTarget, targetPos, QVector3D(1.0f, 0.2f, 0.2f),
   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 {
 namespace Core {
 class World;
 class World;
 using EntityID = unsigned int;
 using EntityID = unsigned int;
-}
-} 
+} // namespace Core
+} // namespace Engine
 
 
 namespace App::Controllers {
 namespace App::Controllers {
 
 
@@ -17,4 +17,4 @@ public:
                                Engine::Core::EntityID targetId);
                                Engine::Core::EntityID targetId);
 };
 };
 
 
-} 
+} // namespace App::Controllers

+ 19 - 17
app/controllers/command_controller.cpp

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

+ 12 - 12
app/controllers/command_controller.h

@@ -1,8 +1,8 @@
 #pragma once
 #pragma once
 
 
 #include <QObject>
 #include <QObject>
-#include <QVector3D>
 #include <QString>
 #include <QString>
+#include <QVector3D>
 #include <vector>
 #include <vector>
 
 
 namespace Engine {
 namespace Engine {
@@ -10,13 +10,13 @@ namespace Core {
 class World;
 class World;
 class Entity;
 class Entity;
 using EntityID = unsigned int;
 using EntityID = unsigned int;
-}
-} 
+} // namespace Core
+} // namespace Engine
 
 
 namespace Game::Systems {
 namespace Game::Systems {
 class SelectionSystem;
 class SelectionSystem;
 class PickingService;
 class PickingService;
-}
+} // namespace Game::Systems
 
 
 namespace App::Controllers {
 namespace App::Controllers {
 
 
@@ -29,18 +29,18 @@ class CommandController : public QObject {
   Q_OBJECT
   Q_OBJECT
 public:
 public:
   CommandController(Engine::Core::World *world,
   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,
   CommandResult onAttackClick(qreal sx, qreal sy, int viewportWidth,
-                             int viewportHeight, void *camera);
+                              int viewportHeight, void *camera);
   CommandResult onStopCommand();
   CommandResult onStopCommand();
   CommandResult onPatrolClick(qreal sx, qreal sy, int viewportWidth,
   CommandResult onPatrolClick(qreal sx, qreal sy, int viewportWidth,
-                             int viewportHeight, void *camera);
+                              int viewportHeight, void *camera);
   CommandResult setRallyAtScreen(qreal sx, qreal sy, int viewportWidth,
   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);
   void recruitNearSelected(const QString &unitType, int localOwnerId);
 
 
   bool hasPatrolFirstWaypoint() const { return m_hasPatrolFirstWaypoint; }
   bool hasPatrolFirstWaypoint() const { return m_hasPatrolFirstWaypoint; }
@@ -62,4 +62,4 @@ private:
   void resetMovement(Engine::Core::Entity *entity);
   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>();
   m_cameraService = std::make_unique<Game::Systems::CameraService>();
 
 
   auto *selectionSystem = m_world->getSystem<Game::Systems::SelectionSystem>();
   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_commandController = std::make_unique<App::Controllers::CommandController>(
       m_world.get(), selectionSystem, m_pickingService.get());
       m_world.get(), selectionSystem, m_pickingService.get());
 
 
   m_cursorManager = std::make_unique<CursorManager>();
   m_cursorManager = std::make_unique<CursorManager>();
   m_hoverTracker = std::make_unique<HoverTracker>(m_pickingService.get());
   m_hoverTracker = std::make_unique<HoverTracker>(m_pickingService.get());
-  
-  
+
   m_mapCatalog = std::make_unique<Game::Map::MapCatalog>();
   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,
   connect(m_cursorManager.get(), &CursorManager::modeChanged, this,
           &GameEngine::cursorModeChanged);
           &GameEngine::cursorModeChanged);
@@ -138,8 +137,7 @@ GameEngine::GameEngine() {
           &Game::Systems::SelectionController::selectionModelRefreshRequested,
           &Game::Systems::SelectionController::selectionModelRefreshRequested,
           this, &GameEngine::selectedUnitsDataChanged);
           this, &GameEngine::selectedUnitsDataChanged);
   connect(m_commandController.get(),
   connect(m_commandController.get(),
-          &App::Controllers::CommandController::attackTargetSelected,
-          [this]() {
+          &App::Controllers::CommandController::attackTargetSelected, [this]() {
             if (auto *selSys =
             if (auto *selSys =
                     m_world->getSystem<Game::Systems::SelectionSystem>()) {
                     m_world->getSystem<Game::Systems::SelectionSystem>()) {
               const auto &sel = selSys->getSelectedUnits();
               const auto &sel = selSys->getSelectedUnits();
@@ -152,16 +150,15 @@ GameEngine::GameEngine() {
                       m_viewport.height, 0);
                       m_viewport.height, 0);
                   if (targetId != 0) {
                   if (targetId != 0) {
                     App::Controllers::ActionVFX::spawnAttackArrow(m_world.get(),
                     App::Controllers::ActionVFX::spawnAttackArrow(m_world.get(),
-                                                                 targetId);
+                                                                  targetId);
                   }
                   }
                 }
                 }
               }
               }
             }
             }
           });
           });
-  
+
   connect(m_commandController.get(),
   connect(m_commandController.get(),
-          &App::Controllers::CommandController::troopLimitReached,
-          [this]() {
+          &App::Controllers::CommandController::troopLimitReached, [this]() {
             setError("Maximum troop limit reached. Cannot produce more units.");
             setError("Maximum troop limit reached. Cannot produce more units.");
           });
           });
 
 
@@ -201,8 +198,8 @@ void GameEngine::onMapClicked(qreal sx, qreal sy) {
   ensureInitialized();
   ensureInitialized();
   if (m_selectionController && m_camera) {
   if (m_selectionController && m_camera) {
     m_selectionController->onClickSelect(sx, sy, false, m_viewport.width,
     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>();
             targetEntity->getComponent<Engine::Core::UnitComponent>();
         if (targetUnit && targetUnit->ownerId != m_runtime.localOwnerId) {
         if (targetUnit && targetUnit->ownerId != m_runtime.localOwnerId) {
           App::Controllers::ActionVFX::spawnAttackArrow(m_world.get(),
           App::Controllers::ActionVFX::spawnAttackArrow(m_world.get(),
-                                                       targetId);
+                                                        targetId);
         }
         }
       }
       }
     }
     }
@@ -356,8 +353,8 @@ void GameEngine::onClickSelect(qreal sx, qreal sy, bool additive) {
   ensureInitialized();
   ensureInitialized();
   if (m_selectionController && m_camera) {
   if (m_selectionController && m_camera) {
     m_selectionController->onClickSelect(sx, sy, additive, m_viewport.width,
     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;
     return;
   ensureInitialized();
   ensureInitialized();
   if (m_selectionController && m_camera) {
   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() {
 void GameEngine::ensureInitialized() {
   QString error;
   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()) {
   if (!error.isEmpty()) {
     setError(error);
     setError(error);
   }
   }
@@ -451,7 +447,8 @@ void GameEngine::update(float dt) {
   }
   }
 
 
   if (m_followSelectionEnabled && m_camera && m_world && m_cameraService) {
   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) {
   if (m_selectedUnitsModel) {
@@ -539,8 +536,8 @@ bool GameEngine::screenToGround(const QPointF &screenPt, QVector3D &outWorld) {
 bool GameEngine::worldToScreen(const QVector3D &world,
 bool GameEngine::worldToScreen(const QVector3D &world,
                                QPointF &outScreen) const {
                                QPointF &outScreen) const {
   return App::Utils::worldToScreen(m_pickingService.get(), m_camera.get(),
   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() {
 void GameEngine::syncSelectionFlags() {
@@ -747,8 +744,8 @@ void GameEngine::setRallyAtScreen(qreal sx, qreal sy) {
   if (!m_commandController || !m_camera)
   if (!m_commandController || !m_camera)
     return;
     return;
   m_commandController->setRallyAtScreen(sx, sy, m_viewport.width,
   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() {
 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,
 void GameEngine::startSkirmish(const QString &mapPath,
                                const QVariantList &playerConfigs) {
                                const QVariantList &playerConfigs) {
@@ -793,17 +788,17 @@ void GameEngine::startSkirmish(const QString &mapPath,
     loader.setFogRenderer(m_fog.get());
     loader.setFogRenderer(m_fog.get());
     loader.setStoneRenderer(m_stone.get());
     loader.setStoneRenderer(m_stone.get());
 
 
-    loader.setOnOwnersUpdated([this]() {
-      emit ownerInfoChanged();
-    });
+    loader.setOnOwnersUpdated([this]() { emit ownerInfoChanged(); });
 
 
     loader.setOnVisibilityMaskReady([this]() {
     loader.setOnVisibilityMaskReady([this]() {
-      m_runtime.visibilityVersion = Game::Map::VisibilityService::instance().version();
+      m_runtime.visibilityVersion =
+          Game::Map::VisibilityService::instance().version();
       m_runtime.visibilityUpdateAccumulator = 0.0f;
       m_runtime.visibilityUpdateAccumulator = 0.0f;
     });
     });
 
 
     int updatedPlayerId = m_selectedPlayerId;
     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) {
     if (updatedPlayerId != m_selectedPlayerId) {
       m_selectedPlayerId = updatedPlayerId;
       m_selectedPlayerId = updatedPlayerId;
@@ -821,8 +816,9 @@ void GameEngine::startSkirmish(const QString &mapPath,
     m_level.camNear = result.camNear;
     m_level.camNear = result.camNear;
     m_level.camFar = result.camFar;
     m_level.camFar = result.camFar;
     m_level.maxTroopsPerPlayer = result.maxTroopsPerPlayer;
     m_level.maxTroopsPerPlayer = result.maxTroopsPerPlayer;
-    
-    Game::GameConfig::instance().setMaxTroopsPerPlayer(result.maxTroopsPerPlayer);
+
+    Game::GameConfig::instance().setMaxTroopsPerPlayer(
+        result.maxTroopsPerPlayer);
 
 
     if (m_victoryService) {
     if (m_victoryService) {
       m_victoryService->configure(result.victoryConfig, m_runtime.localOwnerId);
       m_victoryService->configure(result.victoryConfig, m_runtime.localOwnerId);
@@ -1007,4 +1003,3 @@ QVector3D GameEngine::getPatrolPreviewWaypoint() const {
     return QVector3D();
     return QVector3D();
   return m_commandController->getPatrolFirstWaypoint();
   return m_commandController->getPatrolFirstWaypoint();
 }
 }
-

+ 9 - 9
app/game_engine.h

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

+ 10 - 7
app/hover_tracker.h

@@ -7,24 +7,27 @@ namespace Engine {
 namespace Core {
 namespace Core {
 class World;
 class World;
 using EntityID = unsigned int;
 using EntityID = unsigned int;
-} 
-} 
+} // namespace Core
+} // namespace Engine
 
 
 namespace Render {
 namespace Render {
 namespace GL {
 namespace GL {
 class Camera;
 class Camera;
 }
 }
-} 
+} // namespace Render
 
 
 class HoverTracker {
 class HoverTracker {
 public:
 public:
   HoverTracker(Game::Systems::PickingService *pickingService);
   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:
 private:
   Game::Systems::PickingService *m_pickingService;
   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);
   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;
 out vec4 FragColor;
 
 
 void main() {
 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
 #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_mvp;
 uniform mat4 u_model;
 uniform mat4 u_model;
@@ -12,8 +12,8 @@ out vec2 v_texCoord;
 out vec3 v_worldPos;
 out vec3 v_worldPos;
 
 
 void main() {
 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;
 out vec4 FragColor;
 
 
 void main() {
 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;
 const float EPSILON = 1e-5;
 
 
 void main() {
 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;
 out vec4 FragColor;
 
 
 void main() {
 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;
 out float v_alpha;
 
 
 void main() {
 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;
 out vec4 fragColor;
 
 
 void main() {
 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
 #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 mat4 u_viewProj;
 uniform float u_time;
 uniform float u_time;
@@ -17,43 +17,41 @@ out vec3 v_color;
 out float v_alpha;
 out float v_alpha;
 
 
 void main() {
 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
 // Hash for subtle per-cell variation
 float hash12(vec2 p) {
 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() {
 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_worldPos;
 in vec3 v_normal;
 in vec3 v_normal;
 in vec2 v_uv;
 in vec2 v_uv;
-layout (location = 0) out vec4 FragColor;
+layout(location = 0) out vec4 FragColor;
 uniform vec3 u_grassPrimary;
 uniform vec3 u_grassPrimary;
 uniform vec3 u_grassSecondary;
 uniform vec3 u_grassSecondary;
 uniform vec3 u_grassDry;
 uniform vec3 u_grassDry;
@@ -17,53 +17,70 @@ uniform float u_soilBlendSharpness;
 uniform float u_ambientBoost;
 uniform float u_ambientBoost;
 uniform vec3 u_lightDir;
 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
 #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_mvp;
 uniform mat4 u_model;
 uniform mat4 u_model;
 uniform vec2 u_noiseOffset;
 uniform vec2 u_noiseOffset;
@@ -11,22 +11,43 @@ out vec3 v_worldPos;
 out vec3 v_normal;
 out vec3 v_normal;
 out vec2 v_uv;
 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;
 out vec4 FragColor;
 
 
 void main() {
 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 = 0) in vec3 aPos;
 layout(location = 1) in vec3 aNormal;
 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;
 uniform mat4 uViewProj;
 
 
@@ -12,26 +12,26 @@ out vec3 vNormal;
 out vec3 vColor;
 out vec3 vColor;
 
 
 void main() {
 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 vec2 v_uv;
 in float v_vertexDisplacement;
 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_tileSize, u_macroNoiseScale, u_detailNoiseScale;
 uniform float u_slopeRockThreshold, u_slopeRockSharpness;
 uniform float u_slopeRockThreshold, u_slopeRockSharpness;
 uniform float u_soilBlendHeight, u_soilBlendSharpness;
 uniform float u_soilBlendHeight, u_soilBlendSharpness;
@@ -17,243 +17,270 @@ uniform float u_heightNoiseStrength, u_heightNoiseFrequency;
 uniform float u_ambientBoost, u_rockDetailStrength;
 uniform float u_ambientBoost, u_rockDetailStrength;
 
 
 // lets soil “climb” up steep toes (world units)
 // 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) --------
 // -------- 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 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)
 // 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 --------------------------------------------
 // ---- 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)
 // (kept for reference; no longer used by the fix)
 // float maxRiseNearby(vec2 uv, int r){ ... }  // removed to avoid confusion
 // float maxRiseNearby(vec2 uv, int r){ ... }  // removed to avoid confusion
 
 
 // --- world <-> UV helpers for height texture sampling (FIX ADD) ---
 // --- 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
 #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_mvp;
 uniform mat4 u_model;
 uniform mat4 u_model;
@@ -17,73 +17,75 @@ out float v_vertexDisplacement;
 
 
 // Simple hash function for noise
 // Simple hash function for noise
 float hash21(vec2 p) {
 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
 // Value noise
 float noise21(vec2 p) {
 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)
 // Low-octave fBM (2 octaves for smooth, broad displacement)
 float fbm2(vec2 p) {
 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
 // 2D rotation matrix
 mat2 rot2(float angle) {
 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() {
 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 {
 namespace GL {
 class Renderer;
 class Renderer;
 class Camera;
 class Camera;
-} 
-} 
+} // namespace GL
+} // namespace Render
 
 
 namespace Game {
 namespace Game {
 namespace Map {
 namespace Map {
@@ -19,5 +19,5 @@ struct Environment {
                            Render::GL::Camera &camera);
                            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;
   return res;
 }
 }
 
 
-} 
-} 
+} // namespace Map
+} // namespace Game

+ 19 - 23
game/map/map_catalog.cpp

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

+ 9 - 10
game/map/map_catalog.h

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

+ 15 - 15
game/map/skirmish_loader.cpp

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

+ 17 - 15
game/map/skirmish_loader.h

@@ -11,8 +11,8 @@ namespace Engine {
 namespace Core {
 namespace Core {
 class World;
 class World;
 using EntityID = unsigned int;
 using EntityID = unsigned int;
-}
-}
+} // namespace Core
+} // namespace Engine
 
 
 namespace Render {
 namespace Render {
 namespace GL {
 namespace GL {
@@ -23,8 +23,8 @@ class TerrainRenderer;
 class BiomeRenderer;
 class BiomeRenderer;
 class FogRenderer;
 class FogRenderer;
 class StoneRenderer;
 class StoneRenderer;
-}
-}
+} // namespace GL
+} // namespace Render
 
 
 namespace Game {
 namespace Game {
 namespace Map {
 namespace Map {
@@ -51,12 +51,15 @@ public:
   using OwnersUpdatedCallback = std::function<void()>;
   using OwnersUpdatedCallback = std::function<void()>;
   using VisibilityMaskReadyCallback = 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 setBiomeRenderer(Render::GL::BiomeRenderer *biome) { m_biome = biome; }
   void setFogRenderer(Render::GL::FogRenderer *fog) { m_fog = fog; }
   void setFogRenderer(Render::GL::FogRenderer *fog) { m_fog = fog; }
   void setStoneRenderer(Render::GL::StoneRenderer *stone) { m_stone = stone; }
   void setStoneRenderer(Render::GL::StoneRenderer *stone) { m_stone = stone; }
@@ -64,15 +67,14 @@ public:
   void setOnOwnersUpdated(OwnersUpdatedCallback callback) {
   void setOnOwnersUpdated(OwnersUpdatedCallback callback) {
     m_onOwnersUpdated = callback;
     m_onOwnersUpdated = callback;
   }
   }
-  
+
   void setOnVisibilityMaskReady(VisibilityMaskReadyCallback callback) {
   void setOnVisibilityMaskReady(VisibilityMaskReadyCallback callback) {
     m_onVisibilityMaskReady = callback;
     m_onVisibilityMaskReady = callback;
   }
   }
 
 
   SkirmishLoadResult start(const QString &mapPath,
   SkirmishLoadResult start(const QString &mapPath,
-                          const QVariantList &playerConfigs,
-                          int selectedPlayerId,
-                          int &outSelectedPlayerId);
+                           const QVariantList &playerConfigs,
+                           int selectedPlayerId, int &outSelectedPlayerId);
 
 
 private:
 private:
   Engine::Core::World &m_world;
   Engine::Core::World &m_world;
@@ -87,5 +89,5 @@ private:
   VisibilityMaskReadyCallback m_onVisibilityMaskReady;
   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);
   return m_heightMap->getTerrainType(gridX, gridZ);
 }
 }
 
 
-} 
+} // namespace Game::Map

+ 1 - 1
game/map/terrain_service.h

@@ -44,4 +44,4 @@ private:
   BiomeSettings m_biomeSettings;
   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) {
   if (ground) {
     ground->configureExtent(50.0f);
     ground->configureExtent(50.0f);
   }
   }
-  
+
   return true;
   return true;
 }
 }
 
 
 void WorldBootstrap::ensureInitialized(bool &initialized,
 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) {
   if (!initialized) {
     initialized = initialize(renderer, camera, ground, outError);
     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 Renderer;
 class Camera;
 class Camera;
 class GroundRenderer;
 class GroundRenderer;
-}
-}
+} // namespace GL
+} // namespace Render
 
 
 namespace Game {
 namespace Game {
 namespace Map {
 namespace Map {
@@ -19,7 +19,7 @@ public:
                          Render::GL::Camera &camera,
                          Render::GL::Camera &camera,
                          Render::GL::GroundRenderer *ground = nullptr,
                          Render::GL::GroundRenderer *ground = nullptr,
                          QString *outError = nullptr);
                          QString *outError = nullptr);
-  
+
   static void ensureInitialized(bool &initialized,
   static void ensureInitialized(bool &initialized,
                                 Render::GL::Renderer &renderer,
                                 Render::GL::Renderer &renderer,
                                 Render::GL::Camera &camera,
                                 Render::GL::Camera &camera,
@@ -27,5 +27,5 @@ public:
                                 QString *outError = nullptr);
                                 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_reasoner.h"
-#include "ai_utils.h"
 #include "../../game_config.h"
 #include "../../game_config.h"
+#include "ai_utils.h"
 #include <algorithm>
 #include <algorithm>
 #include <cmath>
 #include <cmath>
 #include <limits>
 #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;
     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<Engine::Core::EntityID> unitsToMove;
   std::vector<float> targetX, targetY, targetZ;
   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,
 void CameraService::orbit(Render::GL::Camera &camera, float yawDeg,
-                           float pitchDeg) {
+                          float pitchDeg) {
   if (!std::isfinite(yawDeg) || !std::isfinite(pitchDeg)) {
   if (!std::isfinite(yawDeg) || !std::isfinite(pitchDeg)) {
     return;
     return;
   }
   }
@@ -109,8 +109,8 @@ void CameraService::snapToEntity(Render::GL::Camera &camera,
   if (auto *t = entity.getComponent<Engine::Core::TransformComponent>()) {
   if (auto *t = entity.getComponent<Engine::Core::TransformComponent>()) {
     QVector3D center(t->position.x, t->position.y, t->position.z);
     QVector3D center(t->position.x, t->position.y, t->position.z);
     const auto &camConfig = Game::GameConfig::instance().camera();
     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 Render {
 namespace GL {
 namespace GL {
 class Camera;
 class Camera;
-} // namespace GL
+}
 } // namespace Render
 } // namespace Render
 
 
 namespace Game {
 namespace Game {

+ 3 - 4
game/systems/formation_system.cpp

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

+ 8 - 8
game/systems/formation_system.h

@@ -11,7 +11,7 @@ namespace Systems {
 
 
 enum class FormationType { Roman, Barbarian };
 enum class FormationType { Roman, Barbarian };
 
 
-} // namespace Systems
+}
 } // namespace Game
 } // namespace Game
 
 
 namespace std {
 namespace std {
@@ -20,7 +20,7 @@ template <> struct hash<Game::Systems::FormationType> {
     return std::hash<int>()(static_cast<int>(ft));
     return std::hash<int>()(static_cast<int>(ft));
   }
   }
 };
 };
-}
+} // namespace std
 
 
 namespace Game {
 namespace Game {
 namespace Systems {
 namespace Systems {
@@ -38,18 +38,18 @@ public:
 
 
 class RomanFormation : public IFormation {
 class RomanFormation : public IFormation {
 public:
 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; }
   FormationType getType() const override { return FormationType::Roman; }
 };
 };
 
 
 class BarbarianFormation : public IFormation {
 class BarbarianFormation : public IFormation {
 public:
 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; }
   FormationType getType() const override { return FormationType::Barbarian; }
 };
 };

+ 2 - 2
game/systems/production_service.cpp

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

+ 13 - 14
game/systems/selection_system.cpp

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

+ 3 - 2
game/systems/selection_system.h

@@ -10,7 +10,7 @@ namespace Engine {
 namespace Core {
 namespace Core {
 class Entity;
 class Entity;
 class World;
 class World;
-}
+} // namespace Core
 } // namespace Engine
 } // namespace Engine
 
 
 namespace Game::Systems {
 namespace Game::Systems {
@@ -41,7 +41,8 @@ class SelectionController : public QObject {
 public:
 public:
   SelectionController(Engine::Core::World *world,
   SelectionController(Engine::Core::World *world,
                       SelectionSystem *selectionSystem,
                       SelectionSystem *selectionSystem,
-                      PickingService *pickingService, QObject *parent = nullptr);
+                      PickingService *pickingService,
+                      QObject *parent = nullptr);
 
 
   void onClickSelect(qreal sx, qreal sy, bool additive, int viewportWidth,
   void onClickSelect(qreal sx, qreal sy, bool additive, int viewportWidth,
                      int viewportHeight, void *camera, int localOwnerId);
                      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;
   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")
   if (event.unitType == "barracks")
     return;
     return;
 
 

+ 1 - 1
game/systems/troop_count_registry.h

@@ -30,7 +30,7 @@ private:
   TroopCountRegistry &operator=(const TroopCountRegistry &) = delete;
   TroopCountRegistry &operator=(const TroopCountRegistry &) = delete;
 
 
   std::unordered_map<int, int> m_troopCounts;
   std::unordered_map<int, int> m_troopCounts;
-  
+
   Engine::Core::ScopedEventSubscription<Engine::Core::UnitSpawnedEvent>
   Engine::Core::ScopedEventSubscription<Engine::Core::UnitSpawnedEvent>
       m_unitSpawnedSubscription;
       m_unitSpawnedSubscription;
   Engine::Core::ScopedEventSubscription<Engine::Core::UnitDiedEvent>
   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);
   auto color = registry.getOwnerColor(ownerId);
   return QVector3D(color[0], color[1], color[2]);
   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 {
 namespace Core {
 class RenderableComponent;
 class RenderableComponent;
 }
 }
-} 
+} // namespace Engine
 
 
 namespace Game::Visuals {
 namespace Game::Visuals {
 
 
@@ -35,4 +35,4 @@ VisualDef::MeshKind meshKindFromString(const QString &s);
 void applyToRenderable(const VisualDef &def,
 void applyToRenderable(const VisualDef &def,
                        Engine::Core::RenderableComponent &r);
                        Engine::Core::RenderableComponent &r);
 
 
-} 
+} // namespace Game::Visuals

+ 116 - 151
ui/qml/CursorManager.qml

@@ -2,184 +2,149 @@ import QtQuick 2.15
 
 
 Item {
 Item {
     id: cursorManager
     id: cursorManager
-    
+
     property string currentMode: "normal"
     property string currentMode: "normal"
     property var cursorItem: null
     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 {
     Component {
         id: attackCursorComponent
         id: attackCursorComponent
+
         Canvas {
         Canvas {
             width: 32
             width: 32
             height: 32
             height: 32
             onPaint: {
             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 {
     Component {
         id: guardCursorComponent
         id: guardCursorComponent
+
         Canvas {
         Canvas {
             width: 32
             width: 32
             height: 32
             height: 32
             onPaint: {
             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 {
     Component {
         id: patrolCursorComponent
         id: patrolCursorComponent
+
         Canvas {
         Canvas {
             width: 32
             width: 32
             height: 32
             height: 32
             onPaint: {
             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 {
 Item {
     id: gameView
     id: gameView
-    objectName: "GameView"
-    
+
     property bool isPaused: false
     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 mapClicked(real x, real y)
     signal unitSelected(int unitId)
     signal unitSelected(int unitId)
     signal areaSelected(real x1, real y1, real x2, real y2)
     signal areaSelected(real x1, real y1, real x2, real y2)
-    
-    property string cursorMode: "normal"  
-    
+
     function setPaused(paused) {
     function setPaused(paused) {
-        isPaused = paused
+        isPaused = paused;
         if (typeof game !== 'undefined' && game.setPaused)
         if (typeof game !== 'undefined' && game.setPaused)
-            game.setPaused(paused)
+            game.setPaused(paused);
+
     }
     }
-    
+
     function setGameSpeed(speed) {
     function setGameSpeed(speed) {
-        gameSpeed = speed
+        gameSpeed = speed;
         if (typeof game !== 'undefined' && game.setGameSpeed)
         if (typeof game !== 'undefined' && game.setGameSpeed)
-            game.setGameSpeed(speed)
+            game.setGameSpeed(speed);
+
     }
     }
-    
+
     function issueCommand(command) {
     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
         id: renderArea
+
+        property int keyPanCount: 0
+        property bool mousePanActive: false
+
         anchors.fill: parent
         anchors.fill: parent
-        engine: game 
+        engine: game
         focus: false
         focus: false
-        
-        
+        Component.onCompleted: {
+            if (typeof game !== 'undefined' && game.cursorMode)
+                gameView.cursorMode = game.cursorMode;
+
+        }
+
         Connections {
         Connections {
-            target: game
             function onCursorModeChanged() {
             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 {
         MouseArea {
             id: mouseArea
             id: mouseArea
+
+            property bool isSelecting: false
+            property real startX: 0
+            property real startY: 0
+
             anchors.fill: parent
             anchors.fill: parent
             acceptedButtons: Qt.LeftButton | Qt.RightButton
             acceptedButtons: Qt.LeftButton | Qt.RightButton
             hoverEnabled: true
             hoverEnabled: true
             propagateComposedEvents: true
             propagateComposedEvents: true
             preventStealing: true
             preventStealing: true
-            
-            
             cursorShape: (gameView.cursorMode === "normal") ? Qt.ArrowCursor : Qt.BlankCursor
             cursorShape: (gameView.cursorMode === "normal") ? Qt.ArrowCursor : Qt.BlankCursor
-            
             enabled: gameView.visible
             enabled: gameView.visible
-            
             onEntered: {
             onEntered: {
-                
-                if (typeof game !== 'undefined' && game.setHoverAtScreen) {
-                    game.setHoverAtScreen(0, 0) 
-                }
+                if (typeof game !== 'undefined' && game.setHoverAtScreen)
+                    game.setHoverAtScreen(0, 0);
+
             }
             }
-            
             onExited: {
             onExited: {
-                
-                if (typeof game !== 'undefined' && game.setHoverAtScreen) {
-                    game.setHoverAtScreen(-1, -1)
-                }
+                if (typeof game !== 'undefined' && game.setHoverAtScreen)
+                    game.setHoverAtScreen(-1, -1);
+
             }
             }
-            
             onPositionChanged: function(mouse) {
             onPositionChanged: function(mouse) {
-                
-                
                 if (isSelecting) {
                 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 {
                 } 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) {
             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) {
             onPressed: function(mouse) {
                 if (mouse.button === Qt.LeftButton) {
                 if (mouse.button === Qt.LeftButton) {
-                    
                     if (gameView.setRallyMode) {
                     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 (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 (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) {
                 } 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) {
             onReleased: function(mouse) {
                 if (mouse.button === Qt.LeftButton && isSelecting) {
                 if (mouse.button === Qt.LeftButton && isSelecting) {
-                    isSelecting = false
-                    selectionBox.visible = false
-                    
+                    isSelecting = false;
+                    selectionBox.visible = false;
                     if (selectionBox.width > 5 && selectionBox.height > 5) {
                     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 {
                     } 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) {
                 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 {
         Rectangle {
             id: selectionBox
             id: selectionBox
+
             visible: false
             visible: false
             border.color: "white"
             border.color: "white"
             border.width: 1
             border.width: 1
             color: "transparent"
             color: "transparent"
         }
         }
+
     }
     }
-    
-    
+
     Item {
     Item {
         id: customCursorContainer
         id: customCursorContainer
+
         visible: gameView.cursorMode !== "normal"
         visible: gameView.cursorMode !== "normal"
         width: 32
         width: 32
         height: 32
         height: 32
         z: 999999
         z: 999999
-        
-        
         x: (typeof game !== 'undefined' && game.globalCursorX) ? game.globalCursorX - 16 : 0
         x: (typeof game !== 'undefined' && game.globalCursorX) ? game.globalCursorX - 16 : 0
         y: (typeof game !== 'undefined' && game.globalCursorY) ? game.globalCursorY - 16 : 0
         y: (typeof game !== 'undefined' && game.globalCursorY) ? game.globalCursorY - 16 : 0
-        
-        
+
         Item {
         Item {
             id: attackCursorContainer
             id: attackCursorContainer
+
+            property real pulseScale: 1
+
             visible: gameView.cursorMode === "attack"
             visible: gameView.cursorMode === "attack"
             anchors.fill: parent
             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 {
             Canvas {
                 id: attackCursor
                 id: attackCursor
+
                 anchors.fill: parent
                 anchors.fill: parent
                 scale: attackCursorContainer.pulseScale
                 scale: attackCursorContainer.pulseScale
                 transformOrigin: Item.Center
                 transformOrigin: Item.Center
-                
                 onPaint: {
                 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()
                 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 {
         Canvas {
             id: guardCursor
             id: guardCursor
+
             visible: gameView.cursorMode === "guard"
             visible: gameView.cursorMode === "guard"
             anchors.fill: parent
             anchors.fill: parent
             onPaint: {
             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()
             Component.onCompleted: requestPaint()
         }
         }
-        
-        
+
         Canvas {
         Canvas {
             id: patrolCursor
             id: patrolCursor
+
             visible: gameView.cursorMode === "patrol"
             visible: gameView.cursorMode === "patrol"
             anchors.fill: parent
             anchors.fill: parent
             onPaint: {
             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()
             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 {
 Item {
     id: hud
     id: hud
-    
-    signal pauseToggled()
-    signal speedChanged(real speed)
-    signal commandModeChanged(string mode) 
-    signal recruit(string unitType)
-    
+
     property bool gameIsPaused: false
     property bool gameIsPaused: false
-    property real currentSpeed: 1.0
+    property real currentSpeed: 1
     property string currentCommandMode: "normal"
     property string currentCommandMode: "normal"
-    
-    
     property int topPanelHeight: topPanel.height
     property int topPanelHeight: topPanel.height
     property int bottomPanelHeight: bottomPanel.height
     property int bottomPanelHeight: bottomPanel.height
-    
     property int selectionTick: 0
     property int selectionTick: 0
     property bool hasMovableUnits: false
     property bool hasMovableUnits: false
 
 
+    signal pauseToggled()
+    signal speedChanged(real speed)
+    signal commandModeChanged(string mode)
+    signal recruit(string unitType)
+
     Connections {
     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) {
             if (typeof game !== 'undefined' && game.hasUnitsSelected && game.hasSelectedType) {
-                
-                var troopTypes = ["warrior", "archer"]
+                var troopTypes = ["warrior", "archer"];
                 for (var i = 0; i < troopTypes.length; i++) {
                 for (var i = 0; i < troopTypes.length; i++) {
                     if (game.hasSelectedType(troopTypes[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) {
             if (currentCommandMode !== actualMode) {
-                currentCommandMode = actualMode
-                commandModeChanged(actualMode)
+                currentCommandMode = actualMode;
+                commandModeChanged(actualMode);
             }
             }
-            
-            hasMovableUnits = hasTroops
+            hasMovableUnits = hasTroops;
         }
         }
+
+        target: (typeof game !== 'undefined') ? game : null
     }
     }
 
 
-    
     Timer {
     Timer {
         id: productionRefresh
         id: productionRefresh
+
         interval: 100
         interval: 100
         repeat: true
         repeat: true
         running: true
         running: true
         onTriggered: {
         onTriggered: {
-            selectionTick += 1
-            
-            
+            selectionTick += 1;
             if (hasMovableUnits && typeof game !== 'undefined' && game.getSelectedUnitsCommandMode) {
             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 {
     Item {
         id: topPanel
         id: topPanel
+
         anchors.top: parent.top
         anchors.top: parent.top
         anchors.left: parent.left
         anchors.left: parent.left
         anchors.right: parent.right
         anchors.right: parent.right
         height: Math.max(50, parent.height * 0.08)
         height: Math.max(50, parent.height * 0.08)
+
         HUDTop {
         HUDTop {
             id: hudTop
             id: hudTop
+
             anchors.fill: parent
             anchors.fill: parent
             gameIsPaused: hud.gameIsPaused
             gameIsPaused: hud.gameIsPaused
             currentSpeed: hud.currentSpeed
             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 {
     Item {
         id: bottomPanel
         id: bottomPanel
+
         anchors.bottom: parent.bottom
         anchors.bottom: parent.bottom
         anchors.left: parent.left
         anchors.left: parent.left
         anchors.right: parent.right
         anchors.right: parent.right
-        height: Math.max(140, parent.height * 0.20)
+        height: Math.max(140, parent.height * 0.2)
+
         HUDBottom {
         HUDBottom {
             id: hudBottom
             id: hudBottom
+
             anchors.fill: parent
             anchors.fill: parent
             currentCommandMode: hud.currentCommandMode
             currentCommandMode: hud.currentCommandMode
             selectionTick: hud.selectionTick
             selectionTick: hud.selectionTick
             hasMovableUnits: hud.hasMovableUnits
             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 {
 RowLayout {
     id: bottomRoot
     id: bottomRoot
-    
-    
+
     property string currentCommandMode
     property string currentCommandMode
     property int selectionTick
     property int selectionTick
     property bool hasMovableUnits
     property bool hasMovableUnits
-    
-    
+
     signal commandModeChanged(string mode)
     signal commandModeChanged(string mode)
     signal recruit(string unitType)
     signal recruit(string unitType)
-    
+
     anchors.fill: parent
     anchors.fill: parent
     anchors.margins: 10
     anchors.margins: 10
     spacing: 12
     spacing: 12
 
 
-    
     Rectangle {
     Rectangle {
         Layout.fillWidth: true
         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.fillHeight: true
         Layout.alignment: Qt.AlignTop
         Layout.alignment: Qt.AlignTop
         color: "#0f1419"
         color: "#0f1419"
@@ -34,8 +31,20 @@ RowLayout {
             anchors.margins: 6
             anchors.margins: 6
             spacing: 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 {
             ScrollView {
@@ -49,6 +58,7 @@ RowLayout {
 
 
                 ListView {
                 ListView {
                     id: selectedUnitsList
                     id: selectedUnitsList
+
                     model: (typeof game !== 'undefined' && game.selectedUnitsModel) ? game.selectedUnitsModel : null
                     model: (typeof game !== 'undefined' && game.selectedUnitsModel) ? game.selectedUnitsModel : null
                     boundsBehavior: Flickable.StopAtBounds
                     boundsBehavior: Flickable.StopAtBounds
                     flickableDirection: Flickable.VerticalFlick
                     flickableDirection: Flickable.VerticalFlick
@@ -88,25 +98,35 @@ RowLayout {
                                     width: parent.width * (typeof healthRatio !== 'undefined' ? healthRatio : 0)
                                     width: parent.width * (typeof healthRatio !== 'undefined' ? healthRatio : 0)
                                     height: parent.height
                                     height: parent.height
                                     color: {
                                     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
                                     radius: 6
                                 }
                                 }
+
                             }
                             }
+
                         }
                         }
+
                     }
                     }
+
                 }
                 }
+
             }
             }
+
         }
         }
+
     }
     }
 
 
-    
     Column {
     Column {
         Layout.fillWidth: true
         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.fillHeight: true
         Layout.alignment: Qt.AlignTop
         Layout.alignment: Qt.AlignTop
         spacing: 8
         spacing: 8
@@ -118,50 +138,240 @@ RowLayout {
             border.color: bottomRoot.currentCommandMode === "normal" ? "#34495e" : (bottomRoot.currentCommandMode === "attack" ? "#e74c3c" : "#3498db")
             border.color: bottomRoot.currentCommandMode === "normal" ? "#34495e" : (bottomRoot.currentCommandMode === "attack" ? "#e74c3c" : "#3498db")
             border.width: 2
             border.width: 2
             radius: 6
             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
                 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")
                 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"))
                 color: !bottomRoot.hasMovableUnits ? "#5a6c7d" : (bottomRoot.currentCommandMode === "normal" ? "#7f8c8d" : (bottomRoot.currentCommandMode === "attack" ? "#ff6b6b" : "#3498db"))
                 font.pointSize: bottomRoot.currentCommandMode === "normal" ? 10 : 11
                 font.pointSize: bottomRoot.currentCommandMode === "normal" ? 10 : 11
                 font.bold: bottomRoot.currentCommandMode !== "normal" && bottomRoot.hasMovableUnits
                 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 {
         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
             width: parent.width
             columns: 3
             columns: 3
             rowSpacing: 6
             rowSpacing: 6
             columnSpacing: 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 {
     Rectangle {
         Layout.fillWidth: true
         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.fillHeight: true
         Layout.alignment: Qt.AlignTop
         Layout.alignment: Qt.AlignTop
         color: "#34495e"
         color: "#34495e"
@@ -173,10 +383,18 @@ RowLayout {
             anchors.margins: 8
             anchors.margins: 8
             spacing: 6
             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 {
             ScrollView {
                 id: prodScroll
                 id: prodScroll
+
                 anchors.left: parent.left
                 anchors.left: parent.left
                 anchors.right: parent.right
                 anchors.right: parent.right
                 anchors.top: prodHeader.bottom
                 anchors.top: prodHeader.bottom
@@ -190,20 +408,32 @@ RowLayout {
 
 
                     Repeater {
                     Repeater {
                         model: (bottomRoot.selectionTick, (typeof game !== 'undefined' && game.hasSelectedType && game.hasSelectedType("barracks"))) ? 1 : 0
                         model: (bottomRoot.selectionTick, (typeof game !== 'undefined' && game.hasSelectedType && game.hasSelectedType("barracks"))) ? 1 : 0
+
                         delegate: Column {
                         delegate: Column {
+                            property var prod: (bottomRoot.selectionTick, (typeof game !== 'undefined' && game.getSelectedProductionState) ? game.getSelectedProductionState() : ({
+                            }))
+
                             spacing: 6
                             spacing: 6
-                            property var prod: (bottomRoot.selectionTick, (typeof game !== 'undefined' && game.getSelectedProductionState) ? game.getSelectedProductionState() : ({}))
 
 
                             Button {
                             Button {
                                 id: recruitBtn
                                 id: recruitBtn
+
                                 text: "Recruit Archer (" + (prod.villagerCost || 1) + ")"
                                 text: "Recruit Archer (" + (prod.villagerCost || 1) + ")"
                                 focusPolicy: Qt.NoFocus
                                 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")
                                 onClicked: bottomRoot.recruit("archer")
                                 ToolTip.visible: hovered
                                 ToolTip.visible: hovered
@@ -224,41 +454,82 @@ RowLayout {
                                     anchors.left: parent.left
                                     anchors.left: parent.left
                                     anchors.verticalCenter: parent.verticalCenter
                                     anchors.verticalCenter: parent.verticalCenter
                                     height: parent.height
                                     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"
                                     color: "#27ae60"
                                     radius: 4
                                     radius: 4
                                 }
                                 }
+
                             }
                             }
 
 
                             Row {
                             Row {
                                 spacing: 8
                                 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 {
                             Row {
                                 spacing: 6
                                 spacing: 6
+
                                 Button {
                                 Button {
                                     text: (typeof gameView !== 'undefined' && gameView.setRallyMode) ? "Click map to set rally (right-click to cancel)" : "Set Rally"
                                     text: (typeof gameView !== 'undefined' && gameView.setRallyMode) ? "Click map to set rally (right-click to cancel)" : "Set Rally"
                                     focusPolicy: Qt.NoFocus
                                     focusPolicy: Qt.NoFocus
                                     enabled: !!prod.hasBarracks
                                     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 {
                     Item {
                         visible: (bottomRoot.selectionTick, (typeof game === 'undefined' || !game.hasSelectedType || !game.hasSelectedType("barracks")))
                         visible: (bottomRoot.selectionTick, (typeof game === 'undefined' || !game.hasSelectedType || !game.hasSelectedType("barracks")))
                         width: parent.width
                         width: parent.width
                         height: 30
                         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 {
 Item {
     id: topRoot
     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 int barMinHeight: 72
     readonly property bool compact: width < 800
     readonly property bool compact: width < 800
     readonly property bool ultraCompact: width < 560
     readonly property bool ultraCompact: width < 560
 
 
+    signal pauseToggled()
+    signal speedChanged(real speed)
+
     Rectangle {
     Rectangle {
         id: topPanel
         id: topPanel
+
         anchors.left: parent.left
         anchors.left: parent.left
         anchors.right: parent.right
         anchors.right: parent.right
         anchors.top: parent.top
         anchors.top: parent.top
@@ -24,58 +25,82 @@ Item {
         opacity: 0.98
         opacity: 0.98
         clip: true
         clip: true
 
 
-        
         Rectangle {
         Rectangle {
             anchors.fill: parent
             anchors.fill: parent
+            opacity: 0.9
+
             gradient: Gradient {
             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 {
         Rectangle {
             anchors.left: parent.left
             anchors.left: parent.left
             anchors.right: parent.right
             anchors.right: parent.right
             anchors.bottom: parent.bottom
             anchors.bottom: parent.bottom
             height: 2
             height: 2
+
             gradient: Gradient {
             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 {
         RowLayout {
             id: barRow
             id: barRow
+
             anchors.fill: parent
             anchors.fill: parent
             anchors.margins: 8
             anchors.margins: 8
             spacing: 12
             spacing: 12
 
 
-            
             RowLayout {
             RowLayout {
                 id: leftGroup
                 id: leftGroup
+
                 spacing: 10
                 spacing: 10
                 Layout.alignment: Qt.AlignVCenter
                 Layout.alignment: Qt.AlignVCenter
 
 
-                
                 Button {
                 Button {
                     id: pauseBtn
                     id: pauseBtn
+
                     Layout.preferredWidth: topRoot.compact ? 48 : 56
                     Layout.preferredWidth: topRoot.compact ? 48 : 56
                     Layout.preferredHeight: Math.min(40, topPanel.height - 12)
                     Layout.preferredHeight: Math.min(40, topPanel.height - 12)
-                    text: topRoot.gameIsPaused ? "\u25B6" : "\u23F8" 
+                    text: topRoot.gameIsPaused ? "\u25B6" : "\u23F8"
                     font.pixelSize: 26
                     font.pixelSize: 26
                     font.bold: true
                     font.bold: true
                     focusPolicy: Qt.NoFocus
                     focusPolicy: Qt.NoFocus
+                    onClicked: topRoot.pauseToggled()
+
                     background: Rectangle {
                     background: Rectangle {
-                        color: parent.pressed ? "#e74c3c"
-                              : parent.hovered ? "#c0392b" : "#34495e"
+                        color: parent.pressed ? "#e74c3c" : parent.hovered ? "#c0392b" : "#34495e"
                         radius: 6
                         radius: 6
                         border.color: "#2c3e50"
                         border.color: "#2c3e50"
                         border.width: 1
                         border.width: 1
                     }
                     }
+
                     contentItem: Text {
                     contentItem: Text {
                         text: parent.text
                         text: parent.text
                         font: parent.font
                         font: parent.font
@@ -83,21 +108,35 @@ Item {
                         horizontalAlignment: Text.AlignHCenter
                         horizontalAlignment: Text.AlignHCenter
                         verticalAlignment: Text.AlignVCenter
                         verticalAlignment: Text.AlignVCenter
                     }
                     }
-                    onClicked: topRoot.pauseToggled()
+
                 }
                 }
 
 
-                
                 Rectangle {
                 Rectangle {
-                    width: 2; Layout.fillHeight: true; radius: 1
+                    width: 2
+                    Layout.fillHeight: true
+                    radius: 1
                     visible: !topRoot.compact
                     visible: !topRoot.compact
+
                     gradient: Gradient {
                     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 {
                 RowLayout {
                     spacing: 8
                     spacing: 8
                     Layout.alignment: Qt.AlignVCenter
                     Layout.alignment: Qt.AlignVCenter
@@ -113,29 +152,38 @@ Item {
 
 
                     Row {
                     Row {
                         id: speedRow
                         id: speedRow
+
+                        property var options: [0.5, 1, 2]
+
                         spacing: 8
                         spacing: 8
                         visible: !topRoot.compact
                         visible: !topRoot.compact
 
 
-                        property var options: [0.5, 1.0, 2.0]
-                        ButtonGroup { id: speedGroup }
+                        ButtonGroup {
+                            id: speedGroup
+                        }
 
 
                         Repeater {
                         Repeater {
                             model: speedRow.options
                             model: speedRow.options
+
                             delegate: Button {
                             delegate: Button {
                                 Layout.minimumWidth: 48
                                 Layout.minimumWidth: 48
-                                width: 56; height: Math.min(34, topPanel.height - 16)
+                                width: 56
+                                height: Math.min(34, topPanel.height - 16)
                                 checkable: true
                                 checkable: true
                                 enabled: !topRoot.gameIsPaused
                                 enabled: !topRoot.gameIsPaused
                                 checked: (topRoot.currentSpeed === modelData) && !topRoot.gameIsPaused
                                 checked: (topRoot.currentSpeed === modelData) && !topRoot.gameIsPaused
                                 focusPolicy: Qt.NoFocus
                                 focusPolicy: Qt.NoFocus
                                 text: modelData + "x"
                                 text: modelData + "x"
+                                ButtonGroup.group: speedGroup
+                                onClicked: topRoot.speedChanged(modelData)
+
                                 background: Rectangle {
                                 background: Rectangle {
-                                    color: parent.checked ? "#27ae60"
-                                          : parent.hovered ? "#34495e" : "#2c3e50"
+                                    color: parent.checked ? "#27ae60" : parent.hovered ? "#34495e" : "#2c3e50"
                                     radius: 6
                                     radius: 6
                                     border.color: parent.checked ? "#229954" : "#1a252f"
                                     border.color: parent.checked ? "#229954" : "#1a252f"
                                     border.width: 1
                                     border.width: 1
                                 }
                                 }
+
                                 contentItem: Text {
                                 contentItem: Text {
                                     text: parent.text
                                     text: parent.text
                                     font.pixelSize: 13
                                     font.pixelSize: 13
@@ -144,40 +192,56 @@ Item {
                                     horizontalAlignment: Text.AlignHCenter
                                     horizontalAlignment: Text.AlignHCenter
                                     verticalAlignment: Text.AlignVCenter
                                     verticalAlignment: Text.AlignVCenter
                                 }
                                 }
-                                ButtonGroup.group: speedGroup
-                                onClicked: topRoot.speedChanged(modelData)
+
                             }
                             }
+
                         }
                         }
+
                     }
                     }
 
 
                     ComboBox {
                     ComboBox {
                         id: speedCombo
                         id: speedCombo
+
                         visible: topRoot.compact
                         visible: topRoot.compact
                         Layout.preferredWidth: 120
                         Layout.preferredWidth: 120
                         model: ["0.5x", "1x", "2x"]
                         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
                         enabled: !topRoot.gameIsPaused
                         font.pixelSize: 13
                         font.pixelSize: 13
                         onActivated: function(i) {
                         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 {
                 Rectangle {
-                    width: 2; Layout.fillHeight: true; radius: 1
+                    width: 2
+                    Layout.fillHeight: true
+                    radius: 1
                     visible: !topRoot.compact
                     visible: !topRoot.compact
+
                     gradient: Gradient {
                     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 {
                 RowLayout {
                     spacing: 8
                     spacing: 8
                     Layout.alignment: Qt.AlignVCenter
                     Layout.alignment: Qt.AlignVCenter
@@ -193,70 +257,104 @@ Item {
 
 
                     Button {
                     Button {
                         id: followBtn
                         id: followBtn
+
                         Layout.preferredWidth: topRoot.compact ? 44 : 80
                         Layout.preferredWidth: topRoot.compact ? 44 : 80
                         Layout.preferredHeight: Math.min(34, topPanel.height - 16)
                         Layout.preferredHeight: Math.min(34, topPanel.height - 16)
                         checkable: true
                         checkable: true
                         text: topRoot.compact ? "\u2609" : "Follow"
                         text: topRoot.compact ? "\u2609" : "Follow"
                         font.pixelSize: 13
                         font.pixelSize: 13
                         focusPolicy: Qt.NoFocus
                         focusPolicy: Qt.NoFocus
+                        onToggled: {
+                            if (typeof game !== 'undefined' && game.cameraFollowSelection)
+                                game.cameraFollowSelection(checked);
+
+                        }
+
                         background: Rectangle {
                         background: Rectangle {
-                            color: parent.checked ? "#3498db"
-                                  : parent.hovered ? "#34495e" : "#2c3e50"
+                            color: parent.checked ? "#3498db" : parent.hovered ? "#34495e" : "#2c3e50"
                             radius: 6
                             radius: 6
                             border.color: parent.checked ? "#2980b9" : "#1a252f"
                             border.color: parent.checked ? "#2980b9" : "#1a252f"
                             border.width: 1
                             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 {
                     Button {
                         id: resetBtn
                         id: resetBtn
+
                         Layout.preferredWidth: topRoot.compact ? 44 : 80
                         Layout.preferredWidth: topRoot.compact ? 44 : 80
                         Layout.preferredHeight: Math.min(34, topPanel.height - 16)
                         Layout.preferredHeight: Math.min(34, topPanel.height - 16)
-                        text: topRoot.compact ? "\u21BA" : "Reset" 
+                        text: topRoot.compact ? "\u21BA" : "Reset"
                         font.pixelSize: 13
                         font.pixelSize: 13
                         focusPolicy: Qt.NoFocus
                         focusPolicy: Qt.NoFocus
+                        onClicked: {
+                            if (typeof game !== 'undefined' && game.resetCamera)
+                                game.resetCamera();
+
+                        }
+
                         background: Rectangle {
                         background: Rectangle {
                             color: parent.hovered ? "#34495e" : "#2c3e50"
                             color: parent.hovered ? "#34495e" : "#2c3e50"
                             radius: 6
                             radius: 6
                             border.color: "#1a252f"
                             border.color: "#1a252f"
                             border.width: 1
                             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 {
             RowLayout {
                 id: rightGroup
                 id: rightGroup
+
                 spacing: 12
                 spacing: 12
                 Layout.alignment: Qt.AlignVCenter
                 Layout.alignment: Qt.AlignVCenter
 
 
-                
                 Row {
                 Row {
                     id: statsRow
                     id: statsRow
+
                     spacing: 10
                     spacing: 10
                     Layout.alignment: Qt.AlignVCenter
                     Layout.alignment: Qt.AlignVCenter
 
 
                     Label {
                     Label {
                         id: playerLbl
                         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: {
                         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.pixelSize: 14
                         font.bold: true
                         font.bold: true
@@ -274,16 +372,21 @@ Item {
 
 
                     Label {
                     Label {
                         id: ownersLbl
                         id: ownersLbl
+
                         text: {
                         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++) {
                             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"
                         color: "#ecf0f1"
                         font.pixelSize: 13
                         font.pixelSize: 13
@@ -293,39 +396,48 @@ Item {
                         ToolTip.visible: ma.containsMouse
                         ToolTip.visible: ma.containsMouse
                         ToolTip.delay: 500
                         ToolTip.delay: 500
                         ToolTip.text: {
                         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++) {
                             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 {
                         MouseArea {
                             id: ma
                             id: ma
+
                             anchors.fill: parent
                             anchors.fill: parent
                             hoverEnabled: true
                             hoverEnabled: true
                         }
                         }
+
                     }
                     }
 
 
                     Label {
                     Label {
                         id: enemyLbl
                         id: enemyLbl
+
                         text: "💀 " + (typeof game !== 'undefined' ? game.enemyTroopsDefeated : 0)
                         text: "💀 " + (typeof game !== 'undefined' ? game.enemyTroopsDefeated : 0)
                         color: "#ecf0f1"
                         color: "#ecf0f1"
                         font.pixelSize: 14
                         font.pixelSize: 14
                         elide: Text.ElideRight
                         elide: Text.ElideRight
                         verticalAlignment: Text.AlignVCenter
                         verticalAlignment: Text.AlignVCenter
                     }
                     }
+
                 }
                 }
 
 
-                
                 Item {
                 Item {
                     id: miniWrap
                     id: miniWrap
+
                     visible: !topRoot.ultraCompact
                     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
                     Layout.preferredHeight: topPanel.height - 8
 
 
                     Rectangle {
                     Rectangle {
@@ -341,7 +453,6 @@ Item {
                             radius: 6
                             radius: 6
                             color: "#0a0f14"
                             color: "#0a0f14"
 
 
-                            
                             Label {
                             Label {
                                 anchors.centerIn: parent
                                 anchors.centerIn: parent
                                 text: "MINIMAP"
                                 text: "MINIMAP"
@@ -349,10 +460,17 @@ Item {
                                 font.pixelSize: 12
                                 font.pixelSize: 12
                                 font.bold: true
                                 font.bold: true
                             }
                             }
+
                         }
                         }
+
                     }
                     }
+
                 }
                 }
+
             }
             }
+
         }
         }
+
     }
     }
+
 }
 }

+ 6 - 3
ui/qml/HUDVictory.qml

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

+ 172 - 158
ui/qml/Main.qml

@@ -1,80 +1,77 @@
-
 import QtQuick 2.15
 import QtQuick 2.15
-import QtQuick.Window 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Controls 2.15
+import QtQuick.Window 2.15
 
 
 ApplicationWindow {
 ApplicationWindow {
     id: mainWindow
     id: mainWindow
+
+    property alias gameView: gameViewItem
+    property bool menuVisible: true
+    property bool gameStarted: false
+    property bool gamePaused: false
+    property bool edgeScrollDisabled: false
+
     width: 1280
     width: 1280
     height: 720
     height: 720
-    
     visibility: Window.FullScreen
     visibility: Window.FullScreen
     visible: true
     visible: true
     title: "Standard of Iron - RTS Game"
     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 {
     GameView {
         id: gameViewItem
         id: gameViewItem
+
         anchors.fill: parent
         anchors.fill: parent
         z: 0
         z: 0
         focus: !mainWindow.menuVisible
         focus: !mainWindow.menuVisible
-        visible: gameStarted  
+        visible: gameStarted
     }
     }
 
 
-    
     HUD {
     HUD {
         id: hud
         id: hud
+
         anchors.fill: parent
         anchors.fill: parent
         z: 1
         z: 1
         visible: !mainWindow.menuVisible && gameStarted
         visible: !mainWindow.menuVisible && gameStarted
-        
-        onActiveFocusChanged: if (activeFocus) gameViewItem.forceActiveFocus()
+        onActiveFocusChanged: {
+            if (activeFocus)
+                gameViewItem.forceActiveFocus();
 
 
+        }
         onPauseToggled: {
         onPauseToggled: {
-            mainWindow.gamePaused = !mainWindow.gamePaused
-            gameViewItem.setPaused(mainWindow.gamePaused)
-            gameViewItem.forceActiveFocus()
+            mainWindow.gamePaused = !mainWindow.gamePaused;
+            gameViewItem.setPaused(mainWindow.gamePaused);
+            gameViewItem.forceActiveFocus();
         }
         }
-
         onSpeedChanged: function(speed) {
         onSpeedChanged: function(speed) {
-            gameViewItem.setGameSpeed(speed)
-            gameViewItem.forceActiveFocus()
+            gameViewItem.setGameSpeed(speed);
+            gameViewItem.forceActiveFocus();
         }
         }
-
         onCommandModeChanged: function(mode) {
         onCommandModeChanged: function(mode) {
-            console.log("Main: Command mode changed to:", mode)
+            console.log("Main: Command mode changed to:", mode);
             if (typeof game !== 'undefined') {
             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 {
             } else {
-                console.log("Main: game is undefined")
+                console.log("Main: game is undefined");
             }
             }
-            gameViewItem.forceActiveFocus()
+            gameViewItem.forceActiveFocus();
         }
         }
-
         onRecruit: function(unitType) {
         onRecruit: function(unitType) {
             if (typeof game !== 'undefined' && game.recruitNearSelected)
             if (typeof game !== 'undefined' && game.recruitNearSelected)
-                game.recruitNearSelected(unitType)
-            gameViewItem.forceActiveFocus()
+                game.recruitNearSelected(unitType);
+
+            gameViewItem.forceActiveFocus();
         }
         }
     }
     }
 
 
-    
     Rectangle {
     Rectangle {
         id: pauseOverlay
         id: pauseOverlay
+
         anchors.fill: parent
         anchors.fill: parent
         z: 10
         z: 10
         visible: mainWindow.gamePaused && gameStarted
         visible: mainWindow.gamePaused && gameStarted
-        color: "#80000000"  
-        
+        color: "#80000000"
+
         Rectangle {
         Rectangle {
             anchors.centerIn: parent
             anchors.centerIn: parent
             width: 300
             width: 300
@@ -83,11 +80,11 @@ ApplicationWindow {
             radius: 8
             radius: 8
             border.color: "#34495e"
             border.color: "#34495e"
             border.width: 2
             border.width: 2
-            
+
             Column {
             Column {
                 anchors.centerIn: parent
                 anchors.centerIn: parent
                 spacing: 20
                 spacing: 20
-                
+
                 Text {
                 Text {
                     text: "PAUSED"
                     text: "PAUSED"
                     color: "#ecf0f1"
                     color: "#ecf0f1"
@@ -95,117 +92,118 @@ ApplicationWindow {
                     font.bold: true
                     font.bold: true
                     anchors.horizontalCenter: parent.horizontalCenter
                     anchors.horizontalCenter: parent.horizontalCenter
                 }
                 }
-                
+
                 Text {
                 Text {
                     text: "Press Space to resume"
                     text: "Press Space to resume"
                     color: "#bdc3c7"
                     color: "#bdc3c7"
                     font.pixelSize: 14
                     font.pixelSize: 14
                     anchors.horizontalCenter: parent.horizontalCenter
                     anchors.horizontalCenter: parent.horizontalCenter
                 }
                 }
+
             }
             }
+
         }
         }
+
     }
     }
 
 
-    
     MainMenu {
     MainMenu {
         id: mainMenu
         id: mainMenu
+
         anchors.fill: parent
         anchors.fill: parent
         z: 20
         z: 20
         visible: mainWindow.menuVisible
         visible: mainWindow.menuVisible
-
         Component.onCompleted: {
         Component.onCompleted: {
-            if (mainWindow.menuVisible) mainMenu.forceActiveFocus()
-        }
+            if (mainWindow.menuVisible)
+                mainMenu.forceActiveFocus();
 
 
+        }
         onVisibleChanged: {
         onVisibleChanged: {
             if (visible) {
             if (visible) {
-                mainMenu.forceActiveFocus()
-                gameViewItem.focus = false
+                mainMenu.forceActiveFocus();
+                gameViewItem.focus = false;
             } else if (gameStarted) {
             } else if (gameStarted) {
-                
-                gameViewItem.forceActiveFocus()
+                gameViewItem.forceActiveFocus();
             }
             }
         }
         }
-
         onOpenSkirmish: function() {
         onOpenSkirmish: function() {
-            mapSelect.visible = true
-            mainWindow.menuVisible = false  
+            mapSelect.visible = true;
+            mainWindow.menuVisible = false;
         }
         }
         onOpenSettings: function() {
         onOpenSettings: function() {
-            if (typeof game !== 'undefined' && game.openSettings) game.openSettings()
+            if (typeof game !== 'undefined' && game.openSettings)
+                game.openSettings();
+
         }
         }
         onLoadSave: function() {
         onLoadSave: function() {
-            if (typeof game !== 'undefined' && game.loadSave) game.loadSave()
+            if (typeof game !== 'undefined' && game.loadSave)
+                game.loadSave();
+
         }
         }
         onExitRequested: function() {
         onExitRequested: function() {
-            if (typeof game !== 'undefined' && game.exitGame) game.exitGame()
+            if (typeof game !== 'undefined' && game.exitGame)
+                game.exitGame();
+
         }
         }
     }
     }
 
 
     MapSelect {
     MapSelect {
         id: mapSelect
         id: mapSelect
+
         anchors.fill: parent
         anchors.fill: parent
         z: 21
         z: 21
         visible: false
         visible: false
-
         onVisibleChanged: {
         onVisibleChanged: {
             if (visible) {
             if (visible) {
-                mapSelect.forceActiveFocus()
-                gameViewItem.focus = false
+                mapSelect.forceActiveFocus();
+                gameViewItem.focus = false;
             }
             }
         }
         }
-
         onMapChosen: function(mapPath, playerConfigs) {
         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() {
         onCancelled: function() {
-            mapSelect.visible = false
-            mainWindow.menuVisible = true
+            mapSelect.visible = false;
+            mainWindow.menuVisible = true;
         }
         }
     }
     }
 
 
-    
-    
-    
     Item {
     Item {
         id: edgeScrollOverlay
         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 xPos: -1
         property real yPos: -1
         property real yPos: -1
-        
         property int verticalShift: 6
         property int verticalShift: 6
 
 
-        
         function inHudZone(x, y) {
         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 {
         MouseArea {
             anchors.fill: parent
             anchors.fill: parent
             hoverEnabled: true
             hoverEnabled: true
@@ -213,96 +211,111 @@ ApplicationWindow {
             propagateComposedEvents: true
             propagateComposedEvents: true
             preventStealing: false
             preventStealing: false
             onPositionChanged: function(mouse) {
             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 (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() {
             onEntered: function() {
-                edgeScrollTimer.start()
+                edgeScrollTimer.start();
                 if (typeof game !== 'undefined' && game.setHoverAtScreen) {
                 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() {
             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 {
         Timer {
             id: edgeScrollTimer
             id: edgeScrollTimer
+
             interval: 16
             interval: 16
             repeat: true
             repeat: true
             onTriggered: {
             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 (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 {
     Dialog {
         id: errorDialog
         id: errorDialog
+
         anchors.centerIn: parent
         anchors.centerIn: parent
         width: Math.min(parent.width * 0.6, 500)
         width: Math.min(parent.width * 0.6, 500)
         title: "Error"
         title: "Error"
         modal: true
         modal: true
         standardButtons: Dialog.Ok
         standardButtons: Dialog.Ok
+        onAccepted: {
+            if (game)
+                game.clearError();
+
+        }
 
 
         contentItem: Rectangle {
         contentItem: Rectangle {
             color: "#2a2a2a"
             color: "#2a2a2a"
@@ -310,6 +323,7 @@ ApplicationWindow {
 
 
             Text {
             Text {
                 id: errorText
                 id: errorText
+
                 anchors.centerIn: parent
                 anchors.centerIn: parent
                 width: parent.width - 40
                 width: parent.width - 40
                 text: game ? game.lastError : ""
                 text: game ? game.lastError : ""
@@ -317,19 +331,19 @@ ApplicationWindow {
                 wrapMode: Text.WordWrap
                 wrapMode: Text.WordWrap
                 font.pixelSize: 14
                 font.pixelSize: 14
             }
             }
-        }
 
 
-        onAccepted: {
-            if (game) game.clearError()
         }
         }
+
     }
     }
 
 
     Connections {
     Connections {
-        target: game
         function onLastErrorChanged() {
         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 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Layouts 1.3
 import QtQuick.Layouts 1.3
@@ -7,24 +6,51 @@ import StandardOfIron.UI 1.0
 
 
 Item {
 Item {
     id: root
     id: root
-    anchors.fill: parent
-    z: 10
-    focus: true
 
 
     signal openSkirmish()
     signal openSkirmish()
     signal openSettings()
     signal openSettings()
     signal loadSave()
     signal loadSave()
     signal exitRequested()
     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 {
     Rectangle {
         anchors.fill: parent
         anchors.fill: parent
         color: Theme.dim
         color: Theme.dim
     }
     }
 
 
-    
     Rectangle {
     Rectangle {
         id: container
         id: container
+
+        property int selectedIndex: 0
+
         width: Math.min(parent.width * 0.78, 1100)
         width: Math.min(parent.width * 0.78, 1100)
         height: Math.min(parent.height * 0.78, 700)
         height: Math.min(parent.height * 0.78, 700)
         anchors.centerIn: parent
         anchors.centerIn: parent
@@ -33,26 +59,24 @@ Item {
         border.color: Theme.panelBr
         border.color: Theme.panelBr
         border.width: 1
         border.width: 1
         opacity: 0.98
         opacity: 0.98
-        clip: true                         
-
-        property int selectedIndex: 0
+        clip: true
 
 
         GridLayout {
         GridLayout {
             id: grid
             id: grid
+
             anchors.fill: parent
             anchors.fill: parent
             anchors.margins: Theme.spacingXLarge
             anchors.margins: Theme.spacingXLarge
             rowSpacing: Theme.spacingMedium
             rowSpacing: Theme.spacingMedium
             columnSpacing: 18
             columnSpacing: 18
             columns: parent.width > 900 ? 2 : 1
             columns: parent.width > 900 ? 2 : 1
 
 
-            
             ColumnLayout {
             ColumnLayout {
                 Layout.preferredWidth: parent.width > 900 ? parent.width * 0.45 : parent.width
                 Layout.preferredWidth: parent.width > 900 ? parent.width * 0.45 : parent.width
                 spacing: Theme.spacingLarge
                 spacing: Theme.spacingLarge
 
 
-                
                 ColumnLayout {
                 ColumnLayout {
                     spacing: Theme.spacingSmall
                     spacing: Theme.spacingSmall
+
                     Label {
                     Label {
                         text: "STANDARD OF IRON"
                         text: "STANDARD OF IRON"
                         color: Theme.textMain
                         color: Theme.textMain
@@ -62,6 +86,7 @@ Item {
                         Layout.fillWidth: true
                         Layout.fillWidth: true
                         elide: Label.ElideRight
                         elide: Label.ElideRight
                     }
                     }
+
                     Label {
                     Label {
                         text: "A tiny but ambitious RTS"
                         text: "A tiny but ambitious RTS"
                         color: Theme.textSub
                         color: Theme.textSub
@@ -70,23 +95,46 @@ Item {
                         Layout.fillWidth: true
                         Layout.fillWidth: true
                         elide: Label.ElideRight
                         elide: Label.ElideRight
                     }
                     }
+
                 }
                 }
 
 
-                
                 ListModel {
                 ListModel {
                     id: menuModel
                     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 {
                 Repeater {
                     model: menuModel
                     model: menuModel
+
                     delegate: Item {
                     delegate: Item {
                         id: menuItem
                         id: menuItem
+
                         property int idx: index
                         property int idx: index
+
                         Layout.fillWidth: true
                         Layout.fillWidth: true
                         Layout.preferredHeight: container.width > 900 ? 64 : 56
                         Layout.preferredHeight: container.width > 900 ? 64 : 56
 
 
@@ -94,25 +142,24 @@ Item {
                             anchors.fill: parent
                             anchors.fill: parent
                             radius: Theme.radiusLarge
                             radius: Theme.radiusLarge
                             clip: true
                             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.width: 1
                             border.color: container.selectedIndex === idx ? Theme.selectedBr : Theme.cardBorder
                             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 {
                             RowLayout {
                                 anchors.fill: parent
                                 anchors.fill: parent
                                 anchors.margins: Theme.spacingSmall
                                 anchors.margins: Theme.spacingSmall
                                 spacing: Theme.spacingMedium
                                 spacing: Theme.spacingMedium
 
 
-                                
-                                Item { Layout.fillWidth: true; Layout.preferredWidth: 1 }
+                                Item {
+                                    Layout.fillWidth: true
+                                    Layout.preferredWidth: 1
+                                }
 
 
-                                
                                 ColumnLayout {
                                 ColumnLayout {
                                     Layout.fillWidth: true
                                     Layout.fillWidth: true
                                     spacing: Theme.spacingTiny
                                     spacing: Theme.spacingTiny
+
                                     Text {
                                     Text {
                                         text: model.title
                                         text: model.title
                                         Layout.fillWidth: true
                                         Layout.fillWidth: true
@@ -121,6 +168,7 @@ Item {
                                         font.pointSize: Theme.fontSizeLarge
                                         font.pointSize: Theme.fontSizeLarge
                                         font.bold: container.selectedIndex === idx
                                         font.bold: container.selectedIndex === idx
                                     }
                                     }
+
                                     Text {
                                     Text {
                                         text: model.subtitle
                                         text: model.subtitle
                                         Layout.fillWidth: true
                                         Layout.fillWidth: true
@@ -128,53 +176,85 @@ Item {
                                         color: container.selectedIndex === idx ? Theme.accentBright : Theme.textSubLite
                                         color: container.selectedIndex === idx ? Theme.accentBright : Theme.textSubLite
                                         font.pointSize: Theme.fontSizeSmall
                                         font.pointSize: Theme.fontSizeSmall
                                     }
                                     }
+
                                 }
                                 }
 
 
-                                
                                 Text {
                                 Text {
                                     text: "›"
                                     text: "›"
                                     font.pointSize: Theme.fontSizeTitle
                                     font.pointSize: Theme.fontSizeTitle
                                     color: container.selectedIndex === idx ? Theme.textMain : Theme.textHint
                                     color: container.selectedIndex === idx ? Theme.textMain : Theme.textHint
                                 }
                                 }
+
                             }
                             }
+
+                            Behavior on color {
+                                ColorAnimation {
+                                    duration: Theme.animNormal
+                                }
+
+                            }
+
+                            Behavior on border.color {
+                                ColorAnimation {
+                                    duration: Theme.animNormal
+                                }
+
+                            }
+
                         }
                         }
 
 
-                        
                         MouseArea {
                         MouseArea {
                             id: menuItemMouse
                             id: menuItemMouse
+
                             anchors.fill: parent
                             anchors.fill: parent
                             hoverEnabled: true
                             hoverEnabled: true
                             acceptedButtons: Qt.LeftButton
                             acceptedButtons: Qt.LeftButton
                             cursorShape: Qt.PointingHandCursor
                             cursorShape: Qt.PointingHandCursor
                             onEntered: container.selectedIndex = idx
                             onEntered: container.selectedIndex = idx
                             onClicked: {
                             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 {
                 RowLayout {
                     spacing: Theme.spacingSmall
                     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 {
                     Label {
                         text: Qt.formatDateTime(new Date(), "yyyy-MM-dd")
                         text: Qt.formatDateTime(new Date(), "yyyy-MM-dd")
                         color: Theme.textHint
                         color: Theme.textHint
                         font.pointSize: Theme.fontSizeSmall
                         font.pointSize: Theme.fontSizeSmall
                         elide: Label.ElideRight
                         elide: Label.ElideRight
                     }
                     }
+
                 }
                 }
+
             }
             }
 
 
-            
             Rectangle {
             Rectangle {
                 color: Qt.rgba(0, 0, 0, 0)
                 color: Qt.rgba(0, 0, 0, 0)
                 radius: Theme.radiusMedium
                 radius: Theme.radiusMedium
@@ -185,9 +265,9 @@ Item {
                     anchors.margins: Theme.spacingSmall
                     anchors.margins: Theme.spacingSmall
                     spacing: Theme.spacingMedium
                     spacing: Theme.spacingMedium
 
 
-                    
                     Rectangle {
                     Rectangle {
                         id: promo
                         id: promo
+
                         color: Theme.cardBase
                         color: Theme.cardBase
                         radius: Theme.radiusLarge
                         radius: Theme.radiusLarge
                         border.color: Theme.border
                         border.color: Theme.border
@@ -199,6 +279,7 @@ Item {
                             anchors.fill: parent
                             anchors.fill: parent
                             anchors.margins: Theme.spacingMedium
                             anchors.margins: Theme.spacingMedium
                             spacing: Theme.spacingSmall
                             spacing: Theme.spacingSmall
+
                             Label {
                             Label {
                                 text: "Featured"
                                 text: "Featured"
                                 color: Theme.accent
                                 color: Theme.accent
@@ -206,6 +287,7 @@ Item {
                                 Layout.fillWidth: true
                                 Layout.fillWidth: true
                                 elide: Label.ElideRight
                                 elide: Label.ElideRight
                             }
                             }
+
                             Label {
                             Label {
                                 text: "Skirmish Mode"
                                 text: "Skirmish Mode"
                                 color: Theme.textMain
                                 color: Theme.textMain
@@ -214,18 +296,20 @@ Item {
                                 Layout.fillWidth: true
                                 Layout.fillWidth: true
                                 elide: Label.ElideRight
                                 elide: Label.ElideRight
                             }
                             }
+
                             Text {
                             Text {
                                 text: "Pick a map, adjust your forces and jump into battle. Modern controls and responsive UI."
                                 text: "Pick a map, adjust your forces and jump into battle. Modern controls and responsive UI."
                                 color: Theme.textSubLite
                                 color: Theme.textSubLite
                                 wrapMode: Text.WordWrap
                                 wrapMode: Text.WordWrap
-                                maximumLineCount: 3           
+                                maximumLineCount: 3
                                 elide: Text.ElideRight
                                 elide: Text.ElideRight
                                 Layout.fillWidth: true
                                 Layout.fillWidth: true
                             }
                             }
+
                         }
                         }
+
                     }
                     }
 
 
-                    
                     Rectangle {
                     Rectangle {
                         color: Theme.cardBase
                         color: Theme.cardBase
                         radius: Theme.radiusLarge
                         radius: Theme.radiusLarge
@@ -238,6 +322,7 @@ Item {
                             anchors.fill: parent
                             anchors.fill: parent
                             anchors.margins: Theme.spacingSmall
                             anchors.margins: Theme.spacingSmall
                             spacing: Theme.spacingSmall
                             spacing: Theme.spacingSmall
+
                             Label {
                             Label {
                                 text: "Tips"
                                 text: "Tips"
                                 color: Theme.accent
                                 color: Theme.accent
@@ -245,6 +330,7 @@ Item {
                                 Layout.fillWidth: true
                                 Layout.fillWidth: true
                                 elide: Label.ElideRight
                                 elide: Label.ElideRight
                             }
                             }
+
                             Text {
                             Text {
                                 text: "Hover menu items or use Up/Down and Enter to navigate. Play opens map selection."
                                 text: "Hover menu items or use Up/Down and Enter to navigate. Play opens map selection."
                                 color: Theme.textSubLite
                                 color: Theme.textSubLite
@@ -253,35 +339,17 @@ Item {
                                 elide: Text.ElideRight
                                 elide: Text.ElideRight
                                 Layout.fillWidth: true
                                 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 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Controls 2.15
 
 
 Item {
 Item {
     id: root
     id: root
-    anchors.fill: parent
 
 
     property var mapsModel: []
     property var mapsModel: []
     property int currentIndex: 0
     property int currentIndex: 0
-    property var colors: ({})
+    property var colors: ({
+    })
 
 
     signal mapSelected(int index)
     signal mapSelected(int index)
     signal mapDoubleClicked()
     signal mapDoubleClicked()
 
 
     function field(obj, key) {
     function field(obj, key) {
-        return (obj && obj[key] !== undefined) ? String(obj[key]) : ""
+        return (obj && obj[key] !== undefined) ? String(obj[key]) : "";
     }
     }
 
 
-    
+    anchors.fill: parent
+
     Text {
     Text {
         id: title
         id: title
+
         text: "Maps"
         text: "Maps"
         color: colors.textMain
         color: colors.textMain
         font.pixelSize: 18
         font.pixelSize: 18
         font.bold: true
         font.bold: true
+
         anchors {
         anchors {
             top: parent.top
             top: parent.top
             left: parent.left
             left: parent.left
             right: parent.right
             right: parent.right
         }
         }
+
     }
     }
 
 
     Text {
     Text {
         id: countLabel
         id: countLabel
+
         text: "(" + (list.count || 0) + ")"
         text: "(" + (list.count || 0) + ")"
         color: colors.textSubLite
         color: colors.textSubLite
         font.pixelSize: 12
         font.pixelSize: 12
+
         anchors {
         anchors {
             left: title.right
             left: title.right
             leftMargin: 8
             leftMargin: 8
             verticalCenter: title.verticalCenter
             verticalCenter: title.verticalCenter
         }
         }
+
     }
     }
 
 
-    
     Rectangle {
     Rectangle {
         id: listFrame
         id: listFrame
+
+        color: "transparent"
+        radius: 10
+        border.color: colors.panelBr
+        border.width: 1
+        clip: true
+
         anchors {
         anchors {
             top: title.bottom
             top: title.bottom
             topMargin: 12
             topMargin: 12
@@ -53,14 +65,10 @@ Item {
             right: parent.right
             right: parent.right
             bottom: parent.bottom
             bottom: parent.bottom
         }
         }
-        color: "transparent"
-        radius: 10
-        border.color: colors.panelBr
-        border.width: 1
-        clip: true
 
 
         ListView {
         ListView {
             id: list
             id: list
+
             anchors.fill: parent
             anchors.fill: parent
             anchors.margins: 8
             anchors.margins: 8
             model: root.mapsModel
             model: root.mapsModel
@@ -69,15 +77,31 @@ Item {
             currentIndex: root.currentIndex
             currentIndex: root.currentIndex
             keyNavigationWraps: false
             keyNavigationWraps: false
             boundsBehavior: Flickable.StopAtBounds
             boundsBehavior: Flickable.StopAtBounds
-
             onCurrentIndexChanged: {
             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 {
             highlight: Rectangle {
                 color: "transparent"
                 color: "transparent"
@@ -85,8 +109,6 @@ Item {
                 border.color: colors.selectedBr
                 border.color: colors.selectedBr
                 border.width: 1
                 border.width: 1
             }
             }
-            highlightMoveDuration: 120
-            highlightFollowsCurrentItem: true
 
 
             delegate: Item {
             delegate: Item {
                 width: list.width
                 width: list.width
@@ -94,6 +116,7 @@ Item {
 
 
                 MouseArea {
                 MouseArea {
                     id: rowMouse
                     id: rowMouse
+
                     anchors.fill: parent
                     anchors.fill: parent
                     hoverEnabled: true
                     hoverEnabled: true
                     acceptedButtons: Qt.LeftButton
                     acceptedButtons: Qt.LeftButton
@@ -106,29 +129,26 @@ Item {
                     anchors.fill: parent
                     anchors.fill: parent
                     radius: 8
                     radius: 8
                     clip: true
                     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.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 {
                     Rectangle {
                         id: thumbWrap
                         id: thumbWrap
+
                         width: 60
                         width: 60
                         height: 42
                         height: 42
                         radius: 6
                         radius: 6
                         color: "#031314"
                         color: "#031314"
                         border.color: colors.thumbBr
                         border.color: colors.thumbBr
                         border.width: 1
                         border.width: 1
+                        clip: true
+
                         anchors {
                         anchors {
                             left: parent.left
                             left: parent.left
                             leftMargin: 10
                             leftMargin: 10
                             verticalCenter: parent.verticalCenter
                             verticalCenter: parent.verticalCenter
                         }
                         }
-                        clip: true
 
 
                         Image {
                         Image {
                             anchors.fill: parent
                             anchors.fill: parent
@@ -137,9 +157,12 @@ Item {
                             fillMode: Image.PreserveAspectCrop
                             fillMode: Image.PreserveAspectCrop
                             visible: status === Image.Ready
                             visible: status === Image.Ready
                         }
                         }
+
                     }
                     }
 
 
                     Column {
                     Column {
+                        spacing: 4
+
                         anchors {
                         anchors {
                             left: thumbWrap.right
                             left: thumbWrap.right
                             leftMargin: 10
                             leftMargin: 10
@@ -147,7 +170,6 @@ Item {
                             rightMargin: 10
                             rightMargin: 10
                             verticalCenter: parent.verticalCenter
                             verticalCenter: parent.verticalCenter
                         }
                         }
-                        spacing: 4
 
 
                         Text {
                         Text {
                             text: (typeof name !== "undefined") ? String(name) : ""
                             text: (typeof name !== "undefined") ? String(name) : ""
@@ -165,21 +187,29 @@ Item {
                             elide: Text.ElideRight
                             elide: Text.ElideRight
                             width: parent.width
                             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
                 }
                 }
+
             }
             }
+
         }
         }
+
     }
     }
+
 }
 }

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 399 - 248
ui/qml/MapSelect.qml


+ 124 - 70
ui/qml/PlayerConfigPanel.qml

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

+ 30 - 18
ui/qml/PlayerListItem.qml

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

+ 106 - 150
ui/qml/StyleGuide.qml

@@ -1,170 +1,126 @@
-
-pragma Singleton
 import QtQuick 2.15
 import QtQuick 2.15
+pragma Singleton
 
 
 QtObject {
 QtObject {
     id: root
     id: root
 
 
-    
     readonly property var palette: ({
     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: ({
     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: ({
     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: ({
     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: ({
     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 2.15
 import QtQuick.Controls 2.15
 import QtQuick.Controls 2.15
 
 
 Button {
 Button {
     id: control
     id: control
-    
-    property string buttonStyle: "primary" 
+
+    property string buttonStyle: "primary"
     property var styleConfig: {
     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
     implicitHeight: styleConfig.height
     implicitWidth: styleConfig.minWidth
     implicitWidth: styleConfig.minWidth
     hoverEnabled: true
     hoverEnabled: true
-    
+    ToolTip.visible: control.ToolTip.text !== "" && hoverArea.containsMouse
+    ToolTip.delay: 500
+
     MouseArea {
     MouseArea {
         id: hoverArea
         id: hoverArea
+
         anchors.fill: parent
         anchors.fill: parent
         hoverEnabled: true
         hoverEnabled: true
         acceptedButtons: Qt.NoButton
         acceptedButtons: Qt.NoButton
         cursorShape: control.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
         cursorShape: control.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
     }
     }
-    
+
     contentItem: Text {
     contentItem: Text {
         text: control.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
         font.bold: true
         color: {
         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
         horizontalAlignment: Text.AlignHCenter
         verticalAlignment: Text.AlignVCenter
         verticalAlignment: Text.AlignVCenter
         elide: Text.ElideRight
         elide: Text.ElideRight
-        
-        Behavior on font.pointSize { 
-            NumberAnimation { duration: StyleGuide.animation.fast } 
+
+        Behavior on font.pointSize {
+            NumberAnimation {
+                duration: StyleGuide.animation.fast
+            }
+
         }
         }
+
         Behavior on color {
         Behavior on color {
-            ColorAnimation { duration: StyleGuide.animation.normal }
+            ColorAnimation {
+                duration: StyleGuide.animation.normal
+            }
+
         }
         }
+
     }
     }
-    
+
     background: Rectangle {
     background: Rectangle {
         implicitWidth: styleConfig.minWidth
         implicitWidth: styleConfig.minWidth
         implicitHeight: styleConfig.height
         implicitHeight: styleConfig.height
         radius: styleConfig.radius
         radius: styleConfig.radius
         color: {
         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.width: 1
         border.color: {
         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
+
 }
 }

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio