game_engine.cpp 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. #include "game_engine.h"
  2. #include <QCoreApplication>
  3. #include <QCursor>
  4. #include <QDebug>
  5. #include <QOpenGLContext>
  6. #include <QQuickWindow>
  7. #include <QVariant>
  8. #include "game/core/component.h"
  9. #include "game/core/world.h"
  10. #include "game/map/level_loader.h"
  11. #include "game/map/terrain_service.h"
  12. #include "game/map/visibility_service.h"
  13. #include "game/systems/ai_system.h"
  14. #include "game/systems/arrow_system.h"
  15. #include "game/systems/building_collision_registry.h"
  16. #include "game/systems/camera_controller.h"
  17. #include "game/systems/camera_follow_system.h"
  18. #include "game/systems/combat_system.h"
  19. #include "game/systems/command_service.h"
  20. #include "game/systems/formation_planner.h"
  21. #include "game/systems/movement_system.h"
  22. #include "game/systems/patrol_system.h"
  23. #include "game/systems/picking_service.h"
  24. #include "game/systems/production_service.h"
  25. #include "game/systems/production_system.h"
  26. #include "game/systems/selection_system.h"
  27. #include "game/systems/terrain_alignment_system.h"
  28. #include "render/geom/arrow.h"
  29. #include "game/core/event_manager.h"
  30. #include "render/geom/patrol_flags.h"
  31. #include "render/gl/bootstrap.h"
  32. #include "render/gl/camera.h"
  33. #include "render/gl/resources.h"
  34. #include "render/ground/fog_renderer.h"
  35. #include "render/ground/ground_renderer.h"
  36. #include "render/ground/terrain_renderer.h"
  37. #include "render/scene_renderer.h"
  38. #include "selected_units_model.h"
  39. #include <QDir>
  40. #include <QFile>
  41. #include <QJsonDocument>
  42. #include <QJsonObject>
  43. #include <cmath>
  44. #include <limits>
  45. GameEngine::GameEngine() {
  46. m_world = std::make_unique<Engine::Core::World>();
  47. m_renderer = std::make_unique<Render::GL::Renderer>();
  48. m_camera = std::make_unique<Render::GL::Camera>();
  49. m_ground = std::make_unique<Render::GL::GroundRenderer>();
  50. m_terrain = std::make_unique<Render::GL::TerrainRenderer>();
  51. m_fog = std::make_unique<Render::GL::FogRenderer>();
  52. std::unique_ptr<Engine::Core::System> arrowSys =
  53. std::make_unique<Game::Systems::ArrowSystem>();
  54. m_arrowSystem = static_cast<Game::Systems::ArrowSystem *>(arrowSys.get());
  55. m_world->addSystem(std::move(arrowSys));
  56. m_world->addSystem(std::make_unique<Game::Systems::MovementSystem>());
  57. m_world->addSystem(std::make_unique<Game::Systems::PatrolSystem>());
  58. m_world->addSystem(std::make_unique<Game::Systems::CombatSystem>());
  59. m_world->addSystem(std::make_unique<Game::Systems::AISystem>());
  60. m_world->addSystem(std::make_unique<Game::Systems::ProductionSystem>());
  61. m_world->addSystem(std::make_unique<Game::Systems::TerrainAlignmentSystem>());
  62. {
  63. std::unique_ptr<Engine::Core::System> selSys =
  64. std::make_unique<Game::Systems::SelectionSystem>();
  65. m_selectionSystem =
  66. dynamic_cast<Game::Systems::SelectionSystem *>(selSys.get());
  67. m_world->addSystem(std::move(selSys));
  68. }
  69. m_selectedUnitsModel = new SelectedUnitsModel(this, this);
  70. QMetaObject::invokeMethod(m_selectedUnitsModel, "refresh");
  71. m_pickingService = std::make_unique<Game::Systems::PickingService>();
  72. // subscribe to unit died events to track enemy defeats
  73. Engine::Core::EventManager::instance().subscribe<Engine::Core::UnitDiedEvent>(
  74. [this](const Engine::Core::UnitDiedEvent &e) {
  75. // increment only if the unit belonged to an enemy
  76. if (e.ownerId != m_runtime.localOwnerId) {
  77. m_enemyTroopsDefeated++;
  78. emit enemyTroopsDefeatedChanged();
  79. }
  80. });
  81. }
  82. GameEngine::~GameEngine() = default;
  83. void GameEngine::onMapClicked(qreal sx, qreal sy) {
  84. if (!m_window)
  85. return;
  86. ensureInitialized();
  87. QVector3D hit;
  88. if (!screenToGround(QPointF(sx, sy), hit))
  89. return;
  90. onClickSelect(sx, sy, false);
  91. }
  92. void GameEngine::onRightClick(qreal sx, qreal sy) {
  93. if (!m_window)
  94. return;
  95. ensureInitialized();
  96. if (!m_selectionSystem)
  97. return;
  98. const auto &sel = m_selectionSystem->getSelectedUnits();
  99. if (!sel.empty()) {
  100. m_selectionSystem->clearSelection();
  101. syncSelectionFlags();
  102. emit selectedUnitsChanged();
  103. if (m_selectedUnitsModel)
  104. QMetaObject::invokeMethod(m_selectedUnitsModel, "refresh");
  105. setCursorMode("normal");
  106. return;
  107. }
  108. }
  109. void GameEngine::onAttackClick(qreal sx, qreal sy) {
  110. if (!m_window)
  111. return;
  112. ensureInitialized();
  113. if (!m_selectionSystem || !m_pickingService || !m_camera || !m_world)
  114. return;
  115. (void)sx;
  116. (void)sy;
  117. const auto &selected = m_selectionSystem->getSelectedUnits();
  118. if (selected.empty()) {
  119. setCursorMode("normal");
  120. return;
  121. }
  122. Engine::Core::EntityID targetId =
  123. m_pickingService->pickUnitFirst(float(sx), float(sy), *m_world, *m_camera,
  124. m_viewport.width, m_viewport.height, 0);
  125. if (targetId == 0) {
  126. setCursorMode("normal");
  127. return;
  128. }
  129. auto *targetEntity = m_world->getEntity(targetId);
  130. if (!targetEntity) {
  131. return;
  132. }
  133. auto *targetUnit = targetEntity->getComponent<Engine::Core::UnitComponent>();
  134. if (!targetUnit) {
  135. (void)targetId;
  136. return;
  137. }
  138. if (targetUnit->ownerId == m_runtime.localOwnerId) {
  139. return;
  140. }
  141. Game::Systems::CommandService::attackTarget(*m_world, selected, targetId,
  142. true);
  143. if (m_arrowSystem) {
  144. auto *targetTrans =
  145. targetEntity->getComponent<Engine::Core::TransformComponent>();
  146. if (targetTrans) {
  147. QVector3D targetPos(targetTrans->position.x,
  148. targetTrans->position.y + 1.0f,
  149. targetTrans->position.z);
  150. QVector3D aboveTarget = targetPos + QVector3D(0, 2.0f, 0);
  151. m_arrowSystem->spawnArrow(aboveTarget, targetPos,
  152. QVector3D(1.0f, 0.2f, 0.2f), 6.0f);
  153. }
  154. }
  155. setCursorMode("normal");
  156. }
  157. void GameEngine::onStopCommand() {
  158. if (!m_selectionSystem || !m_world)
  159. return;
  160. ensureInitialized();
  161. const auto &selected = m_selectionSystem->getSelectedUnits();
  162. if (selected.empty())
  163. return;
  164. for (auto id : selected) {
  165. auto *entity = m_world->getEntity(id);
  166. if (!entity)
  167. continue;
  168. if (auto *movement =
  169. entity->getComponent<Engine::Core::MovementComponent>()) {
  170. auto *transform =
  171. entity->getComponent<Engine::Core::TransformComponent>();
  172. movement->hasTarget = false;
  173. movement->path.clear();
  174. movement->pathPending = false;
  175. movement->pendingRequestId = 0;
  176. movement->repathCooldown = 0.0f;
  177. if (transform) {
  178. movement->targetX = transform->position.x;
  179. movement->targetY = transform->position.z;
  180. movement->goalX = transform->position.x;
  181. movement->goalY = transform->position.z;
  182. } else {
  183. movement->targetX = 0.0f;
  184. movement->targetY = 0.0f;
  185. movement->goalX = 0.0f;
  186. movement->goalY = 0.0f;
  187. }
  188. }
  189. if (auto *attack =
  190. entity->getComponent<Engine::Core::AttackTargetComponent>()) {
  191. attack->targetId = 0;
  192. }
  193. if (auto *patrol = entity->getComponent<Engine::Core::PatrolComponent>()) {
  194. patrol->patrolling = false;
  195. patrol->waypoints.clear();
  196. }
  197. }
  198. setCursorMode("normal");
  199. }
  200. void GameEngine::onPatrolClick(qreal sx, qreal sy) {
  201. if (!m_selectionSystem || !m_world)
  202. return;
  203. ensureInitialized();
  204. const auto &selected = m_selectionSystem->getSelectedUnits();
  205. if (selected.empty())
  206. return;
  207. QVector3D hit;
  208. if (!screenToGround(QPointF(sx, sy), hit))
  209. return;
  210. if (!m_patrol.hasFirstWaypoint) {
  211. m_patrol.firstWaypoint = hit;
  212. m_patrol.hasFirstWaypoint = true;
  213. return;
  214. }
  215. QVector3D secondWaypoint = hit;
  216. for (auto id : selected) {
  217. auto *entity = m_world->getEntity(id);
  218. if (!entity)
  219. continue;
  220. auto *building = entity->getComponent<Engine::Core::BuildingComponent>();
  221. if (building)
  222. continue;
  223. auto *patrol = entity->getComponent<Engine::Core::PatrolComponent>();
  224. if (!patrol) {
  225. patrol = entity->addComponent<Engine::Core::PatrolComponent>();
  226. }
  227. if (patrol) {
  228. patrol->waypoints.clear();
  229. patrol->waypoints.push_back(
  230. {m_patrol.firstWaypoint.x(), m_patrol.firstWaypoint.z()});
  231. patrol->waypoints.push_back({secondWaypoint.x(), secondWaypoint.z()});
  232. patrol->currentWaypoint = 0;
  233. patrol->patrolling = true;
  234. }
  235. if (auto *movement =
  236. entity->getComponent<Engine::Core::MovementComponent>()) {
  237. movement->hasTarget = false;
  238. movement->path.clear();
  239. movement->pathPending = false;
  240. movement->pendingRequestId = 0;
  241. movement->repathCooldown = 0.0f;
  242. if (auto *transform =
  243. entity->getComponent<Engine::Core::TransformComponent>()) {
  244. movement->targetX = transform->position.x;
  245. movement->targetY = transform->position.z;
  246. movement->goalX = transform->position.x;
  247. movement->goalY = transform->position.z;
  248. }
  249. }
  250. if (auto *attack =
  251. entity->getComponent<Engine::Core::AttackTargetComponent>()) {
  252. attack->targetId = 0;
  253. }
  254. }
  255. m_patrol.hasFirstWaypoint = false;
  256. setCursorMode("normal");
  257. }
  258. void GameEngine::setCursorMode(const QString &mode) {
  259. if (m_runtime.cursorMode == mode)
  260. return;
  261. if (m_runtime.cursorMode == "patrol" && mode != "patrol") {
  262. m_patrol.hasFirstWaypoint = false;
  263. }
  264. m_runtime.cursorMode = mode;
  265. if (m_window) {
  266. if (mode == "normal") {
  267. m_window->setCursor(Qt::ArrowCursor);
  268. } else {
  269. m_window->setCursor(Qt::BlankCursor);
  270. }
  271. }
  272. emit cursorModeChanged();
  273. emit globalCursorChanged();
  274. }
  275. qreal GameEngine::globalCursorX() const {
  276. if (!m_window)
  277. return 0;
  278. QPoint globalPos = QCursor::pos();
  279. QPoint localPos = m_window->mapFromGlobal(globalPos);
  280. return localPos.x();
  281. }
  282. qreal GameEngine::globalCursorY() const {
  283. if (!m_window)
  284. return 0;
  285. QPoint globalPos = QCursor::pos();
  286. QPoint localPos = m_window->mapFromGlobal(globalPos);
  287. return localPos.y();
  288. }
  289. void GameEngine::setHoverAtScreen(qreal sx, qreal sy) {
  290. if (!m_window)
  291. return;
  292. ensureInitialized();
  293. if (!m_pickingService || !m_camera || !m_world)
  294. return;
  295. if (sx < 0 || sy < 0) {
  296. if (m_runtime.cursorMode != "normal") {
  297. m_window->setCursor(Qt::ArrowCursor);
  298. }
  299. m_hover.entityId = 0;
  300. return;
  301. }
  302. if (m_runtime.cursorMode == "normal") {
  303. m_window->setCursor(Qt::ArrowCursor);
  304. } else {
  305. m_window->setCursor(Qt::BlankCursor);
  306. }
  307. m_hover.entityId =
  308. m_pickingService->updateHover(float(sx), float(sy), *m_world, *m_camera,
  309. m_viewport.width, m_viewport.height);
  310. }
  311. void GameEngine::onClickSelect(qreal sx, qreal sy, bool additive) {
  312. if (!m_window || !m_selectionSystem)
  313. return;
  314. ensureInitialized();
  315. if (!m_pickingService || !m_camera || !m_world)
  316. return;
  317. Engine::Core::EntityID picked = m_pickingService->pickSingle(
  318. float(sx), float(sy), *m_world, *m_camera, m_viewport.width,
  319. m_viewport.height, m_runtime.localOwnerId, true);
  320. if (picked) {
  321. if (!additive)
  322. m_selectionSystem->clearSelection();
  323. m_selectionSystem->selectUnit(picked);
  324. syncSelectionFlags();
  325. emit selectedUnitsChanged();
  326. if (m_selectedUnitsModel)
  327. QMetaObject::invokeMethod(m_selectedUnitsModel, "refresh");
  328. return;
  329. }
  330. const auto &selected = m_selectionSystem->getSelectedUnits();
  331. if (!selected.empty()) {
  332. QVector3D hit;
  333. if (!screenToGround(QPointF(sx, sy), hit)) {
  334. return;
  335. }
  336. auto targets = Game::Systems::FormationPlanner::spreadFormation(
  337. int(selected.size()), hit, 1.0f);
  338. Game::Systems::CommandService::moveUnits(*m_world, selected, targets);
  339. syncSelectionFlags();
  340. return;
  341. }
  342. }
  343. void GameEngine::onAreaSelected(qreal x1, qreal y1, qreal x2, qreal y2,
  344. bool additive) {
  345. if (!m_window || !m_selectionSystem)
  346. return;
  347. ensureInitialized();
  348. if (!additive)
  349. m_selectionSystem->clearSelection();
  350. if (!m_pickingService || !m_camera || !m_world)
  351. return;
  352. auto picked = m_pickingService->pickInRect(
  353. float(x1), float(y1), float(x2), float(y2), *m_world, *m_camera,
  354. m_viewport.width, m_viewport.height, m_runtime.localOwnerId);
  355. for (auto id : picked)
  356. m_selectionSystem->selectUnit(id);
  357. syncSelectionFlags();
  358. emit selectedUnitsChanged();
  359. if (m_selectedUnitsModel)
  360. QMetaObject::invokeMethod(m_selectedUnitsModel, "refresh");
  361. }
  362. void GameEngine::initialize() {
  363. if (!Render::GL::RenderBootstrap::initialize(*m_renderer, *m_camera)) {
  364. return;
  365. }
  366. if (m_ground) {
  367. m_ground->configureExtent(50.0f);
  368. }
  369. m_runtime.initialized = true;
  370. }
  371. void GameEngine::ensureInitialized() {
  372. if (!m_runtime.initialized)
  373. initialize();
  374. }
  375. int GameEngine::enemyTroopsDefeated() const { return m_enemyTroopsDefeated; }
  376. void GameEngine::update(float dt) {
  377. if (m_runtime.loading) {
  378. return;
  379. }
  380. if (m_runtime.paused) {
  381. dt = 0.0f;
  382. } else {
  383. dt *= m_runtime.timeScale;
  384. }
  385. if (m_renderer) {
  386. m_renderer->updateAnimationTime(dt);
  387. }
  388. if (m_camera) {
  389. m_camera->update(dt);
  390. }
  391. if (m_world) {
  392. m_world->update(dt);
  393. auto &visibilityService = Game::Map::VisibilityService::instance();
  394. if (visibilityService.isInitialized()) {
  395. visibilityService.update(*m_world, m_runtime.localOwnerId);
  396. const auto newVersion = visibilityService.version();
  397. if (newVersion != m_runtime.visibilityVersion) {
  398. if (m_fog) {
  399. m_fog->updateMask(
  400. visibilityService.getWidth(), visibilityService.getHeight(),
  401. visibilityService.getTileSize(), visibilityService.cells());
  402. }
  403. m_runtime.visibilityVersion = newVersion;
  404. }
  405. }
  406. }
  407. syncSelectionFlags();
  408. checkVictoryCondition();
  409. int currentTroopCount = playerTroopCount();
  410. if (currentTroopCount != m_runtime.lastTroopCount) {
  411. m_runtime.lastTroopCount = currentTroopCount;
  412. emit troopCountChanged();
  413. }
  414. if (m_followSelectionEnabled && m_camera && m_selectionSystem && m_world) {
  415. Game::Systems::CameraFollowSystem cfs;
  416. cfs.update(*m_world, *m_selectionSystem, *m_camera);
  417. }
  418. if (m_selectedUnitsModel)
  419. QMetaObject::invokeMethod(m_selectedUnitsModel, "refresh",
  420. Qt::QueuedConnection);
  421. }
  422. void GameEngine::render(int pixelWidth, int pixelHeight) {
  423. if (!m_renderer || !m_world || !m_runtime.initialized || m_runtime.loading)
  424. return;
  425. if (pixelWidth > 0 && pixelHeight > 0) {
  426. m_viewport.width = pixelWidth;
  427. m_viewport.height = pixelHeight;
  428. m_renderer->setViewport(pixelWidth, pixelHeight);
  429. }
  430. if (m_selectionSystem) {
  431. const auto &sel = m_selectionSystem->getSelectedUnits();
  432. std::vector<unsigned int> ids(sel.begin(), sel.end());
  433. m_renderer->setSelectedEntities(ids);
  434. }
  435. m_renderer->beginFrame();
  436. if (m_ground && m_renderer) {
  437. if (auto *res = m_renderer->resources())
  438. m_ground->submit(*m_renderer, *res);
  439. }
  440. if (m_terrain && m_renderer) {
  441. if (auto *res = m_renderer->resources())
  442. m_terrain->submit(*m_renderer, *res);
  443. }
  444. if (m_fog && m_renderer) {
  445. if (auto *res = m_renderer->resources())
  446. m_fog->submit(*m_renderer, *res);
  447. }
  448. if (m_renderer)
  449. m_renderer->setHoveredEntityId(m_hover.entityId);
  450. if (m_renderer)
  451. m_renderer->setLocalOwnerId(m_runtime.localOwnerId);
  452. m_renderer->renderWorld(m_world.get());
  453. if (m_arrowSystem) {
  454. if (auto *res = m_renderer->resources())
  455. Render::GL::renderArrows(m_renderer.get(), res, *m_arrowSystem);
  456. }
  457. if (auto *res = m_renderer->resources()) {
  458. std::optional<QVector3D> previewWaypoint;
  459. if (m_patrol.hasFirstWaypoint) {
  460. previewWaypoint = m_patrol.firstWaypoint;
  461. }
  462. Render::GL::renderPatrolFlags(m_renderer.get(), res, *m_world,
  463. previewWaypoint);
  464. }
  465. m_renderer->endFrame();
  466. emit globalCursorChanged();
  467. }
  468. bool GameEngine::screenToGround(const QPointF &screenPt, QVector3D &outWorld) {
  469. if (!m_window || !m_camera || !m_pickingService)
  470. return false;
  471. int w = (m_viewport.width > 0 ? m_viewport.width : m_window->width());
  472. int h = (m_viewport.height > 0 ? m_viewport.height : m_window->height());
  473. return m_pickingService->screenToGround(*m_camera, w, h, screenPt, outWorld);
  474. }
  475. bool GameEngine::worldToScreen(const QVector3D &world,
  476. QPointF &outScreen) const {
  477. if (!m_camera || m_viewport.width <= 0 || m_viewport.height <= 0 ||
  478. !m_pickingService)
  479. return false;
  480. return m_pickingService->worldToScreen(*m_camera, m_viewport.width,
  481. m_viewport.height, world, outScreen);
  482. }
  483. void GameEngine::syncSelectionFlags() {
  484. if (!m_world || !m_selectionSystem)
  485. return;
  486. const auto &sel = m_selectionSystem->getSelectedUnits();
  487. std::vector<Engine::Core::EntityID> toKeep;
  488. toKeep.reserve(sel.size());
  489. for (auto id : sel) {
  490. if (auto *e = m_world->getEntity(id)) {
  491. if (auto *u = e->getComponent<Engine::Core::UnitComponent>()) {
  492. if (u->health > 0)
  493. toKeep.push_back(id);
  494. }
  495. }
  496. }
  497. if (toKeep.size() != sel.size()) {
  498. m_selectionSystem->clearSelection();
  499. for (auto id : toKeep)
  500. m_selectionSystem->selectUnit(id);
  501. }
  502. if (m_selectionSystem->getSelectedUnits().empty()) {
  503. if (m_runtime.cursorMode != "normal") {
  504. setCursorMode("normal");
  505. }
  506. }
  507. }
  508. void GameEngine::cameraMove(float dx, float dz) {
  509. ensureInitialized();
  510. if (!m_camera)
  511. return;
  512. float scale = 0.12f;
  513. if (m_camera) {
  514. float dist = m_camera->getDistance();
  515. scale = std::max(0.12f, dist * 0.05f);
  516. }
  517. Game::Systems::CameraController ctrl;
  518. ctrl.move(*m_camera, dx * scale, dz * scale);
  519. }
  520. void GameEngine::cameraElevate(float dy) {
  521. ensureInitialized();
  522. if (!m_camera)
  523. return;
  524. Game::Systems::CameraController ctrl;
  525. float distance = m_camera ? m_camera->getDistance() : 10.0f;
  526. float scale = std::clamp(distance * 0.05f, 0.1f, 5.0f);
  527. ctrl.moveUp(*m_camera, dy * scale);
  528. }
  529. void GameEngine::resetCamera() {
  530. ensureInitialized();
  531. if (!m_camera || !m_world)
  532. return;
  533. Engine::Core::Entity *focusEntity = nullptr;
  534. for (auto *e : m_world->getEntitiesWith<Engine::Core::UnitComponent>()) {
  535. if (!e)
  536. continue;
  537. auto *u = e->getComponent<Engine::Core::UnitComponent>();
  538. if (!u)
  539. continue;
  540. if (u->unitType == "barracks" && u->ownerId == m_runtime.localOwnerId &&
  541. u->health > 0) {
  542. focusEntity = e;
  543. break;
  544. }
  545. }
  546. if (!focusEntity && m_level.playerUnitId != 0)
  547. focusEntity = m_world->getEntity(m_level.playerUnitId);
  548. if (focusEntity) {
  549. if (auto *t =
  550. focusEntity->getComponent<Engine::Core::TransformComponent>()) {
  551. QVector3D center(t->position.x, t->position.y, t->position.z);
  552. if (m_camera) {
  553. m_camera->setRTSView(center, 12.0f, 45.0f, 225.0f);
  554. }
  555. }
  556. }
  557. }
  558. void GameEngine::cameraZoom(float delta) {
  559. ensureInitialized();
  560. if (!m_camera)
  561. return;
  562. Game::Systems::CameraController ctrl;
  563. ctrl.zoomDistance(*m_camera, delta);
  564. }
  565. float GameEngine::cameraDistance() const {
  566. if (!m_camera)
  567. return 0.0f;
  568. return m_camera->getDistance();
  569. }
  570. void GameEngine::cameraYaw(float degrees) {
  571. ensureInitialized();
  572. if (!m_camera)
  573. return;
  574. Game::Systems::CameraController ctrl;
  575. ctrl.yaw(*m_camera, degrees);
  576. }
  577. void GameEngine::cameraOrbit(float yawDeg, float pitchDeg) {
  578. ensureInitialized();
  579. if (!m_camera)
  580. return;
  581. qDebug() << "GameEngine::cameraOrbit called:" << "yaw=" << yawDeg
  582. << "pitch=" << pitchDeg;
  583. if (!std::isfinite(yawDeg) || !std::isfinite(pitchDeg)) {
  584. qWarning()
  585. << "GameEngine::cameraOrbit received invalid input, applying fallback"
  586. << yawDeg << pitchDeg;
  587. float fallback = 2.0f;
  588. if (!std::isfinite(pitchDeg))
  589. pitchDeg = fallback;
  590. if (!std::isfinite(yawDeg))
  591. yawDeg = 0.0f;
  592. }
  593. Game::Systems::CameraController ctrl;
  594. ctrl.orbit(*m_camera, yawDeg, pitchDeg);
  595. }
  596. void GameEngine::cameraOrbitDirection(int direction, bool shift) {
  597. float step = shift ? 8.0f : 4.0f;
  598. float pitch = step * float(direction);
  599. cameraOrbit(0.0f, pitch);
  600. }
  601. void GameEngine::cameraFollowSelection(bool enable) {
  602. ensureInitialized();
  603. m_followSelectionEnabled = enable;
  604. if (m_camera) {
  605. Game::Systems::CameraController ctrl;
  606. ctrl.setFollowEnabled(*m_camera, enable);
  607. }
  608. if (enable && m_camera && m_selectionSystem && m_world) {
  609. Game::Systems::CameraFollowSystem cfs;
  610. cfs.snapToSelection(*m_world, *m_selectionSystem, *m_camera);
  611. } else if (m_camera) {
  612. auto pos = m_camera->getPosition();
  613. auto tgt = m_camera->getTarget();
  614. QVector3D front = (tgt - pos).normalized();
  615. m_camera->lookAt(pos, tgt, QVector3D(0, 1, 0));
  616. }
  617. }
  618. void GameEngine::cameraSetFollowLerp(float alpha) {
  619. ensureInitialized();
  620. if (!m_camera)
  621. return;
  622. float a = std::clamp(alpha, 0.0f, 1.0f);
  623. Game::Systems::CameraController ctrl;
  624. ctrl.setFollowLerp(*m_camera, a);
  625. }
  626. QObject *GameEngine::selectedUnitsModel() { return m_selectedUnitsModel; }
  627. bool GameEngine::hasUnitsSelected() const {
  628. if (!m_selectionSystem)
  629. return false;
  630. const auto &sel = m_selectionSystem->getSelectedUnits();
  631. return !sel.empty();
  632. }
  633. int GameEngine::playerTroopCount() const {
  634. if (!m_world)
  635. return 0;
  636. int count = 0;
  637. auto entities = m_world->getEntitiesWith<Engine::Core::UnitComponent>();
  638. for (auto *entity : entities) {
  639. auto *unit = entity->getComponent<Engine::Core::UnitComponent>();
  640. if (!unit)
  641. continue;
  642. if (unit->ownerId == m_runtime.localOwnerId && unit->health > 0 &&
  643. unit->unitType != "barracks") {
  644. count++;
  645. }
  646. }
  647. return count;
  648. }
  649. bool GameEngine::hasSelectedType(const QString &type) const {
  650. if (!m_selectionSystem || !m_world)
  651. return false;
  652. const auto &sel = m_selectionSystem->getSelectedUnits();
  653. for (auto id : sel) {
  654. if (auto *e = m_world->getEntity(id)) {
  655. if (auto *u = e->getComponent<Engine::Core::UnitComponent>()) {
  656. if (QString::fromStdString(u->unitType) == type)
  657. return true;
  658. }
  659. }
  660. }
  661. return false;
  662. }
  663. void GameEngine::recruitNearSelected(const QString &unitType) {
  664. ensureInitialized();
  665. if (!m_world)
  666. return;
  667. if (!m_selectionSystem)
  668. return;
  669. const auto &sel = m_selectionSystem->getSelectedUnits();
  670. if (sel.empty())
  671. return;
  672. Game::Systems::ProductionService::startProductionForFirstSelectedBarracks(
  673. *m_world, sel, m_runtime.localOwnerId, unitType.toStdString());
  674. }
  675. QVariantMap GameEngine::getSelectedProductionState() const {
  676. QVariantMap m;
  677. m["hasBarracks"] = false;
  678. m["inProgress"] = false;
  679. m["timeRemaining"] = 0.0;
  680. m["buildTime"] = 0.0;
  681. m["producedCount"] = 0;
  682. m["maxUnits"] = 0;
  683. if (!m_selectionSystem || !m_world)
  684. return m;
  685. Game::Systems::ProductionState st;
  686. Game::Systems::ProductionService::getSelectedBarracksState(
  687. *m_world, m_selectionSystem->getSelectedUnits(), m_runtime.localOwnerId,
  688. st);
  689. m["hasBarracks"] = st.hasBarracks;
  690. m["inProgress"] = st.inProgress;
  691. m["timeRemaining"] = st.timeRemaining;
  692. m["buildTime"] = st.buildTime;
  693. m["producedCount"] = st.producedCount;
  694. m["maxUnits"] = st.maxUnits;
  695. return m;
  696. }
  697. QString GameEngine::getSelectedUnitsCommandMode() const {
  698. if (!m_selectionSystem || !m_world)
  699. return "normal";
  700. const auto &sel = m_selectionSystem->getSelectedUnits();
  701. if (sel.empty())
  702. return "normal";
  703. int attackingCount = 0;
  704. int patrollingCount = 0;
  705. int totalUnits = 0;
  706. for (auto id : sel) {
  707. auto *e = m_world->getEntity(id);
  708. if (!e)
  709. continue;
  710. auto *u = e->getComponent<Engine::Core::UnitComponent>();
  711. if (!u)
  712. continue;
  713. if (u->unitType == "barracks")
  714. continue;
  715. totalUnits++;
  716. if (e->getComponent<Engine::Core::AttackTargetComponent>())
  717. attackingCount++;
  718. auto *patrol = e->getComponent<Engine::Core::PatrolComponent>();
  719. if (patrol && patrol->patrolling)
  720. patrollingCount++;
  721. }
  722. if (totalUnits == 0)
  723. return "normal";
  724. if (patrollingCount == totalUnits)
  725. return "patrol";
  726. if (attackingCount == totalUnits)
  727. return "attack";
  728. return "normal";
  729. }
  730. void GameEngine::setRallyAtScreen(qreal sx, qreal sy) {
  731. ensureInitialized();
  732. if (!m_world || !m_selectionSystem)
  733. return;
  734. QVector3D hit;
  735. if (!screenToGround(QPointF(sx, sy), hit))
  736. return;
  737. Game::Systems::ProductionService::setRallyForFirstSelectedBarracks(
  738. *m_world, m_selectionSystem->getSelectedUnits(), m_runtime.localOwnerId,
  739. hit.x(), hit.z());
  740. }
  741. QVariantList GameEngine::availableMaps() const {
  742. QVariantList list;
  743. QDir mapsDir(QStringLiteral("assets/maps"));
  744. if (!mapsDir.exists())
  745. return list;
  746. QStringList files =
  747. mapsDir.entryList(QStringList() << "*.json", QDir::Files, QDir::Name);
  748. for (const QString &f : files) {
  749. QString path = mapsDir.filePath(f);
  750. QFile file(path);
  751. QString name = f;
  752. QString desc;
  753. if (file.open(QIODevice::ReadOnly)) {
  754. QByteArray data = file.readAll();
  755. file.close();
  756. QJsonParseError err;
  757. QJsonDocument doc = QJsonDocument::fromJson(data, &err);
  758. if (err.error == QJsonParseError::NoError && doc.isObject()) {
  759. QJsonObject obj = doc.object();
  760. if (obj.contains("name") && obj["name"].isString())
  761. name = obj["name"].toString();
  762. if (obj.contains("description") && obj["description"].isString())
  763. desc = obj["description"].toString();
  764. }
  765. }
  766. QVariantMap entry;
  767. entry["name"] = name;
  768. entry["description"] = desc;
  769. entry["path"] = path;
  770. list.append(entry);
  771. }
  772. return list;
  773. }
  774. void GameEngine::startSkirmish(const QString &mapPath) {
  775. m_level.mapName = mapPath;
  776. if (!m_runtime.initialized) {
  777. initialize();
  778. return;
  779. }
  780. if (m_world && m_renderer && m_camera) {
  781. m_runtime.loading = true;
  782. if (m_selectionSystem) {
  783. m_selectionSystem->clearSelection();
  784. }
  785. if (m_renderer) {
  786. m_renderer->pause();
  787. m_renderer->lockWorldForModification();
  788. m_renderer->setSelectedEntities({});
  789. m_renderer->setHoveredEntityId(0);
  790. }
  791. m_hover.entityId = 0;
  792. m_world->clear();
  793. Game::Systems::BuildingCollisionRegistry::instance().clear();
  794. auto lr = Game::Map::LevelLoader::loadFromAssets(m_level.mapName, *m_world,
  795. *m_renderer, *m_camera);
  796. if (m_ground) {
  797. if (lr.ok)
  798. m_ground->configure(lr.tileSize, lr.gridWidth, lr.gridHeight);
  799. else
  800. m_ground->configureExtent(50.0f);
  801. }
  802. if (m_terrain) {
  803. auto &terrainService = Game::Map::TerrainService::instance();
  804. if (terrainService.isInitialized() && terrainService.getHeightMap()) {
  805. m_terrain->configure(*terrainService.getHeightMap());
  806. }
  807. }
  808. int mapWidth = lr.ok ? lr.gridWidth : 100;
  809. int mapHeight = lr.ok ? lr.gridHeight : 100;
  810. Game::Systems::CommandService::initialize(mapWidth, mapHeight);
  811. auto &visibilityService = Game::Map::VisibilityService::instance();
  812. visibilityService.initialize(mapWidth, mapHeight, lr.tileSize);
  813. if (m_world)
  814. visibilityService.update(*m_world, m_runtime.localOwnerId);
  815. if (m_fog && visibilityService.isInitialized()) {
  816. m_fog->updateMask(
  817. visibilityService.getWidth(), visibilityService.getHeight(),
  818. visibilityService.getTileSize(), visibilityService.cells());
  819. m_runtime.visibilityVersion = visibilityService.version();
  820. } else {
  821. m_runtime.visibilityVersion = 0;
  822. }
  823. m_level.mapName = lr.mapName;
  824. m_level.playerUnitId = lr.playerUnitId;
  825. m_level.camFov = lr.camFov;
  826. m_level.camNear = lr.camNear;
  827. m_level.camFar = lr.camFar;
  828. m_level.maxTroopsPerPlayer = lr.maxTroopsPerPlayer;
  829. if (m_renderer) {
  830. m_renderer->unlockWorldForModification();
  831. m_renderer->resume();
  832. }
  833. if (m_world && m_camera) {
  834. Engine::Core::Entity *focusEntity = nullptr;
  835. auto candidates = m_world->getEntitiesWith<Engine::Core::UnitComponent>();
  836. for (auto *e : candidates) {
  837. if (!e)
  838. continue;
  839. auto *u = e->getComponent<Engine::Core::UnitComponent>();
  840. if (!u)
  841. continue;
  842. if (u->unitType == "barracks" && u->ownerId == m_runtime.localOwnerId &&
  843. u->health > 0) {
  844. focusEntity = e;
  845. break;
  846. }
  847. }
  848. if (!focusEntity && m_level.playerUnitId != 0) {
  849. focusEntity = m_world->getEntity(m_level.playerUnitId);
  850. }
  851. if (focusEntity) {
  852. if (auto *t =
  853. focusEntity->getComponent<Engine::Core::TransformComponent>()) {
  854. QVector3D center(t->position.x, t->position.y, t->position.z);
  855. m_camera->setRTSView(center, 12.0f, 45.0f, 225.0f);
  856. }
  857. }
  858. }
  859. m_runtime.loading = false;
  860. }
  861. }
  862. void GameEngine::openSettings() { qInfo() << "Open settings requested"; }
  863. void GameEngine::loadSave() {
  864. qInfo() << "Load save requested (not implemented)";
  865. }
  866. void GameEngine::exitGame() {
  867. qInfo() << "Exit requested";
  868. QCoreApplication::quit();
  869. }
  870. void GameEngine::getSelectedUnitIds(
  871. std::vector<Engine::Core::EntityID> &out) const {
  872. out.clear();
  873. if (!m_selectionSystem)
  874. return;
  875. const auto &ids = m_selectionSystem->getSelectedUnits();
  876. out.assign(ids.begin(), ids.end());
  877. }
  878. bool GameEngine::getUnitInfo(Engine::Core::EntityID id, QString &name,
  879. int &health, int &maxHealth, bool &isBuilding,
  880. bool &alive) const {
  881. if (!m_world)
  882. return false;
  883. auto *e = m_world->getEntity(id);
  884. if (!e)
  885. return false;
  886. isBuilding = e->hasComponent<Engine::Core::BuildingComponent>();
  887. if (auto *u = e->getComponent<Engine::Core::UnitComponent>()) {
  888. name = QString::fromStdString(u->unitType);
  889. health = u->health;
  890. maxHealth = u->maxHealth;
  891. alive = (u->health > 0);
  892. return true;
  893. }
  894. name = QStringLiteral("Entity");
  895. health = maxHealth = 0;
  896. alive = true;
  897. return true;
  898. }
  899. void GameEngine::checkVictoryCondition() {
  900. if (!m_world || m_runtime.victoryState != "")
  901. return;
  902. if (m_level.mapName.isEmpty())
  903. return;
  904. bool enemyBarracksAlive = false;
  905. bool playerBarracksAlive = false;
  906. auto entities = m_world->getEntitiesWith<Engine::Core::UnitComponent>();
  907. for (auto *e : entities) {
  908. auto *unit = e->getComponent<Engine::Core::UnitComponent>();
  909. if (!unit || unit->health <= 0)
  910. continue;
  911. if (unit->unitType == "barracks") {
  912. if (unit->ownerId == 2) {
  913. enemyBarracksAlive = true;
  914. } else if (unit->ownerId == m_runtime.localOwnerId) {
  915. playerBarracksAlive = true;
  916. }
  917. }
  918. }
  919. if (!enemyBarracksAlive) {
  920. m_runtime.victoryState = "victory";
  921. emit victoryStateChanged();
  922. qInfo() << "VICTORY! Enemy barracks destroyed!";
  923. }
  924. else if (!playerBarracksAlive) {
  925. m_runtime.victoryState = "defeat";
  926. emit victoryStateChanged();
  927. qInfo() << "DEFEAT! Your barracks was destroyed!";
  928. }
  929. }