game_engine.cpp 82 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648
  1. #include "game_engine.h"
  2. #include "../controllers/action_vfx.h"
  3. #include "../controllers/command_controller.h"
  4. #include "../models/audio_system_proxy.h"
  5. #include "../models/cursor_manager.h"
  6. #include "../models/hover_tracker.h"
  7. #include "ambient_state_manager.h"
  8. #include "app/models/cursor_mode.h"
  9. #include "app/utils/engine_view_helpers.h"
  10. #include "app/utils/movement_utils.h"
  11. #include "app/utils/selection_utils.h"
  12. #include "audio_event_handler.h"
  13. #include "audio_resource_loader.h"
  14. #include "camera_controller.h"
  15. #include "campaign_manager.h"
  16. #include "core/system.h"
  17. #include "game/audio/audio_system.h"
  18. #include "game/map/mission_context.h"
  19. #include "game/units/spawn_type.h"
  20. #include "game/units/troop_type.h"
  21. #include "game_state_restorer.h"
  22. #include "input_command_handler.h"
  23. #include "level_orchestrator.h"
  24. #include "loading_progress_tracker.h"
  25. #include "minimap_manager.h"
  26. #include "production_manager.h"
  27. #include "renderer_bootstrap.h"
  28. #include "selection_query_service.h"
  29. #include <QBuffer>
  30. #include <QCoreApplication>
  31. #include <QCursor>
  32. #include <QDebug>
  33. #include <QElapsedTimer>
  34. #include <QEventLoop>
  35. #include <QImage>
  36. #include <QOpenGLContext>
  37. #include <QPainter>
  38. #include <QQuickWindow>
  39. #include <QSize>
  40. #include <QTimer>
  41. #include <QVariant>
  42. #include <QVariantMap>
  43. #include <cmath>
  44. #include <memory>
  45. #include <optional>
  46. #include <qbuffer.h>
  47. #include <qcoreapplication.h>
  48. #include <qdir.h>
  49. #include <qevent.h>
  50. #include <qglobal.h>
  51. #include <qimage.h>
  52. #include <qjsonobject.h>
  53. #include <qnamespace.h>
  54. #include <qobject.h>
  55. #include <qobjectdefs.h>
  56. #include <qpoint.h>
  57. #include <qsize.h>
  58. #include <qstringliteral.h>
  59. #include <qstringview.h>
  60. #include <qtmetamacros.h>
  61. #include <qvectornd.h>
  62. #include <unordered_set>
  63. #include "../models/selected_units_model.h"
  64. #include "game/core/component.h"
  65. #include "game/core/event_manager.h"
  66. #include "game/core/world.h"
  67. #include "game/game_config.h"
  68. #include "game/map/campaign_loader.h"
  69. #include "game/map/environment.h"
  70. #include "game/map/level_loader.h"
  71. #include "game/map/map_catalog.h"
  72. #include "game/map/map_loader.h"
  73. #include "game/map/map_transformer.h"
  74. #include "game/map/minimap/map_preview_generator.h"
  75. #include "game/map/minimap/minimap_generator.h"
  76. #include "game/map/minimap/minimap_utils.h"
  77. #include "game/map/minimap/unit_layer.h"
  78. #include "game/map/mission_loader.h"
  79. #include "game/map/skirmish_loader.h"
  80. #include "game/map/terrain_service.h"
  81. #include "game/map/visibility_service.h"
  82. #include "game/map/world_bootstrap.h"
  83. #include "game/systems/ai_system.h"
  84. #include "game/systems/ai_system/ai_strategy.h"
  85. #include "game/systems/arrow_system.h"
  86. #include "game/systems/ballista_attack_system.h"
  87. #include "game/systems/building_collision_registry.h"
  88. #include "game/systems/camera_service.h"
  89. #include "game/systems/camera_visibility_service.h"
  90. #include "game/systems/capture_system.h"
  91. #include "game/systems/catapult_attack_system.h"
  92. #include "game/systems/cleanup_system.h"
  93. #include "game/systems/combat_system.h"
  94. #include "game/systems/command_service.h"
  95. #include "game/systems/formation_planner.h"
  96. #include "game/systems/game_state_serializer.h"
  97. #include "game/systems/global_stats_registry.h"
  98. #include "game/systems/guard_system.h"
  99. #include "game/systems/healing_beam_system.h"
  100. #include "game/systems/healing_system.h"
  101. #include "game/systems/movement_system.h"
  102. #include "game/systems/nation_id.h"
  103. #include "game/systems/nation_registry.h"
  104. #include "game/systems/owner_registry.h"
  105. #include "game/systems/patrol_system.h"
  106. #include "game/systems/picking_service.h"
  107. #include "game/systems/production_service.h"
  108. #include "game/systems/production_system.h"
  109. #include "game/systems/projectile_system.h"
  110. #include "game/systems/rain_manager.h"
  111. #include "game/systems/save_load_service.h"
  112. #include "game/systems/selection_system.h"
  113. #include "game/systems/terrain_alignment_system.h"
  114. #include "game/systems/troop_count_registry.h"
  115. #include "game/systems/troop_profile_service.h"
  116. #include "game/systems/victory_service.h"
  117. #include "game/units/factory.h"
  118. #include "game/units/troop_config.h"
  119. #include "game/visuals/team_colors.h"
  120. #include "render/entity/combat_dust_renderer.h"
  121. #include "render/entity/healer_aura_renderer.h"
  122. #include "render/entity/healing_beam_renderer.h"
  123. #include "render/entity/healing_waves_renderer.h"
  124. #include "render/geom/arrow.h"
  125. #include "render/geom/formation_arrow.h"
  126. #include "render/geom/patrol_flags.h"
  127. #include "render/geom/stone.h"
  128. #include "render/gl/bootstrap.h"
  129. #include "render/gl/camera.h"
  130. #include "render/ground/biome_renderer.h"
  131. #include "render/ground/bridge_renderer.h"
  132. #include "render/ground/firecamp_renderer.h"
  133. #include "render/ground/fog_renderer.h"
  134. #include "render/ground/ground_renderer.h"
  135. #include "render/ground/olive_renderer.h"
  136. #include "render/ground/pine_renderer.h"
  137. #include "render/ground/plant_renderer.h"
  138. #include "render/ground/rain_renderer.h"
  139. #include "render/ground/river_renderer.h"
  140. #include "render/ground/riverbank_renderer.h"
  141. #include "render/ground/road_renderer.h"
  142. #include "render/ground/stone_renderer.h"
  143. #include "render/ground/terrain_renderer.h"
  144. #include "render/scene_renderer.h"
  145. #include "utils/resource_utils.h"
  146. #include <QDir>
  147. #include <QFile>
  148. #include <QJsonArray>
  149. #include <QJsonDocument>
  150. #include <QJsonObject>
  151. #include <QSet>
  152. #include <QStringList>
  153. #include <algorithm>
  154. #include <cmath>
  155. #include <utility>
  156. #include <vector>
  157. namespace {
  158. auto resolve_mission_file_path(const QString &mission_id) -> QString {
  159. const QStringList search_paths = {
  160. QStringLiteral("assets/missions/%1.json"),
  161. QStringLiteral("../assets/missions/%1.json"),
  162. QStringLiteral("../../assets/missions/%1.json"),
  163. QStringLiteral(":/assets/missions/%1.json"),
  164. QStringLiteral("/assets/missions/%1.json"),
  165. QStringLiteral("/../assets/missions/%1.json")};
  166. for (const auto &pattern : search_paths) {
  167. QString candidate = pattern.arg(mission_id);
  168. candidate = Utils::Resources::resolveResourcePath(candidate);
  169. if (QFile::exists(candidate)) {
  170. return candidate;
  171. }
  172. }
  173. return {};
  174. }
  175. auto build_condition_list(
  176. const std::vector<Game::Mission::Condition> &conditions) -> QVariantList {
  177. QVariantList list;
  178. for (const auto &condition : conditions) {
  179. QVariantMap cond;
  180. cond["type"] = condition.type;
  181. cond["description"] = condition.description;
  182. if (condition.duration.has_value()) {
  183. cond["duration"] = condition.duration.value();
  184. }
  185. if (condition.structure_type.has_value()) {
  186. cond["structure_type"] = condition.structure_type.value();
  187. }
  188. if (!condition.structure_types.empty()) {
  189. QVariantList types;
  190. for (const auto &type : condition.structure_types) {
  191. types.append(type);
  192. }
  193. cond["structure_types"] = types;
  194. }
  195. if (condition.min_count.has_value()) {
  196. cond["min_count"] = condition.min_count.value();
  197. }
  198. if (condition.wave_count.has_value()) {
  199. cond["wave_count"] = condition.wave_count.value();
  200. }
  201. list.append(cond);
  202. }
  203. return list;
  204. }
  205. auto build_unit_setup_list(const std::vector<Game::Mission::UnitSetup> &units)
  206. -> QVariantList {
  207. QVariantList list;
  208. for (const auto &unit : units) {
  209. QVariantMap entry;
  210. entry["type"] = unit.type;
  211. entry["count"] = unit.count;
  212. entry["x"] = unit.position.x;
  213. entry["z"] = unit.position.z;
  214. list.append(entry);
  215. }
  216. return list;
  217. }
  218. auto build_building_setup_list(const std::vector<Game::Mission::BuildingSetup>
  219. &buildings) -> QVariantList {
  220. QVariantList list;
  221. for (const auto &building : buildings) {
  222. QVariantMap entry;
  223. entry["type"] = building.type;
  224. entry["max_population"] = building.max_population;
  225. entry["x"] = building.position.x;
  226. entry["z"] = building.position.z;
  227. list.append(entry);
  228. }
  229. return list;
  230. }
  231. auto build_player_setup_map(const Game::Mission::PlayerSetup &setup)
  232. -> QVariantMap {
  233. QVariantMap map;
  234. map["nation"] = setup.nation;
  235. map["faction"] = setup.faction;
  236. map["color"] = setup.color;
  237. map["starting_units"] = build_unit_setup_list(setup.starting_units);
  238. map["starting_buildings"] =
  239. build_building_setup_list(setup.starting_buildings);
  240. QVariantMap resources;
  241. resources["gold"] = setup.starting_resources.gold;
  242. resources["food"] = setup.starting_resources.food;
  243. map["starting_resources"] = resources;
  244. return map;
  245. }
  246. auto build_mission_definition_map(
  247. const Game::Mission::MissionDefinition &mission) -> QVariantMap {
  248. QVariantMap result;
  249. result["id"] = mission.id;
  250. result["title"] = mission.title;
  251. result["summary"] = mission.summary;
  252. result["map_path"] = mission.map_path;
  253. if (mission.teaching_goal.has_value()) {
  254. result["teaching_goal"] = mission.teaching_goal.value();
  255. }
  256. if (mission.narrative_intent.has_value()) {
  257. result["narrative_intent"] = mission.narrative_intent.value();
  258. }
  259. if (mission.historical_context.has_value()) {
  260. result["historical_context"] = mission.historical_context.value();
  261. }
  262. if (mission.terrain_type.has_value()) {
  263. result["terrain_type"] = mission.terrain_type.value();
  264. }
  265. result["player_setup"] = build_player_setup_map(mission.player_setup);
  266. result["victory_conditions"] =
  267. build_condition_list(mission.victory_conditions);
  268. result["defeat_conditions"] = build_condition_list(mission.defeat_conditions);
  269. result["optional_objectives"] =
  270. build_condition_list(mission.optional_objectives);
  271. return result;
  272. }
  273. } // namespace
  274. GameEngine::GameEngine(QObject *parent)
  275. : QObject(parent),
  276. m_selectedUnitsModel(new SelectedUnitsModel(this, this)) {
  277. Game::Systems::NationRegistry::instance().initialize_defaults();
  278. Game::Systems::TroopCountRegistry::instance().initialize();
  279. Game::Systems::GlobalStatsRegistry::instance().initialize();
  280. m_world = std::make_unique<Engine::Core::World>();
  281. auto rendering = RendererBootstrap::initialize_rendering();
  282. m_renderer = std::move(rendering.renderer);
  283. m_camera = std::move(rendering.camera);
  284. m_ground = std::move(rendering.ground);
  285. m_terrain = std::move(rendering.terrain);
  286. m_biome = std::move(rendering.biome);
  287. m_river = std::move(rendering.river);
  288. m_road = std::move(rendering.road);
  289. m_riverbank = std::move(rendering.riverbank);
  290. m_bridge = std::move(rendering.bridge);
  291. m_fog = std::move(rendering.fog);
  292. m_stone = std::move(rendering.stone);
  293. m_plant = std::move(rendering.plant);
  294. m_pine = std::move(rendering.pine);
  295. m_olive = std::move(rendering.olive);
  296. m_firecamp = std::move(rendering.firecamp);
  297. m_rain = std::move(rendering.rain);
  298. m_passes = std::move(rendering.passes);
  299. RendererBootstrap::initialize_world_systems(*m_world);
  300. m_pickingService = std::make_unique<Game::Systems::PickingService>();
  301. m_victoryService = std::make_unique<Game::Systems::VictoryService>();
  302. m_saveLoadService = std::make_unique<Game::Systems::SaveLoadService>();
  303. m_cameraService = std::make_unique<Game::Systems::CameraService>();
  304. m_rainManager = std::make_unique<Game::Systems::RainManager>();
  305. m_loading_progress_tracker = std::make_unique<LoadingProgressTracker>(this);
  306. connect(m_loading_progress_tracker.get(),
  307. &LoadingProgressTracker::progress_changed, this,
  308. [this](float progress) { emit loading_progress_changed(progress); });
  309. connect(m_loading_progress_tracker.get(),
  310. &LoadingProgressTracker::stage_changed, this,
  311. [this](LoadingProgressTracker::LoadingStage, QString detail) {
  312. emit loading_stage_changed(detail);
  313. });
  314. auto *selection_system =
  315. m_world->get_system<Game::Systems::SelectionSystem>();
  316. m_selectionController = std::make_unique<Game::Systems::SelectionController>(
  317. m_world.get(), selection_system, m_pickingService.get());
  318. m_commandController = std::make_unique<App::Controllers::CommandController>(
  319. m_world.get(), selection_system, m_pickingService.get());
  320. m_cursor_manager = std::make_unique<CursorManager>();
  321. m_hoverTracker = std::make_unique<HoverTracker>(m_pickingService.get());
  322. m_mapCatalog = std::make_unique<Game::Map::MapCatalog>(this);
  323. connect(m_mapCatalog.get(), &Game::Map::MapCatalog::map_loaded, this,
  324. [this](const QVariantMap &mapData) {
  325. m_available_maps.append(mapData);
  326. emit available_maps_changed();
  327. });
  328. connect(m_mapCatalog.get(), &Game::Map::MapCatalog::loading_changed, this,
  329. [this](bool loading) {
  330. m_maps_loading = loading;
  331. emit maps_loading_changed();
  332. });
  333. connect(m_mapCatalog.get(), &Game::Map::MapCatalog::all_maps_loaded, this,
  334. [this]() { emit available_maps_changed(); });
  335. if (AudioSystem::getInstance().initialize()) {
  336. qInfo() << "AudioSystem initialized successfully";
  337. AudioResourceLoader::load_audio_resources();
  338. } else {
  339. qWarning() << "Failed to initialize AudioSystem";
  340. }
  341. m_audio_systemProxy = std::make_unique<App::Models::AudioSystemProxy>(this);
  342. m_minimap_manager = std::make_unique<MinimapManager>();
  343. m_ambient_state_manager = std::make_unique<AmbientStateManager>();
  344. m_input_handler = std::make_unique<InputCommandHandler>(
  345. m_world.get(), m_selectionController.get(), m_commandController.get(),
  346. m_cursor_manager.get(), m_hoverTracker.get(), m_pickingService.get(),
  347. m_camera.get());
  348. m_camera_controller = std::make_unique<CameraController>(
  349. m_camera.get(), m_cameraService.get(), m_world.get());
  350. m_production_manager = std::make_unique<ProductionManager>(
  351. m_world.get(), m_pickingService.get(), m_camera.get(), this);
  352. connect(m_production_manager.get(),
  353. &ProductionManager::placing_construction_changed, this,
  354. &GameEngine::placing_construction_changed);
  355. m_campaign_manager = std::make_unique<CampaignManager>(this);
  356. connect(m_campaign_manager.get(),
  357. &CampaignManager::available_campaigns_changed, this,
  358. &GameEngine::available_campaigns_changed);
  359. m_selection_query_service =
  360. std::make_unique<SelectionQueryService>(m_world.get(), this);
  361. m_audioEventHandler =
  362. std::make_unique<Game::Audio::AudioEventHandler>(m_world.get());
  363. if (m_audioEventHandler->initialize()) {
  364. qInfo() << "AudioEventHandler initialized successfully";
  365. m_audioEventHandler->loadUnitVoiceMapping("archer", "archer_voice");
  366. m_audioEventHandler->loadUnitVoiceMapping("swordsman", "swordsman_voice");
  367. m_audioEventHandler->loadUnitVoiceMapping("swordsman", "swordsman_voice");
  368. m_audioEventHandler->loadUnitVoiceMapping("spearman", "spearman_voice");
  369. m_audioEventHandler->loadAmbientMusic(Engine::Core::AmbientState::PEACEFUL,
  370. "music_peaceful");
  371. m_audioEventHandler->loadAmbientMusic(Engine::Core::AmbientState::TENSE,
  372. "music_tense");
  373. m_audioEventHandler->loadAmbientMusic(Engine::Core::AmbientState::COMBAT,
  374. "music_combat");
  375. m_audioEventHandler->loadAmbientMusic(Engine::Core::AmbientState::VICTORY,
  376. "music_victory");
  377. m_audioEventHandler->loadAmbientMusic(Engine::Core::AmbientState::DEFEAT,
  378. "music_defeat");
  379. qInfo() << "Audio mappings configured";
  380. } else {
  381. qWarning() << "Failed to initialize AudioEventHandler";
  382. }
  383. connect(m_cursor_manager.get(), &CursorManager::mode_changed, this, [this]() {
  384. if (m_cursor_manager && m_window) {
  385. m_cursor_manager->update_cursor_shape(m_window);
  386. }
  387. emit cursor_mode_changed();
  388. });
  389. connect(m_cursor_manager.get(), &CursorManager::global_cursor_changed, this,
  390. &GameEngine::global_cursor_changed);
  391. connect(m_selectionController.get(),
  392. &Game::Systems::SelectionController::selection_changed, this,
  393. &GameEngine::selected_units_changed);
  394. connect(m_selectionController.get(),
  395. &Game::Systems::SelectionController::selection_changed, this,
  396. &GameEngine::sync_selection_flags);
  397. connect(
  398. m_selectionController.get(),
  399. &Game::Systems::SelectionController::selection_model_refresh_requested,
  400. this, &GameEngine::selected_units_data_changed);
  401. connect(m_commandController.get(),
  402. &App::Controllers::CommandController::attack_target_selected,
  403. [this]() {
  404. if (auto *sel_sys =
  405. m_world->get_system<Game::Systems::SelectionSystem>()) {
  406. const auto &sel = sel_sys->get_selected_units();
  407. if (!sel.empty()) {
  408. auto *cam = m_camera.get();
  409. auto *picking = m_pickingService.get();
  410. if ((cam != nullptr) && (picking != nullptr)) {
  411. Engine::Core::EntityID const target_id =
  412. Game::Systems::PickingService::pick_unit_first(
  413. 0.0F, 0.0F, *m_world, *cam, m_viewport.width,
  414. m_viewport.height, 0);
  415. if (target_id != 0) {
  416. App::Controllers::ActionVFX::spawnAttackArrow(m_world.get(),
  417. target_id);
  418. }
  419. }
  420. }
  421. }
  422. });
  423. connect(m_commandController.get(),
  424. &App::Controllers::CommandController::troop_limit_reached, [this]() {
  425. set_error(
  426. "Maximum troop limit reached. Cannot produce more units.");
  427. });
  428. connect(m_commandController.get(),
  429. &App::Controllers::CommandController::hold_mode_changed, this,
  430. &GameEngine::hold_mode_changed);
  431. connect(m_commandController.get(),
  432. &App::Controllers::CommandController::guard_mode_changed, this,
  433. &GameEngine::guard_mode_changed);
  434. connect(m_commandController.get(),
  435. &App::Controllers::CommandController::formation_mode_changed, this,
  436. &GameEngine::formation_mode_changed);
  437. connect(m_commandController.get(),
  438. &App::Controllers::CommandController::formation_placement_started,
  439. [this]() { emit placing_formation_changed(); });
  440. connect(m_commandController.get(),
  441. &App::Controllers::CommandController::formation_placement_ended,
  442. [this]() { emit placing_formation_changed(); });
  443. connect(this, SIGNAL(selected_units_changed()), m_selectedUnitsModel,
  444. SLOT(refresh()));
  445. connect(this, SIGNAL(selected_units_data_changed()), m_selectedUnitsModel,
  446. SLOT(refresh()));
  447. emit selected_units_changed();
  448. m_unit_died_subscription =
  449. Engine::Core::ScopedEventSubscription<Engine::Core::UnitDiedEvent>(
  450. [this](const Engine::Core::UnitDiedEvent &e) {
  451. on_unit_died(e);
  452. if (Game::Units::isTroopSpawn(e.spawn_type) &&
  453. e.owner_id != m_runtime.local_owner_id &&
  454. e.killer_owner_id == m_runtime.local_owner_id) {
  455. int const production_cost =
  456. Game::Units::TroopConfig::instance().getProductionCost(
  457. e.spawn_type);
  458. m_enemyTroopsDefeated += production_cost;
  459. emit enemy_troops_defeated_changed();
  460. }
  461. });
  462. m_unit_spawned_subscription =
  463. Engine::Core::ScopedEventSubscription<Engine::Core::UnitSpawnedEvent>(
  464. [this](const Engine::Core::UnitSpawnedEvent &e) {
  465. on_unit_spawned(e);
  466. });
  467. }
  468. GameEngine::~GameEngine() {
  469. if (m_audioEventHandler) {
  470. m_audioEventHandler->shutdown();
  471. }
  472. AudioSystem::getInstance().shutdown();
  473. qInfo() << "AudioSystem shut down";
  474. }
  475. void GameEngine::cleanup_opengl_resources() {
  476. qInfo() << "Cleaning up OpenGL resources...";
  477. QOpenGLContext *context = QOpenGLContext::currentContext();
  478. const bool has_valid_context = (context != nullptr);
  479. if (!has_valid_context) {
  480. qInfo() << "No valid OpenGL context, skipping OpenGL cleanup";
  481. }
  482. if (m_renderer && has_valid_context) {
  483. m_renderer->shutdown();
  484. qInfo() << "Renderer shut down";
  485. }
  486. m_passes.clear();
  487. m_ground.reset();
  488. m_terrain.reset();
  489. m_biome.reset();
  490. m_river.reset();
  491. m_road.reset();
  492. m_riverbank.reset();
  493. m_bridge.reset();
  494. m_fog.reset();
  495. m_stone.reset();
  496. m_plant.reset();
  497. m_pine.reset();
  498. m_olive.reset();
  499. m_firecamp.reset();
  500. m_rain.reset();
  501. m_rainManager.reset();
  502. m_renderer.reset();
  503. m_resources.reset();
  504. qInfo() << "OpenGL resources cleaned up";
  505. }
  506. void GameEngine::on_map_clicked(qreal sx, qreal sy) {
  507. if (m_window == nullptr) {
  508. return;
  509. }
  510. ensure_initialized();
  511. if (m_input_handler) {
  512. m_input_handler->on_map_clicked(sx, sy, m_runtime.local_owner_id,
  513. m_viewport);
  514. }
  515. }
  516. void GameEngine::on_right_click(qreal sx, qreal sy) {
  517. if (m_window == nullptr) {
  518. return;
  519. }
  520. ensure_initialized();
  521. if (m_input_handler) {
  522. m_input_handler->on_right_click(sx, sy, m_runtime.local_owner_id,
  523. m_viewport);
  524. }
  525. }
  526. void GameEngine::on_right_double_click(qreal sx, qreal sy) {
  527. if (m_window == nullptr) {
  528. return;
  529. }
  530. ensure_initialized();
  531. if (m_input_handler) {
  532. m_input_handler->on_right_double_click(sx, sy, m_runtime.local_owner_id,
  533. m_viewport);
  534. }
  535. }
  536. void GameEngine::on_attack_click(qreal sx, qreal sy) {
  537. if (m_window == nullptr) {
  538. return;
  539. }
  540. ensure_initialized();
  541. if (m_input_handler) {
  542. m_input_handler->on_attack_click(sx, sy, m_viewport);
  543. }
  544. }
  545. void GameEngine::reset_movement(Engine::Core::Entity *entity) {
  546. InputCommandHandler::reset_movement(entity);
  547. }
  548. void GameEngine::on_stop_command() {
  549. if (!m_input_handler) {
  550. return;
  551. }
  552. ensure_initialized();
  553. m_input_handler->on_stop_command();
  554. }
  555. void GameEngine::on_hold_command() {
  556. if (!m_input_handler) {
  557. return;
  558. }
  559. ensure_initialized();
  560. m_input_handler->on_hold_command();
  561. }
  562. void GameEngine::on_guard_command() {
  563. if (!m_input_handler) {
  564. return;
  565. }
  566. ensure_initialized();
  567. m_input_handler->on_guard_command();
  568. }
  569. void GameEngine::on_formation_command() {
  570. if (!m_input_handler) {
  571. return;
  572. }
  573. ensure_initialized();
  574. m_input_handler->on_formation_command();
  575. }
  576. void GameEngine::on_run_command() {
  577. if (!m_input_handler) {
  578. return;
  579. }
  580. ensure_initialized();
  581. m_input_handler->on_run_command();
  582. }
  583. void GameEngine::on_heal_command() {
  584. if (!m_cursor_manager) {
  585. return;
  586. }
  587. ensure_initialized();
  588. m_cursor_manager->set_mode(CursorMode::Heal);
  589. }
  590. void GameEngine::on_build_command() {
  591. if (!m_cursor_manager) {
  592. return;
  593. }
  594. ensure_initialized();
  595. m_cursor_manager->set_mode(CursorMode::Build);
  596. }
  597. void GameEngine::on_guard_click(qreal sx, qreal sy) {
  598. if (!m_input_handler || !m_camera) {
  599. return;
  600. }
  601. ensure_initialized();
  602. m_input_handler->on_guard_click(sx, sy, m_viewport);
  603. }
  604. auto GameEngine::any_selected_in_hold_mode() const -> bool {
  605. if (!m_input_handler) {
  606. return false;
  607. }
  608. return m_input_handler->any_selected_in_hold_mode();
  609. }
  610. auto GameEngine::any_selected_in_guard_mode() const -> bool {
  611. if (!m_input_handler) {
  612. return false;
  613. }
  614. return m_input_handler->any_selected_in_guard_mode();
  615. }
  616. auto GameEngine::any_selected_in_formation_mode() const -> bool {
  617. if (!m_input_handler) {
  618. return false;
  619. }
  620. return m_input_handler->any_selected_in_formation_mode();
  621. }
  622. auto GameEngine::any_selected_in_run_mode() const -> bool {
  623. if (!m_input_handler) {
  624. return false;
  625. }
  626. return m_input_handler->any_selected_in_run_mode();
  627. }
  628. auto GameEngine::is_placing_formation() const -> bool {
  629. if (m_commandController) {
  630. return m_commandController->is_placing_formation();
  631. }
  632. return false;
  633. }
  634. bool GameEngine::is_campaign_mission() const {
  635. if (!m_campaign_manager) {
  636. return false;
  637. }
  638. return m_campaign_manager->current_mission_context().is_campaign();
  639. }
  640. void GameEngine::on_formation_mouse_move(qreal sx, qreal sy) {
  641. if (!m_input_handler) {
  642. return;
  643. }
  644. ensure_initialized();
  645. m_input_handler->on_formation_mouse_move(sx, sy, m_viewport);
  646. }
  647. void GameEngine::on_formation_scroll(float delta) {
  648. if (!m_input_handler) {
  649. return;
  650. }
  651. ensure_initialized();
  652. m_input_handler->on_formation_scroll(delta);
  653. }
  654. void GameEngine::on_formation_confirm() {
  655. if (!m_input_handler) {
  656. return;
  657. }
  658. ensure_initialized();
  659. m_input_handler->on_formation_confirm();
  660. }
  661. void GameEngine::on_formation_cancel() {
  662. if (!m_input_handler) {
  663. return;
  664. }
  665. ensure_initialized();
  666. m_input_handler->on_formation_cancel();
  667. }
  668. auto GameEngine::is_placing_construction() const -> bool {
  669. return m_production_manager ? m_production_manager->is_placing_construction()
  670. : false;
  671. }
  672. void GameEngine::on_construction_mouse_move(qreal sx, qreal sy) {
  673. ensure_initialized();
  674. if (m_production_manager) {
  675. m_production_manager->on_construction_mouse_move(sx, sy, m_viewport);
  676. }
  677. }
  678. void GameEngine::on_construction_confirm() {
  679. ensure_initialized();
  680. if (m_production_manager) {
  681. m_production_manager->on_construction_confirm();
  682. }
  683. }
  684. void GameEngine::on_construction_cancel() {
  685. if (m_production_manager) {
  686. m_production_manager->on_construction_cancel();
  687. }
  688. }
  689. void GameEngine::on_patrol_click(qreal sx, qreal sy) {
  690. if (!m_input_handler || !m_camera) {
  691. return;
  692. }
  693. ensure_initialized();
  694. m_input_handler->on_patrol_click(sx, sy, m_viewport);
  695. }
  696. void GameEngine::update_cursor(Qt::CursorShape newCursor) {
  697. if (m_window == nullptr) {
  698. return;
  699. }
  700. if (m_runtime.current_cursor != newCursor) {
  701. m_runtime.current_cursor = newCursor;
  702. m_window->setCursor(newCursor);
  703. }
  704. }
  705. void GameEngine::set_error(const QString &errorMessage) {
  706. if (m_runtime.last_error != errorMessage) {
  707. m_runtime.last_error = errorMessage;
  708. qCritical() << "GameEngine error:" << errorMessage;
  709. emit last_error_changed();
  710. }
  711. }
  712. void GameEngine::set_cursor_mode(CursorMode mode) {
  713. if (!m_cursor_manager) {
  714. return;
  715. }
  716. m_cursor_manager->set_mode(mode);
  717. m_cursor_manager->update_cursor_shape(m_window);
  718. }
  719. void GameEngine::set_cursor_mode(const QString &mode) {
  720. set_cursor_mode(CursorModeUtils::fromString(mode));
  721. }
  722. auto GameEngine::cursor_mode() const -> QString {
  723. if (!m_cursor_manager) {
  724. return "normal";
  725. }
  726. return m_cursor_manager->mode_string();
  727. }
  728. auto GameEngine::global_cursor_x() const -> qreal {
  729. if (!m_cursor_manager) {
  730. return 0;
  731. }
  732. return m_cursor_manager->global_cursor_x(m_window);
  733. }
  734. auto GameEngine::global_cursor_y() const -> qreal {
  735. if (!m_cursor_manager) {
  736. return 0;
  737. }
  738. return m_cursor_manager->global_cursor_y(m_window);
  739. }
  740. void GameEngine::set_hover_at_screen(qreal sx, qreal sy) {
  741. if (m_window == nullptr) {
  742. return;
  743. }
  744. ensure_initialized();
  745. if (m_input_handler) {
  746. m_input_handler->set_hover_at_screen(sx, sy, m_viewport);
  747. }
  748. }
  749. void GameEngine::on_click_select(qreal sx, qreal sy, bool additive) {
  750. if (m_window == nullptr) {
  751. return;
  752. }
  753. ensure_initialized();
  754. if (m_input_handler) {
  755. m_input_handler->on_click_select(sx, sy, additive, m_runtime.local_owner_id,
  756. m_viewport);
  757. }
  758. }
  759. void GameEngine::on_area_selected(qreal x1, qreal y1, qreal x2, qreal y2,
  760. bool additive) {
  761. if (m_window == nullptr) {
  762. return;
  763. }
  764. ensure_initialized();
  765. if (m_input_handler) {
  766. m_input_handler->on_area_selected(x1, y1, x2, y2, additive,
  767. m_runtime.local_owner_id, m_viewport);
  768. }
  769. }
  770. void GameEngine::select_all_troops() {
  771. ensure_initialized();
  772. if (m_input_handler) {
  773. m_input_handler->select_all_troops(m_runtime.local_owner_id);
  774. }
  775. }
  776. void GameEngine::select_unit_by_id(int unitId) {
  777. ensure_initialized();
  778. if (m_input_handler) {
  779. m_input_handler->select_unit_by_id(unitId, m_runtime.local_owner_id);
  780. }
  781. }
  782. void GameEngine::ensure_initialized() {
  783. QString error;
  784. Game::Map::WorldBootstrap::ensure_initialized(
  785. m_runtime.initialized, *m_renderer, *m_camera, m_ground.get(), &error);
  786. if (!error.isEmpty()) {
  787. set_error(error);
  788. }
  789. }
  790. auto GameEngine::enemy_troops_defeated() const -> int {
  791. return m_enemyTroopsDefeated;
  792. }
  793. auto GameEngine::get_player_stats(int owner_id) -> QVariantMap {
  794. QVariantMap result;
  795. auto &stats_registry = Game::Systems::GlobalStatsRegistry::instance();
  796. const auto *stats = stats_registry.get_stats(owner_id);
  797. if (stats != nullptr) {
  798. result["troopsRecruited"] = stats->troops_recruited;
  799. result["enemiesKilled"] = stats->enemies_killed;
  800. result["losses"] = stats->losses;
  801. result["barracksOwned"] = stats->barracks_owned;
  802. result["playTimeSec"] = stats->play_time_sec;
  803. result["gameEnded"] = stats->game_ended;
  804. } else {
  805. result["troopsRecruited"] = 0;
  806. result["enemiesKilled"] = 0;
  807. result["losses"] = 0;
  808. result["barracksOwned"] = 0;
  809. result["playTimeSec"] = 0.0F;
  810. result["gameEnded"] = false;
  811. }
  812. return result;
  813. }
  814. void GameEngine::update(float dt) {
  815. if (m_runtime.loading) {
  816. return;
  817. }
  818. if (m_runtime.paused) {
  819. dt = 0.0F;
  820. } else {
  821. dt *= m_runtime.time_scale;
  822. }
  823. if (!m_runtime.paused && !m_runtime.loading) {
  824. if (m_ambient_state_manager) {
  825. m_ambient_state_manager->update(dt, m_world.get(),
  826. m_runtime.local_owner_id, m_entity_cache,
  827. m_runtime.victory_state);
  828. }
  829. }
  830. if (m_renderer) {
  831. m_renderer->update_animation_time(dt);
  832. }
  833. if (m_camera) {
  834. m_camera->update(dt);
  835. }
  836. if (m_world) {
  837. m_world->update(dt);
  838. auto &visibility_service = Game::Map::VisibilityService::instance();
  839. if (visibility_service.is_initialized() && !m_level.is_spectator_mode) {
  840. m_runtime.visibility_update_accumulator += dt;
  841. const float visibility_update_interval =
  842. Game::GameConfig::instance().gameplay().visibility_update_interval;
  843. if (m_runtime.visibility_update_accumulator >=
  844. visibility_update_interval) {
  845. m_runtime.visibility_update_accumulator = 0.0F;
  846. visibility_service.update(*m_world, m_runtime.local_owner_id);
  847. }
  848. const auto new_version = visibility_service.version();
  849. if (new_version != m_runtime.visibility_version) {
  850. if (m_fog) {
  851. m_fog->update_mask(visibility_service.getWidth(),
  852. visibility_service.getHeight(),
  853. visibility_service.getTileSize(),
  854. visibility_service.snapshotCells());
  855. }
  856. m_runtime.visibility_version = new_version;
  857. }
  858. }
  859. if (m_minimap_manager) {
  860. if (!m_level.is_spectator_mode) {
  861. m_minimap_manager->update_fog(dt, m_runtime.local_owner_id);
  862. }
  863. auto *selection_system =
  864. m_world->get_system<Game::Systems::SelectionSystem>();
  865. m_minimap_manager->update_units(m_world.get(), selection_system,
  866. m_runtime.local_owner_id);
  867. m_minimap_manager->update_camera_viewport(
  868. m_camera.get(), static_cast<float>(m_viewport.width),
  869. static_cast<float>(m_viewport.height));
  870. if (m_minimap_manager->consume_dirty_flag()) {
  871. emit minimap_image_changed();
  872. }
  873. }
  874. }
  875. if (m_rainManager) {
  876. m_rainManager->update(dt);
  877. if (m_rain) {
  878. m_rain->set_enabled(m_rainManager->is_enabled());
  879. m_rain->set_intensity(m_rainManager->get_intensity());
  880. m_rain->set_weather_type(m_rainManager->get_weather_type());
  881. m_rain->set_wind_strength(m_rainManager->get_wind_strength());
  882. if (m_camera) {
  883. m_rain->set_camera_position(m_camera->get_position());
  884. }
  885. }
  886. }
  887. if (m_victoryService && m_world) {
  888. m_victoryService->update(*m_world, dt);
  889. }
  890. if (m_camera_controller) {
  891. m_camera_controller->update_follow(m_followSelectionEnabled);
  892. }
  893. if (m_selectedUnitsModel != nullptr) {
  894. auto *selection_system =
  895. m_world->get_system<Game::Systems::SelectionSystem>();
  896. if ((selection_system != nullptr) &&
  897. !selection_system->get_selected_units().empty()) {
  898. m_runtime.selection_refresh_counter++;
  899. if (m_runtime.selection_refresh_counter >= 15) {
  900. m_runtime.selection_refresh_counter = 0;
  901. emit selected_units_data_changed();
  902. }
  903. }
  904. }
  905. }
  906. void GameEngine::render(int pixelWidth, int pixelHeight) {
  907. if (!m_renderer || !m_world || !m_runtime.initialized || m_runtime.loading) {
  908. return;
  909. }
  910. Game::Systems::CameraVisibilityService::instance().set_camera(m_camera.get());
  911. if (pixelWidth > 0 && pixelHeight > 0) {
  912. m_viewport.width = pixelWidth;
  913. m_viewport.height = pixelHeight;
  914. m_renderer->set_viewport(pixelWidth, pixelHeight);
  915. }
  916. if (auto *selection_system =
  917. m_world->get_system<Game::Systems::SelectionSystem>()) {
  918. const auto &sel = selection_system->get_selected_units();
  919. std::vector<unsigned int> const ids(sel.begin(), sel.end());
  920. m_renderer->set_selected_entities(ids);
  921. }
  922. m_renderer->begin_frame();
  923. if (auto *res = m_renderer->resources()) {
  924. for (auto *pass : m_passes) {
  925. if (pass != nullptr) {
  926. pass->submit(*m_renderer, res);
  927. }
  928. }
  929. }
  930. if (m_renderer && m_hoverTracker) {
  931. m_renderer->set_hovered_entity_id(m_hoverTracker->getLastHoveredEntity());
  932. }
  933. if (m_renderer) {
  934. m_renderer->set_local_owner_id(m_runtime.local_owner_id);
  935. }
  936. m_renderer->render_world(m_world.get());
  937. render_game_effects();
  938. m_renderer->end_frame();
  939. update_loading_overlay();
  940. update_cursor_position();
  941. }
  942. void GameEngine::render_game_effects() {
  943. auto *res = m_renderer->resources();
  944. if (!res) {
  945. return;
  946. }
  947. if (auto *arrow_system = m_world->get_system<Game::Systems::ArrowSystem>()) {
  948. Render::GL::render_arrows(m_renderer.get(), res, *arrow_system);
  949. }
  950. if (auto *projectile_system =
  951. m_world->get_system<Game::Systems::ProjectileSystem>()) {
  952. Render::GL::render_projectiles(m_renderer.get(), res, *projectile_system);
  953. }
  954. if (auto *healing_beam_system =
  955. m_world->get_system<Game::Systems::HealingBeamSystem>()) {
  956. if (auto *res = m_renderer->resources()) {
  957. Render::GL::render_healing_beams(m_renderer.get(), res,
  958. *healing_beam_system);
  959. Render::GL::render_healing_waves(m_renderer.get(), res,
  960. *healing_beam_system);
  961. }
  962. }
  963. Render::GL::render_healer_auras(m_renderer.get(), res, m_world.get());
  964. Render::GL::render_combat_dust(m_renderer.get(), res, m_world.get());
  965. std::optional<QVector3D> preview_waypoint;
  966. if (m_commandController && m_commandController->has_patrol_first_waypoint()) {
  967. preview_waypoint = m_commandController->get_patrol_first_waypoint();
  968. }
  969. Render::GL::render_patrol_flags(m_renderer.get(), res, *m_world,
  970. preview_waypoint);
  971. if (m_commandController && m_commandController->is_placing_formation()) {
  972. Render::GL::FormationPlacementInfo placement;
  973. placement.position =
  974. m_commandController->get_formation_placement_position();
  975. placement.angle_degrees =
  976. m_commandController->get_formation_placement_angle();
  977. placement.active = true;
  978. Render::GL::render_formation_arrow(m_renderer.get(), res, placement);
  979. }
  980. }
  981. void GameEngine::update_loading_overlay() {
  982. if (!m_loading_overlay_wait_for_first_frame) {
  983. return;
  984. }
  985. if (!m_renderer || !m_renderer->resources()) {
  986. m_loading_overlay_frames_remaining = 5;
  987. m_loading_overlay_timer.restart();
  988. return;
  989. }
  990. if (m_loading_overlay_frames_remaining > 0) {
  991. m_loading_overlay_frames_remaining--;
  992. }
  993. constexpr qint64 k_loading_overlay_max_wait_ms = 15000;
  994. const qint64 elapsed_ms =
  995. m_loading_overlay_timer.isValid() ? m_loading_overlay_timer.elapsed() : 0;
  996. const bool enough_time = m_loading_overlay_timer.isValid() &&
  997. (elapsed_ms >= m_loading_overlay_min_duration_ms);
  998. const bool exceeded_max_wait = m_loading_overlay_timer.isValid() &&
  999. (elapsed_ms >= k_loading_overlay_max_wait_ms);
  1000. QStringList pending_components;
  1001. const bool biome_ready = !m_biome || m_biome->is_gpu_ready();
  1002. const bool pine_ready = !m_pine || m_pine->is_gpu_ready();
  1003. const bool olive_ready = !m_olive || m_olive->is_gpu_ready();
  1004. const bool plant_ready = !m_plant || m_plant->is_gpu_ready();
  1005. const bool stone_ready = !m_stone || m_stone->is_gpu_ready();
  1006. const bool firecamp_ready = !m_firecamp || m_firecamp->is_gpu_ready();
  1007. if (!biome_ready) {
  1008. pending_components << QStringLiteral("biome");
  1009. }
  1010. if (!pine_ready) {
  1011. pending_components << QStringLiteral("pine");
  1012. }
  1013. if (!olive_ready) {
  1014. pending_components << QStringLiteral("olive");
  1015. }
  1016. if (!plant_ready) {
  1017. pending_components << QStringLiteral("plant");
  1018. }
  1019. if (!stone_ready) {
  1020. pending_components << QStringLiteral("stone");
  1021. }
  1022. if (!firecamp_ready) {
  1023. pending_components << QStringLiteral("firecamp");
  1024. }
  1025. const bool biome_gpu_ready = pending_components.isEmpty();
  1026. if (enough_time && m_loading_overlay_frames_remaining <= 0 &&
  1027. (biome_gpu_ready || exceeded_max_wait)) {
  1028. if (exceeded_max_wait && !biome_gpu_ready) {
  1029. qWarning() << "Loading overlay timed out waiting for GPU readiness"
  1030. << pending_components.join(", ");
  1031. }
  1032. m_loading_overlay_wait_for_first_frame = false;
  1033. m_loading_overlay_active = false;
  1034. if (m_finalize_progress_after_overlay && m_loading_progress_tracker) {
  1035. m_loading_progress_tracker->set_stage(
  1036. LoadingProgressTracker::LoadingStage::COMPLETED);
  1037. }
  1038. m_finalize_progress_after_overlay = false;
  1039. emit is_loading_changed();
  1040. if (m_show_objectives_after_loading) {
  1041. m_show_objectives_after_loading = false;
  1042. emit campaign_mission_changed();
  1043. }
  1044. }
  1045. }
  1046. void GameEngine::update_cursor_position() {
  1047. qreal const current_x = global_cursor_x();
  1048. qreal const current_y = global_cursor_y();
  1049. if (current_x != m_runtime.last_cursor_x ||
  1050. current_y != m_runtime.last_cursor_y) {
  1051. m_runtime.last_cursor_x = current_x;
  1052. m_runtime.last_cursor_y = current_y;
  1053. emit global_cursor_changed();
  1054. }
  1055. }
  1056. auto GameEngine::screen_to_ground(const QPointF &screenPt,
  1057. QVector3D &outWorld) -> bool {
  1058. return App::Utils::screen_to_ground(m_pickingService.get(), m_camera.get(),
  1059. m_window, m_viewport.width,
  1060. m_viewport.height, screenPt, outWorld);
  1061. }
  1062. auto GameEngine::world_to_screen(const QVector3D &world,
  1063. QPointF &outScreen) const -> bool {
  1064. return App::Utils::world_to_screen(m_pickingService.get(), m_camera.get(),
  1065. m_window, m_viewport.width,
  1066. m_viewport.height, world, outScreen);
  1067. }
  1068. void GameEngine::sync_selection_flags() {
  1069. auto *selection_system =
  1070. m_world->get_system<Game::Systems::SelectionSystem>();
  1071. if (!m_world || (selection_system == nullptr)) {
  1072. return;
  1073. }
  1074. App::Utils::sanitize_selection(m_world.get(), selection_system);
  1075. if (selection_system->get_selected_units().empty()) {
  1076. if (m_cursor_manager && m_cursor_manager->mode() != CursorMode::Normal) {
  1077. set_cursor_mode(CursorMode::Normal);
  1078. }
  1079. }
  1080. }
  1081. void GameEngine::camera_move(float dx, float dz) {
  1082. ensure_initialized();
  1083. if (m_camera_controller) {
  1084. m_camera_controller->move(dx, dz);
  1085. }
  1086. }
  1087. void GameEngine::camera_elevate(float dy) {
  1088. ensure_initialized();
  1089. if (m_camera_controller) {
  1090. m_camera_controller->elevate(dy);
  1091. }
  1092. }
  1093. void GameEngine::reset_camera() {
  1094. ensure_initialized();
  1095. if (m_camera_controller) {
  1096. m_camera_controller->reset(m_runtime.local_owner_id, m_level);
  1097. }
  1098. }
  1099. void GameEngine::camera_zoom(float delta) {
  1100. ensure_initialized();
  1101. if (m_camera_controller) {
  1102. m_camera_controller->zoom(delta);
  1103. }
  1104. }
  1105. auto GameEngine::camera_distance() const -> float {
  1106. if (m_camera_controller) {
  1107. return m_camera_controller->distance();
  1108. }
  1109. return 0.0F;
  1110. }
  1111. void GameEngine::camera_yaw(float degrees) {
  1112. ensure_initialized();
  1113. if (m_camera_controller) {
  1114. m_camera_controller->yaw(degrees);
  1115. }
  1116. }
  1117. void GameEngine::camera_orbit(float yaw_deg, float pitch_deg) {
  1118. ensure_initialized();
  1119. if (m_camera_controller) {
  1120. m_camera_controller->orbit(yaw_deg, pitch_deg);
  1121. }
  1122. }
  1123. void GameEngine::camera_orbit_direction(int direction, bool shift) {
  1124. if (m_camera_controller) {
  1125. m_camera_controller->orbit_direction(direction, shift);
  1126. }
  1127. }
  1128. void GameEngine::camera_follow_selection(bool enable) {
  1129. ensure_initialized();
  1130. m_followSelectionEnabled = enable;
  1131. if (m_camera_controller) {
  1132. m_camera_controller->follow_selection(enable);
  1133. }
  1134. }
  1135. void GameEngine::camera_set_follow_lerp(float alpha) {
  1136. ensure_initialized();
  1137. if (m_camera_controller) {
  1138. m_camera_controller->set_follow_lerp(alpha);
  1139. }
  1140. }
  1141. void GameEngine::on_minimap_left_click(qreal mx, qreal my, qreal minimap_width,
  1142. qreal minimap_height) {
  1143. ensure_initialized();
  1144. if (!m_camera || !m_minimap_manager || !m_minimap_manager->has_minimap()) {
  1145. return;
  1146. }
  1147. const QImage &minimap_img = m_minimap_manager->get_image();
  1148. if (minimap_img.isNull()) {
  1149. return;
  1150. }
  1151. const float img_width = static_cast<float>(minimap_img.width());
  1152. const float img_height = static_cast<float>(minimap_img.height());
  1153. const float px =
  1154. (static_cast<float>(mx) / static_cast<float>(minimap_width)) * img_width;
  1155. const float py =
  1156. (static_cast<float>(my) / static_cast<float>(minimap_height)) *
  1157. img_height;
  1158. const auto [world_x, world_z] = Game::Map::Minimap::pixel_to_world(
  1159. px, py, m_minimap_manager->get_world_width(),
  1160. m_minimap_manager->get_world_height(), img_width, img_height,
  1161. m_minimap_manager->get_tile_size());
  1162. if (m_camera) {
  1163. const QVector3D new_target(world_x, 0.0F, world_z);
  1164. const QVector3D current_target = m_camera->get_target();
  1165. const QVector3D current_position = m_camera->get_position();
  1166. const QVector3D offset = current_position - current_target;
  1167. m_camera->look_at(new_target + offset, new_target,
  1168. m_camera->get_up_vector());
  1169. }
  1170. m_followSelectionEnabled = false;
  1171. if (m_camera_controller) {
  1172. m_camera_controller->follow_selection(false);
  1173. }
  1174. }
  1175. void GameEngine::on_minimap_right_click(qreal mx, qreal my, qreal minimap_width,
  1176. qreal minimap_height) {
  1177. ensure_initialized();
  1178. if (m_level.is_spectator_mode || !m_world || !m_minimap_manager ||
  1179. !m_minimap_manager->has_minimap()) {
  1180. return;
  1181. }
  1182. const QImage &minimap_img = m_minimap_manager->get_image();
  1183. if (minimap_img.isNull()) {
  1184. return;
  1185. }
  1186. const float img_width = static_cast<float>(minimap_img.width());
  1187. const float img_height = static_cast<float>(minimap_img.height());
  1188. const float px =
  1189. (static_cast<float>(mx) / static_cast<float>(minimap_width)) * img_width;
  1190. const float py =
  1191. (static_cast<float>(my) / static_cast<float>(minimap_height)) *
  1192. img_height;
  1193. const auto [world_x, world_z] = Game::Map::Minimap::pixel_to_world(
  1194. px, py, m_minimap_manager->get_world_width(),
  1195. m_minimap_manager->get_world_height(), img_width, img_height,
  1196. m_minimap_manager->get_tile_size());
  1197. auto *selection_system =
  1198. m_world->get_system<Game::Systems::SelectionSystem>();
  1199. if (!selection_system) {
  1200. return;
  1201. }
  1202. const auto &selected = selection_system->get_selected_units();
  1203. if (selected.empty()) {
  1204. return;
  1205. }
  1206. const QVector3D target_pos(world_x, 0.0F, world_z);
  1207. auto targets = Game::Systems::FormationPlanner::spread_formation_by_nation(
  1208. *m_world, selected, target_pos,
  1209. Game::GameConfig::instance().gameplay().formation_spacing_default);
  1210. Game::Systems::CommandService::MoveOptions opts;
  1211. opts.group_move = selected.size() > 1;
  1212. Game::Systems::CommandService::move_units(*m_world, selected, targets, opts);
  1213. }
  1214. auto GameEngine::selected_units_model() -> QAbstractItemModel * {
  1215. return m_selectedUnitsModel;
  1216. }
  1217. auto GameEngine::audio_system() -> QObject * {
  1218. return m_audio_systemProxy.get();
  1219. }
  1220. auto GameEngine::has_units_selected() const -> bool {
  1221. if (!m_selectionController) {
  1222. return false;
  1223. }
  1224. return m_selectionController->has_units_selected();
  1225. }
  1226. auto GameEngine::player_troop_count() const -> int {
  1227. return m_entity_cache.player_troop_count;
  1228. }
  1229. auto GameEngine::has_selected_type(const QString &type) const -> bool {
  1230. if (!m_selectionController) {
  1231. return false;
  1232. }
  1233. return m_selectionController->has_selected_type(type);
  1234. }
  1235. void GameEngine::recruit_near_selected(const QString &unit_type) {
  1236. ensure_initialized();
  1237. if (!m_commandController) {
  1238. return;
  1239. }
  1240. m_commandController->recruit_near_selected(unit_type,
  1241. m_runtime.local_owner_id);
  1242. }
  1243. void GameEngine::start_building_placement(const QString &building_type) {
  1244. ensure_initialized();
  1245. if (m_production_manager) {
  1246. m_production_manager->start_building_placement(building_type);
  1247. set_cursor_mode(CursorMode::PlaceBuilding);
  1248. }
  1249. }
  1250. void GameEngine::place_building_at_screen(qreal sx, qreal sy) {
  1251. ensure_initialized();
  1252. if (m_production_manager) {
  1253. m_production_manager->place_building_at_screen(
  1254. sx, sy, m_runtime.local_owner_id, m_viewport);
  1255. set_cursor_mode(CursorMode::Normal);
  1256. }
  1257. }
  1258. void GameEngine::cancel_building_placement() {
  1259. if (m_production_manager) {
  1260. m_production_manager->cancel_building_placement();
  1261. }
  1262. set_cursor_mode(CursorMode::Normal);
  1263. }
  1264. auto GameEngine::pending_building_type() const -> QString {
  1265. return m_production_manager ? m_production_manager->pending_building_type()
  1266. : QString();
  1267. }
  1268. auto GameEngine::get_selected_production_state() const -> QVariantMap {
  1269. return m_production_manager
  1270. ? m_production_manager->get_selected_production_state(
  1271. m_runtime.local_owner_id)
  1272. : QVariantMap();
  1273. }
  1274. auto GameEngine::get_unit_production_info(
  1275. const QString &unit_type, const QString &nation_id) const -> QVariantMap {
  1276. return m_production_manager ? m_production_manager->get_unit_production_info(
  1277. unit_type, nation_id)
  1278. : QVariantMap();
  1279. }
  1280. auto GameEngine::get_selected_builder_production_state() const -> QVariantMap {
  1281. return m_production_manager
  1282. ? m_production_manager->get_selected_builder_production_state()
  1283. : QVariantMap();
  1284. }
  1285. void GameEngine::start_builder_construction(const QString &item_type) {
  1286. if (m_production_manager) {
  1287. m_production_manager->start_builder_construction(item_type);
  1288. }
  1289. }
  1290. auto GameEngine::get_selected_units_command_mode() const -> QString {
  1291. return m_selection_query_service
  1292. ? m_selection_query_service->get_selected_units_command_mode()
  1293. : "normal";
  1294. }
  1295. auto GameEngine::get_selected_units_mode_availability() const -> QVariantMap {
  1296. return m_selection_query_service
  1297. ? m_selection_query_service->get_selected_units_mode_availability()
  1298. : QVariantMap();
  1299. }
  1300. void GameEngine::set_rally_at_screen(qreal sx, qreal sy) {
  1301. ensure_initialized();
  1302. if (m_production_manager) {
  1303. m_production_manager->set_rally_at_screen(sx, sy, m_runtime.local_owner_id,
  1304. m_viewport);
  1305. }
  1306. }
  1307. void GameEngine::start_loading_maps() {
  1308. m_available_maps.clear();
  1309. if (m_mapCatalog) {
  1310. m_mapCatalog->load_maps_async();
  1311. }
  1312. load_campaigns();
  1313. }
  1314. auto GameEngine::available_maps() const -> QVariantList {
  1315. return m_available_maps;
  1316. }
  1317. auto GameEngine::available_nations() const -> QVariantList {
  1318. QVariantList nations;
  1319. const auto &registry = Game::Systems::NationRegistry::instance();
  1320. const auto &all = registry.get_all_nations();
  1321. QList<QVariantMap> ordered;
  1322. ordered.reserve(static_cast<int>(all.size()));
  1323. for (const auto &nation : all) {
  1324. QVariantMap entry;
  1325. entry.insert(
  1326. QStringLiteral("id"),
  1327. QString::fromStdString(Game::Systems::nation_id_to_string(nation.id)));
  1328. entry.insert(QStringLiteral("name"),
  1329. QString::fromStdString(nation.display_name));
  1330. ordered.append(entry);
  1331. }
  1332. std::sort(ordered.begin(), ordered.end(),
  1333. [](const QVariantMap &a, const QVariantMap &b) {
  1334. return a.value(QStringLiteral("name"))
  1335. .toString()
  1336. .localeAwareCompare(
  1337. b.value(QStringLiteral("name")).toString()) < 0;
  1338. });
  1339. for (const auto &entry : ordered) {
  1340. nations.append(entry);
  1341. }
  1342. return nations;
  1343. }
  1344. auto GameEngine::available_campaigns() const -> QVariantList {
  1345. return m_campaign_manager ? m_campaign_manager->available_campaigns()
  1346. : QVariantList();
  1347. }
  1348. void GameEngine::load_campaigns() {
  1349. if (!m_saveLoadService) {
  1350. return;
  1351. }
  1352. QString error;
  1353. auto campaigns = m_saveLoadService->list_campaigns(&error);
  1354. if (!error.isEmpty()) {
  1355. qWarning() << "Failed to load campaigns:" << error;
  1356. return;
  1357. }
  1358. if (m_campaign_manager) {
  1359. m_campaign_manager->set_available_campaigns(campaigns);
  1360. }
  1361. }
  1362. void GameEngine::start_campaign_mission(const QString &mission_path) {
  1363. clear_error();
  1364. if (!m_campaign_manager) {
  1365. set_error("Campaign manager not initialized");
  1366. return;
  1367. }
  1368. m_selected_player_id = 1;
  1369. m_campaign_manager->start_campaign_mission(mission_path,
  1370. m_selected_player_id);
  1371. if (!m_campaign_manager->current_mission_definition().has_value()) {
  1372. set_error("Failed to load mission");
  1373. return;
  1374. }
  1375. const auto &mission = *m_campaign_manager->current_mission_definition();
  1376. QVariantList playerConfigs;
  1377. QVariantMap player1;
  1378. player1.insert("player_id", 1);
  1379. player1.insert("playerName", mission.player_setup.nation);
  1380. player1.insert("colorIndex", 0);
  1381. player1.insert("team_id", 0);
  1382. player1.insert("nationId", mission.player_setup.nation);
  1383. player1.insert("isHuman", true);
  1384. playerConfigs.append(player1);
  1385. int player_id = 2;
  1386. int default_team_id = 1;
  1387. for (const auto &ai_setup : mission.ai_setups) {
  1388. QVariantMap ai_player;
  1389. ai_player.insert("player_id", player_id);
  1390. ai_player.insert("playerName", ai_setup.nation);
  1391. ai_player.insert("colorIndex", player_id - 1);
  1392. int team_id;
  1393. if (ai_setup.team_id.has_value()) {
  1394. team_id = ai_setup.team_id.value();
  1395. } else {
  1396. team_id = default_team_id++;
  1397. }
  1398. ai_player.insert("team_id", team_id);
  1399. ai_player.insert("nationId", ai_setup.nation);
  1400. ai_player.insert("isHuman", false);
  1401. playerConfigs.append(ai_player);
  1402. player_id++;
  1403. }
  1404. start_skirmish_internal(mission.map_path, playerConfigs, false);
  1405. }
  1406. void GameEngine::mark_current_mission_completed() {
  1407. if (!m_campaign_manager) {
  1408. return;
  1409. }
  1410. const QString campaign_id = m_campaign_manager->current_campaign_id();
  1411. if (campaign_id.isEmpty()) {
  1412. qWarning() << "No active campaign mission to mark as completed";
  1413. return;
  1414. }
  1415. if (!m_saveLoadService) {
  1416. qWarning() << "Save/Load service not initialized";
  1417. return;
  1418. }
  1419. QString error;
  1420. bool success =
  1421. m_saveLoadService->mark_campaign_completed(campaign_id, &error);
  1422. if (!success) {
  1423. qWarning() << "Failed to mark campaign as completed:" << error;
  1424. } else {
  1425. m_campaign_manager->mark_current_mission_completed();
  1426. load_campaigns();
  1427. }
  1428. }
  1429. QVariantMap GameEngine::get_current_mission_objectives() const {
  1430. QVariantMap result;
  1431. if (!m_campaign_manager) {
  1432. return result;
  1433. }
  1434. const auto &mission_def = m_campaign_manager->current_mission_definition();
  1435. if (!mission_def.has_value()) {
  1436. return result;
  1437. }
  1438. const auto &mission = *mission_def;
  1439. result["title"] = mission.title;
  1440. result["summary"] = mission.summary;
  1441. result["victory_conditions"] =
  1442. build_condition_list(mission.victory_conditions);
  1443. result["defeat_conditions"] = build_condition_list(mission.defeat_conditions);
  1444. result["optional_objectives"] =
  1445. build_condition_list(mission.optional_objectives);
  1446. return result;
  1447. }
  1448. QVariantMap
  1449. GameEngine::get_mission_definition(const QString &mission_id) const {
  1450. QVariantMap result;
  1451. if (mission_id.isEmpty()) {
  1452. return result;
  1453. }
  1454. const QString mission_path = resolve_mission_file_path(mission_id);
  1455. if (mission_path.isEmpty()) {
  1456. qWarning() << "Mission definition not found for" << mission_id;
  1457. return result;
  1458. }
  1459. Game::Mission::MissionDefinition mission;
  1460. QString error;
  1461. if (!Game::Mission::MissionLoader::loadFromJsonFile(mission_path, mission,
  1462. &error)) {
  1463. qWarning() << "Failed to load mission definition" << mission_id << error;
  1464. return result;
  1465. }
  1466. return build_mission_definition_map(mission);
  1467. }
  1468. void GameEngine::start_skirmish(const QString &map_path,
  1469. const QVariantList &playerConfigs) {
  1470. start_skirmish_internal(map_path, playerConfigs, true);
  1471. }
  1472. void GameEngine::start_skirmish_internal(const QString &map_path,
  1473. const QVariantList &playerConfigs,
  1474. bool set_skirmish_context) {
  1475. clear_error();
  1476. m_level.map_path = map_path;
  1477. m_level.map_name = map_path;
  1478. if (m_campaign_manager && set_skirmish_context) {
  1479. m_campaign_manager->set_skirmish_context(map_path);
  1480. }
  1481. if (!m_runtime.victory_state.isEmpty()) {
  1482. m_runtime.victory_state = "";
  1483. emit victory_state_changed();
  1484. }
  1485. if (m_victoryService) {
  1486. m_victoryService->reset();
  1487. }
  1488. m_enemyTroopsDefeated = 0;
  1489. if (!m_runtime.initialized) {
  1490. ensure_initialized();
  1491. }
  1492. if (!m_world || !m_renderer || !m_camera) {
  1493. set_error("Cannot start skirmish: renderer not initialized");
  1494. return;
  1495. }
  1496. m_finalize_progress_after_overlay = false;
  1497. m_loading_overlay_active = true;
  1498. m_runtime.loading = true;
  1499. emit is_loading_changed();
  1500. if (m_loading_progress_tracker) {
  1501. m_loading_progress_tracker->start_loading();
  1502. }
  1503. QCoreApplication::processEvents(QEventLoop::AllEvents);
  1504. QTimer::singleShot(50, this, [this, map_path, playerConfigs]() {
  1505. perform_skirmish_load(map_path, playerConfigs);
  1506. });
  1507. }
  1508. void GameEngine::perform_skirmish_load(const QString &map_path,
  1509. const QVariantList &playerConfigs) {
  1510. if (!(m_world && m_renderer && m_camera)) {
  1511. set_error("Cannot start skirmish: renderer not initialized");
  1512. m_runtime.loading = false;
  1513. emit is_loading_changed();
  1514. return;
  1515. }
  1516. if (m_hoverTracker) {
  1517. m_hoverTracker->update_hover(-1, -1, *m_world, *m_camera, 0, 0);
  1518. }
  1519. LevelOrchestrator orchestrator;
  1520. LevelOrchestrator::RendererRefs renderers{
  1521. m_renderer.get(), m_camera.get(), m_ground.get(), m_terrain.get(),
  1522. m_biome.get(), m_river.get(), m_road.get(), m_riverbank.get(),
  1523. m_bridge.get(), m_fog.get(), m_stone.get(), m_plant.get(),
  1524. m_pine.get(), m_olive.get(), m_firecamp.get(), m_rain.get()};
  1525. auto visibility_ready = [this]() {
  1526. m_runtime.visibility_version =
  1527. Game::Map::VisibilityService::instance().version();
  1528. m_runtime.visibility_update_accumulator = 0.0F;
  1529. };
  1530. auto owner_update = [this]() { emit owner_info_changed(); };
  1531. const bool allow_default_player_barracks =
  1532. !m_campaign_manager ||
  1533. !m_campaign_manager->current_mission_context().is_campaign();
  1534. auto load_result = orchestrator.load_skirmish(
  1535. map_path, playerConfigs, m_selected_player_id, *m_world, renderers,
  1536. m_level, m_entity_cache, m_victoryService.get(), m_minimap_manager.get(),
  1537. visibility_ready, owner_update, allow_default_player_barracks,
  1538. m_loading_progress_tracker.get());
  1539. if (load_result.updated_player_id != m_selected_player_id) {
  1540. m_selected_player_id = load_result.updated_player_id;
  1541. emit selected_player_id_changed();
  1542. }
  1543. if (!load_result.success) {
  1544. set_error(load_result.error_message);
  1545. m_runtime.loading = false;
  1546. m_loading_overlay_active = false;
  1547. m_loading_overlay_wait_for_first_frame = false;
  1548. m_finalize_progress_after_overlay = false;
  1549. m_show_objectives_after_loading = false;
  1550. emit is_loading_changed();
  1551. return;
  1552. }
  1553. m_runtime.local_owner_id = load_result.updated_player_id;
  1554. apply_mission_setup();
  1555. configure_mission_victory_conditions();
  1556. configure_rain_system();
  1557. finalize_skirmish_load();
  1558. }
  1559. void GameEngine::apply_mission_setup() {
  1560. if (!m_world || !m_campaign_manager) {
  1561. return;
  1562. }
  1563. if (!m_campaign_manager->current_mission_context().is_campaign()) {
  1564. return;
  1565. }
  1566. if (!m_campaign_manager->current_mission_definition().has_value()) {
  1567. return;
  1568. }
  1569. auto reg = Game::Map::MapTransformer::getFactoryRegistry();
  1570. if (!reg) {
  1571. qWarning() << "Mission setup skipped: unit factory registry missing";
  1572. return;
  1573. }
  1574. const auto &mission = *m_campaign_manager->current_mission_definition();
  1575. auto &owner_registry = Game::Systems::OwnerRegistry::instance();
  1576. auto &nation_registry = Game::Systems::NationRegistry::instance();
  1577. Game::Map::MapDefinition map_def;
  1578. QString map_error;
  1579. const QString resolved_map_path =
  1580. Utils::Resources::resolveResourcePath(m_level.map_path);
  1581. bool map_loaded = Game::Map::MapLoader::loadFromJsonFile(resolved_map_path,
  1582. map_def, &map_error);
  1583. if (!map_loaded) {
  1584. qWarning() << "Mission setup: failed to load map definition for"
  1585. << m_level.map_path << "resolved to" << resolved_map_path << "-"
  1586. << map_error;
  1587. }
  1588. bool has_map_spawns = true;
  1589. if (map_loaded) {
  1590. has_map_spawns = !map_def.spawns.empty();
  1591. }
  1592. bool has_mission_spawns = !mission.player_setup.starting_units.empty() ||
  1593. !mission.player_setup.starting_buildings.empty();
  1594. for (const auto &ai_setup : mission.ai_setups) {
  1595. if (!ai_setup.starting_units.empty() ||
  1596. !ai_setup.starting_buildings.empty()) {
  1597. has_mission_spawns = true;
  1598. break;
  1599. }
  1600. }
  1601. if (has_mission_spawns && !has_map_spawns) {
  1602. std::vector<Engine::Core::EntityID> to_remove;
  1603. auto entities = m_world->get_entities_with<Engine::Core::UnitComponent>();
  1604. to_remove.reserve(entities.size());
  1605. for (auto *entity : entities) {
  1606. if (entity != nullptr) {
  1607. to_remove.push_back(entity->get_id());
  1608. }
  1609. }
  1610. for (const auto id : to_remove) {
  1611. m_world->destroy_entity(id);
  1612. }
  1613. }
  1614. auto resolve_nation_id = [&nation_registry](const QString &nation_str) {
  1615. const auto parsed =
  1616. Game::Systems::nation_id_from_string(nation_str.toStdString());
  1617. if (parsed.has_value()) {
  1618. return parsed.value();
  1619. }
  1620. return nation_registry.default_nation_id();
  1621. };
  1622. auto mission_position_to_world = [&](const Game::Mission::Position &pos) {
  1623. float world_x = pos.x;
  1624. float world_z = pos.z;
  1625. if (map_loaded && map_def.coordSystem == Game::Map::CoordSystem::Grid) {
  1626. const float tile = std::max(0.0001F, map_def.grid.tile_size);
  1627. world_x = (pos.x - (map_def.grid.width * 0.5F - 0.5F)) * tile;
  1628. world_z = (pos.z - (map_def.grid.height * 0.5F - 0.5F)) * tile;
  1629. } else if (!map_loaded) {
  1630. const float tile = std::max(0.0001F, m_level.tile_size);
  1631. world_x = (pos.x - (m_level.grid_width * 0.5F - 0.5F)) * tile;
  1632. world_z = (pos.z - (m_level.grid_height * 0.5F - 0.5F)) * tile;
  1633. }
  1634. return QVector3D(world_x, 0.0F, world_z);
  1635. };
  1636. auto apply_team_color = [&](Engine::Core::Entity *entity, int owner_id) {
  1637. if (entity == nullptr) {
  1638. return;
  1639. }
  1640. auto *renderable =
  1641. entity->get_component<Engine::Core::RenderableComponent>();
  1642. if (renderable == nullptr) {
  1643. return;
  1644. }
  1645. const QVector3D team_color = Game::Visuals::team_colorForOwner(owner_id);
  1646. renderable->color[0] = team_color.x();
  1647. renderable->color[1] = team_color.y();
  1648. renderable->color[2] = team_color.z();
  1649. };
  1650. auto parse_color = [](const QString &color_name,
  1651. std::array<float, 3> &out) -> bool {
  1652. if (color_name.isEmpty()) {
  1653. return false;
  1654. }
  1655. const QString trimmed = color_name.trimmed();
  1656. if (trimmed.startsWith('#') && trimmed.length() == 7) {
  1657. bool ok = false;
  1658. int r = trimmed.mid(1, 2).toInt(&ok, 16);
  1659. if (!ok) {
  1660. return false;
  1661. }
  1662. int g = trimmed.mid(3, 2).toInt(&ok, 16);
  1663. if (!ok) {
  1664. return false;
  1665. }
  1666. int b = trimmed.mid(5, 2).toInt(&ok, 16);
  1667. if (!ok) {
  1668. return false;
  1669. }
  1670. constexpr float scale = 255.0F;
  1671. out = {r / scale, g / scale, b / scale};
  1672. return true;
  1673. }
  1674. const QString lowered = trimmed.toLower();
  1675. if (lowered == "red") {
  1676. out = {1.00F, 0.30F, 0.30F};
  1677. return true;
  1678. }
  1679. if (lowered == "brown") {
  1680. out = {0.55F, 0.36F, 0.18F};
  1681. return true;
  1682. }
  1683. if (lowered == "blue") {
  1684. out = {0.20F, 0.55F, 1.00F};
  1685. return true;
  1686. }
  1687. if (lowered == "green") {
  1688. out = {0.20F, 0.80F, 0.40F};
  1689. return true;
  1690. }
  1691. if (lowered == "yellow") {
  1692. out = {1.00F, 0.80F, 0.20F};
  1693. return true;
  1694. }
  1695. if (lowered == "orange") {
  1696. out = {0.95F, 0.55F, 0.10F};
  1697. return true;
  1698. }
  1699. if (lowered == "white") {
  1700. out = {0.95F, 0.95F, 0.95F};
  1701. return true;
  1702. }
  1703. if (lowered == "black") {
  1704. out = {0.15F, 0.15F, 0.15F};
  1705. return true;
  1706. }
  1707. return false;
  1708. };
  1709. auto apply_owner_color = [&](int owner_id, const QString &color_name) {
  1710. std::array<float, 3> color;
  1711. if (!parse_color(color_name, color)) {
  1712. return;
  1713. }
  1714. owner_registry.set_owner_color(owner_id, color[0], color[1], color[2]);
  1715. };
  1716. auto spawn_units_for_owner =
  1717. [&](int owner_id, const Game::Systems::NationID nation_id,
  1718. const std::vector<Game::Mission::UnitSetup> &units) {
  1719. const bool ai_controlled = owner_registry.is_ai(owner_id);
  1720. for (const auto &unit_setup : units) {
  1721. const auto spawn_type =
  1722. Game::Units::spawn_typeFromString(unit_setup.type.toStdString());
  1723. if (!spawn_type.has_value()) {
  1724. qWarning() << "Mission setup: unknown unit type" << unit_setup.type;
  1725. continue;
  1726. }
  1727. const int count = std::max(1, unit_setup.count);
  1728. const int grid =
  1729. static_cast<int>(std::ceil(std::sqrt(static_cast<float>(count))));
  1730. const QVector3D base_pos =
  1731. mission_position_to_world(unit_setup.position);
  1732. float base_tile_size = m_level.tile_size;
  1733. if (map_loaded &&
  1734. map_def.coordSystem == Game::Map::CoordSystem::Grid) {
  1735. base_tile_size = map_def.grid.tile_size;
  1736. }
  1737. const float spacing = std::max(0.5F, base_tile_size * 1.2F);
  1738. for (int i = 0; i < count; ++i) {
  1739. const int row = i / grid;
  1740. const int col = i % grid;
  1741. const float offset_x = (float(col) - (grid - 1) * 0.5F) * spacing;
  1742. const float offset_z = (float(row) - (grid - 1) * 0.5F) * spacing;
  1743. const QVector3D pos = QVector3D(
  1744. base_pos.x() + offset_x, base_pos.y(), base_pos.z() + offset_z);
  1745. Game::Units::SpawnParams sp;
  1746. sp.position = pos;
  1747. sp.player_id = owner_id;
  1748. sp.spawn_type = spawn_type.value();
  1749. sp.ai_controlled = ai_controlled;
  1750. sp.nation_id = nation_id;
  1751. auto unit = reg->create(sp.spawn_type, *m_world, sp);
  1752. if (!unit) {
  1753. qWarning() << "Mission setup: failed to spawn unit"
  1754. << unit_setup.type << "for owner" << owner_id;
  1755. continue;
  1756. }
  1757. apply_team_color(m_world->get_entity(unit->id()), owner_id);
  1758. }
  1759. }
  1760. };
  1761. auto spawn_buildings_for_owner =
  1762. [&](int owner_id, const Game::Systems::NationID nation_id,
  1763. const std::vector<Game::Mission::BuildingSetup> &buildings) {
  1764. const bool ai_controlled = owner_registry.is_ai(owner_id);
  1765. for (const auto &building_setup : buildings) {
  1766. const auto spawn_type = Game::Units::spawn_typeFromString(
  1767. building_setup.type.toStdString());
  1768. if (!spawn_type.has_value()) {
  1769. qWarning() << "Mission setup: unknown building type"
  1770. << building_setup.type;
  1771. continue;
  1772. }
  1773. const QVector3D pos =
  1774. mission_position_to_world(building_setup.position);
  1775. Game::Units::SpawnParams sp;
  1776. sp.position = pos;
  1777. sp.player_id = owner_id;
  1778. sp.spawn_type = spawn_type.value();
  1779. sp.ai_controlled = ai_controlled;
  1780. sp.nation_id = nation_id;
  1781. sp.max_population = building_setup.max_population;
  1782. auto unit = reg->create(sp.spawn_type, *m_world, sp);
  1783. if (!unit) {
  1784. qWarning() << "Mission setup: failed to spawn building"
  1785. << building_setup.type << "for owner" << owner_id;
  1786. continue;
  1787. }
  1788. apply_team_color(m_world->get_entity(unit->id()), owner_id);
  1789. }
  1790. };
  1791. const int local_owner_id = m_runtime.local_owner_id;
  1792. if (owner_registry.get_owner_type(local_owner_id) ==
  1793. Game::Systems::OwnerType::Neutral) {
  1794. owner_registry.register_owner_with_id(
  1795. local_owner_id, Game::Systems::OwnerType::Player,
  1796. "Player " + std::to_string(local_owner_id));
  1797. }
  1798. owner_registry.set_owner_team(local_owner_id, 0);
  1799. const auto player_nation_id = resolve_nation_id(mission.player_setup.nation);
  1800. nation_registry.set_player_nation(local_owner_id, player_nation_id);
  1801. apply_owner_color(local_owner_id, mission.player_setup.color);
  1802. spawn_units_for_owner(local_owner_id, player_nation_id,
  1803. mission.player_setup.starting_units);
  1804. spawn_buildings_for_owner(local_owner_id, player_nation_id,
  1805. mission.player_setup.starting_buildings);
  1806. int ai_owner_id = 2;
  1807. int default_team_id = 1;
  1808. for (const auto &ai_setup : mission.ai_setups) {
  1809. if (owner_registry.get_owner_type(ai_owner_id) ==
  1810. Game::Systems::OwnerType::Neutral) {
  1811. owner_registry.register_owner_with_id(
  1812. ai_owner_id, Game::Systems::OwnerType::AI,
  1813. "AI Player " + std::to_string(ai_owner_id));
  1814. }
  1815. int team_id;
  1816. if (ai_setup.team_id.has_value()) {
  1817. team_id = ai_setup.team_id.value();
  1818. } else {
  1819. team_id = default_team_id++;
  1820. }
  1821. owner_registry.set_owner_team(ai_owner_id, team_id);
  1822. const auto ai_nation_id = resolve_nation_id(ai_setup.nation);
  1823. nation_registry.set_player_nation(ai_owner_id, ai_nation_id);
  1824. apply_owner_color(ai_owner_id, ai_setup.color);
  1825. spawn_units_for_owner(ai_owner_id, ai_nation_id, ai_setup.starting_units);
  1826. spawn_buildings_for_owner(ai_owner_id, ai_nation_id,
  1827. ai_setup.starting_buildings);
  1828. ai_owner_id++;
  1829. }
  1830. auto entities = m_world->get_entities_with<Engine::Core::UnitComponent>();
  1831. for (auto *entity : entities) {
  1832. if (entity == nullptr) {
  1833. continue;
  1834. }
  1835. auto *unit = entity->get_component<Engine::Core::UnitComponent>();
  1836. if (unit == nullptr) {
  1837. continue;
  1838. }
  1839. apply_team_color(entity, unit->owner_id);
  1840. }
  1841. if (auto *ai_system = m_world->get_system<Game::Systems::AISystem>()) {
  1842. ai_system->reinitialize();
  1843. int ai_id = 2;
  1844. for (const auto &ai_setup : mission.ai_setups) {
  1845. Game::Systems::AI::AIStrategy strategy =
  1846. Game::Systems::AI::AIStrategy::Balanced;
  1847. if (ai_setup.strategy.has_value()) {
  1848. strategy = Game::Systems::AI::AIStrategyFactory::parse_strategy(
  1849. ai_setup.strategy.value());
  1850. }
  1851. ai_system->set_ai_strategy(
  1852. ai_id, strategy, ai_setup.personality.aggression,
  1853. ai_setup.personality.defense, ai_setup.personality.harassment);
  1854. ai_id++;
  1855. }
  1856. }
  1857. int prev_selected_player = m_selected_player_id;
  1858. GameStateRestorer::rebuild_registries_after_load(
  1859. m_world.get(), m_selected_player_id, m_level, m_runtime.local_owner_id);
  1860. GameStateRestorer::rebuild_entity_cache(m_world.get(), m_entity_cache,
  1861. m_runtime.local_owner_id);
  1862. if (m_selected_player_id != prev_selected_player) {
  1863. emit selected_player_id_changed();
  1864. }
  1865. emit troop_count_changed();
  1866. emit owner_info_changed();
  1867. }
  1868. void GameEngine::configure_mission_victory_conditions() {
  1869. if (!m_campaign_manager || !m_victoryService) {
  1870. return;
  1871. }
  1872. m_campaign_manager->configure_mission_victory_conditions(
  1873. m_victoryService.get(), m_runtime.local_owner_id);
  1874. m_victoryService->set_victory_callback([this](const QString &state) {
  1875. if (m_runtime.victory_state != state) {
  1876. m_runtime.victory_state = state;
  1877. emit victory_state_changed();
  1878. if (state == "victory" &&
  1879. !m_campaign_manager->current_campaign_id().isEmpty()) {
  1880. mark_current_mission_completed();
  1881. }
  1882. }
  1883. });
  1884. }
  1885. void GameEngine::configure_rain_system() {
  1886. if (m_rainManager) {
  1887. m_rainManager->configure(m_level.rain, m_level.biome_seed);
  1888. }
  1889. if (!m_rain) {
  1890. return;
  1891. }
  1892. const float world_width =
  1893. static_cast<float>(m_level.grid_width) * m_level.tile_size;
  1894. const float world_height =
  1895. static_cast<float>(m_level.grid_height) * m_level.tile_size;
  1896. m_rain->configure(world_width, world_height, m_level.biome_seed,
  1897. m_level.rain.type);
  1898. m_rain->set_enabled(m_level.rain.enabled);
  1899. m_rain->set_wind_strength(m_level.rain.wind_strength);
  1900. const float initial_intensity =
  1901. m_rainManager ? m_rainManager->get_intensity()
  1902. : (m_level.rain.enabled ? m_level.rain.intensity : 0.0F);
  1903. m_rain->set_intensity(initial_intensity);
  1904. }
  1905. void GameEngine::finalize_skirmish_load() {
  1906. m_runtime.loading = false;
  1907. m_loading_overlay_wait_for_first_frame = true;
  1908. m_loading_overlay_frames_remaining = 5;
  1909. m_loading_overlay_min_duration_ms = 1000;
  1910. m_loading_overlay_timer.restart();
  1911. m_finalize_progress_after_overlay = true;
  1912. m_show_objectives_after_loading = is_campaign_mission();
  1913. emit is_loading_changed();
  1914. GameStateRestorer::rebuild_entity_cache(m_world.get(), m_entity_cache,
  1915. m_runtime.local_owner_id);
  1916. emit troop_count_changed();
  1917. m_ambient_state_manager = std::make_unique<AmbientStateManager>();
  1918. Engine::Core::EventManager::instance().publish(
  1919. Engine::Core::AmbientStateChangedEvent(
  1920. Engine::Core::AmbientState::PEACEFUL,
  1921. Engine::Core::AmbientState::PEACEFUL));
  1922. if (m_input_handler) {
  1923. m_input_handler->set_spectator_mode(m_level.is_spectator_mode);
  1924. }
  1925. emit owner_info_changed();
  1926. emit spectator_mode_changed();
  1927. }
  1928. void GameEngine::open_settings() {
  1929. if (m_saveLoadService) {
  1930. m_saveLoadService->open_settings();
  1931. }
  1932. }
  1933. void GameEngine::load_save() { load_from_slot("savegame"); }
  1934. void GameEngine::save_game(const QString &filename) {
  1935. save_to_slot(filename, filename);
  1936. }
  1937. void GameEngine::save_game_to_slot(const QString &slotName) {
  1938. save_to_slot(slotName, slotName);
  1939. }
  1940. void GameEngine::load_game_from_slot(const QString &slotName) {
  1941. load_from_slot(slotName);
  1942. }
  1943. auto GameEngine::load_from_slot(const QString &slot) -> bool {
  1944. if (!m_saveLoadService || !m_world) {
  1945. set_error("Load: not initialized");
  1946. return false;
  1947. }
  1948. m_finalize_progress_after_overlay = false;
  1949. m_loading_overlay_active = true;
  1950. m_runtime.loading = true;
  1951. emit is_loading_changed();
  1952. if (!m_saveLoadService->load_game_from_slot(*m_world, slot)) {
  1953. set_error(m_saveLoadService->get_last_error());
  1954. m_runtime.loading = false;
  1955. m_loading_overlay_active = false;
  1956. m_loading_overlay_wait_for_first_frame = false;
  1957. m_finalize_progress_after_overlay = false;
  1958. m_show_objectives_after_loading = false;
  1959. emit is_loading_changed();
  1960. return false;
  1961. }
  1962. const QJsonObject meta = m_saveLoadService->get_last_metadata();
  1963. if (m_campaign_manager && meta.contains("mission_mode")) {
  1964. Game::Mission::MissionContext mission_context;
  1965. mission_context.mode = meta["mission_mode"].toString();
  1966. mission_context.campaign_id = meta["mission_campaign_id"].toString();
  1967. mission_context.mission_id = meta["mission_id"].toString();
  1968. mission_context.difficulty = meta["mission_difficulty"].toString();
  1969. m_campaign_manager->set_mission_context(mission_context);
  1970. }
  1971. Game::Systems::GameStateSerializer::restore_level_from_metadata(meta,
  1972. m_level);
  1973. Game::Systems::GameStateSerializer::restore_camera_from_metadata(
  1974. meta, m_camera.get(), m_viewport.width, m_viewport.height);
  1975. Game::Systems::RuntimeSnapshot runtime_snap = to_runtime_snapshot();
  1976. Game::Systems::GameStateSerializer::restore_runtime_from_metadata(
  1977. meta, runtime_snap);
  1978. apply_runtime_snapshot(runtime_snap);
  1979. GameStateRestorer::RendererRefs renderers{
  1980. m_renderer.get(), m_camera.get(), m_ground.get(), m_terrain.get(),
  1981. m_biome.get(), m_river.get(), m_road.get(), m_riverbank.get(),
  1982. m_bridge.get(), m_fog.get(), m_stone.get(), m_plant.get(),
  1983. m_pine.get(), m_olive.get(), m_firecamp.get(), m_rain.get()};
  1984. GameStateRestorer::restore_environment_from_metadata(
  1985. meta, m_world.get(), renderers, m_level, m_runtime.local_owner_id,
  1986. m_viewport);
  1987. auto unit_reg = std::make_shared<Game::Units::UnitFactoryRegistry>();
  1988. Game::Units::registerBuiltInUnits(*unit_reg);
  1989. Game::Map::MapTransformer::setFactoryRegistry(unit_reg);
  1990. qInfo() << "Factory registry reinitialized after loading saved game";
  1991. GameStateRestorer::rebuild_registries_after_load(
  1992. m_world.get(), m_selected_player_id, m_level, m_runtime.local_owner_id);
  1993. GameStateRestorer::rebuild_entity_cache(m_world.get(), m_entity_cache,
  1994. m_runtime.local_owner_id);
  1995. emit troop_count_changed();
  1996. if (auto *ai_system = m_world->get_system<Game::Systems::AISystem>()) {
  1997. qInfo() << "Reinitializing AI system after loading saved game";
  1998. ai_system->reinitialize();
  1999. }
  2000. if (m_victoryService) {
  2001. m_victoryService->configure(Game::Map::VictoryConfig(),
  2002. m_runtime.local_owner_id);
  2003. }
  2004. m_runtime.loading = false;
  2005. m_loading_overlay_wait_for_first_frame = true;
  2006. m_loading_overlay_frames_remaining = 5;
  2007. m_loading_overlay_min_duration_ms = 1000;
  2008. m_loading_overlay_timer.restart();
  2009. m_finalize_progress_after_overlay = true;
  2010. emit is_loading_changed();
  2011. qInfo() << "Game load complete, victory/defeat checks re-enabled";
  2012. emit selected_units_changed();
  2013. emit owner_info_changed();
  2014. return true;
  2015. }
  2016. auto GameEngine::save_to_slot(const QString &slot,
  2017. const QString &title) -> bool {
  2018. if (!m_saveLoadService || !m_world) {
  2019. set_error("Save: not initialized");
  2020. return false;
  2021. }
  2022. Game::Systems::RuntimeSnapshot const runtime_snap = to_runtime_snapshot();
  2023. QJsonObject meta = Game::Systems::GameStateSerializer::build_metadata(
  2024. *m_world, m_camera.get(), m_level, runtime_snap);
  2025. meta["title"] = title;
  2026. if (m_campaign_manager) {
  2027. const auto &mission_context = m_campaign_manager->current_mission_context();
  2028. meta["mission_mode"] = mission_context.mode;
  2029. meta["mission_campaign_id"] = mission_context.campaign_id;
  2030. meta["mission_id"] = mission_context.mission_id;
  2031. meta["mission_difficulty"] = mission_context.difficulty;
  2032. }
  2033. const QByteArray screenshot = capture_screenshot();
  2034. if (!m_saveLoadService->save_game_to_slot(
  2035. *m_world, slot, title, m_level.map_name, meta, screenshot)) {
  2036. set_error(m_saveLoadService->get_last_error());
  2037. return false;
  2038. }
  2039. emit save_slots_changed();
  2040. return true;
  2041. }
  2042. auto GameEngine::get_save_slots() const -> QVariantList {
  2043. if (!m_saveLoadService) {
  2044. qWarning() << "Cannot get save slots: service not initialized";
  2045. return {};
  2046. }
  2047. return m_saveLoadService->get_save_slots();
  2048. }
  2049. void GameEngine::refresh_save_slots() { emit save_slots_changed(); }
  2050. auto GameEngine::delete_save_slot(const QString &slotName) -> bool {
  2051. if (!m_saveLoadService) {
  2052. qWarning() << "Cannot delete save slot: service not initialized";
  2053. return false;
  2054. }
  2055. bool const success = m_saveLoadService->delete_save_slot(slotName);
  2056. if (!success) {
  2057. QString const error = m_saveLoadService->get_last_error();
  2058. qWarning() << "Failed to delete save slot:" << error;
  2059. set_error(error);
  2060. } else {
  2061. emit save_slots_changed();
  2062. }
  2063. return success;
  2064. }
  2065. auto GameEngine::to_runtime_snapshot() const -> Game::Systems::RuntimeSnapshot {
  2066. Game::Systems::RuntimeSnapshot snapshot;
  2067. snapshot.paused = m_runtime.paused;
  2068. snapshot.time_scale = m_runtime.time_scale;
  2069. snapshot.local_owner_id = m_runtime.local_owner_id;
  2070. snapshot.victory_state = m_runtime.victory_state;
  2071. snapshot.cursor_mode = static_cast<int>(m_runtime.cursor_mode);
  2072. snapshot.selected_player_id = m_selected_player_id;
  2073. snapshot.follow_selection = m_followSelectionEnabled;
  2074. return snapshot;
  2075. }
  2076. void GameEngine::apply_runtime_snapshot(
  2077. const Game::Systems::RuntimeSnapshot &snapshot) {
  2078. m_runtime.paused = snapshot.paused;
  2079. m_runtime.time_scale = snapshot.time_scale;
  2080. m_runtime.local_owner_id = snapshot.local_owner_id;
  2081. m_runtime.victory_state = snapshot.victory_state;
  2082. m_selected_player_id = snapshot.selected_player_id;
  2083. m_followSelectionEnabled = snapshot.follow_selection;
  2084. m_runtime.cursor_mode = static_cast<CursorMode>(snapshot.cursor_mode);
  2085. if (m_cursor_manager) {
  2086. m_cursor_manager->set_mode(m_runtime.cursor_mode);
  2087. }
  2088. }
  2089. auto GameEngine::capture_screenshot() const -> QByteArray { return {}; }
  2090. void GameEngine::exit_game() {
  2091. if (m_saveLoadService) {
  2092. m_saveLoadService->exit_game();
  2093. }
  2094. }
  2095. auto GameEngine::get_owner_info() const -> QVariantList {
  2096. QVariantList result;
  2097. const auto &owner_registry = Game::Systems::OwnerRegistry::instance();
  2098. const auto &owners = owner_registry.get_all_owners();
  2099. for (const auto &owner : owners) {
  2100. QVariantMap owner_map;
  2101. owner_map["id"] = owner.owner_id;
  2102. owner_map["name"] = QString::fromStdString(owner.name);
  2103. owner_map["team_id"] = owner.team_id;
  2104. QString type_str;
  2105. switch (owner.type) {
  2106. case Game::Systems::OwnerType::Player:
  2107. type_str = "Player";
  2108. break;
  2109. case Game::Systems::OwnerType::AI:
  2110. type_str = "AI";
  2111. break;
  2112. case Game::Systems::OwnerType::Neutral:
  2113. type_str = "Neutral";
  2114. break;
  2115. }
  2116. owner_map["type"] = type_str;
  2117. owner_map["isLocal"] = (owner.owner_id == m_runtime.local_owner_id);
  2118. result.append(owner_map);
  2119. }
  2120. return result;
  2121. }
  2122. void GameEngine::get_selected_unit_ids(
  2123. std::vector<Engine::Core::EntityID> &out) const {
  2124. out.clear();
  2125. if (!m_selectionController) {
  2126. return;
  2127. }
  2128. m_selectionController->get_selected_unit_ids(out);
  2129. }
  2130. auto GameEngine::get_unit_type_key(Engine::Core::EntityID id,
  2131. QString &type_key) const -> bool {
  2132. type_key.clear();
  2133. if (!m_world) {
  2134. return false;
  2135. }
  2136. auto *e = m_world->get_entity(id);
  2137. if (e == nullptr) {
  2138. return false;
  2139. }
  2140. if (auto *u = e->get_component<Engine::Core::UnitComponent>()) {
  2141. type_key = Game::Units::spawn_typeToQString(u->spawn_type);
  2142. return true;
  2143. }
  2144. return false;
  2145. }
  2146. auto GameEngine::get_unit_info(Engine::Core::EntityID id, QString &name,
  2147. int &health, int &max_health, bool &is_building,
  2148. bool &alive, QString &nation) const -> bool {
  2149. if (!m_world) {
  2150. return false;
  2151. }
  2152. auto *e = m_world->get_entity(id);
  2153. if (e == nullptr) {
  2154. return false;
  2155. }
  2156. is_building = e->has_component<Engine::Core::BuildingComponent>();
  2157. if (auto *u = e->get_component<Engine::Core::UnitComponent>()) {
  2158. auto troop_type_opt = Game::Units::spawn_typeToTroopType(u->spawn_type);
  2159. if (troop_type_opt.has_value()) {
  2160. auto profile = Game::Systems::TroopProfileService::instance().get_profile(
  2161. u->nation_id, *troop_type_opt);
  2162. name = QString::fromStdString(profile.display_name);
  2163. } else {
  2164. name = QString::fromStdString(
  2165. Game::Units::spawn_typeToString(u->spawn_type));
  2166. }
  2167. health = u->health;
  2168. max_health = u->max_health;
  2169. alive = (u->health > 0);
  2170. nation = Game::Systems::nation_id_to_qstring(u->nation_id);
  2171. return true;
  2172. }
  2173. name = QStringLiteral("Entity");
  2174. health = max_health = 0;
  2175. alive = true;
  2176. nation = QStringLiteral("");
  2177. return true;
  2178. }
  2179. auto GameEngine::get_unit_stamina_info(Engine::Core::EntityID id,
  2180. float &stamina_ratio, bool &is_running,
  2181. bool &can_run) const -> bool {
  2182. stamina_ratio = 1.0F;
  2183. is_running = false;
  2184. can_run = false;
  2185. if (!m_world) {
  2186. return false;
  2187. }
  2188. auto *e = m_world->get_entity(id);
  2189. if (e == nullptr) {
  2190. return false;
  2191. }
  2192. auto *unit = e->get_component<Engine::Core::UnitComponent>();
  2193. if (unit == nullptr) {
  2194. return false;
  2195. }
  2196. can_run = Game::Units::can_use_run_mode(unit->spawn_type);
  2197. auto *stamina = e->get_component<Engine::Core::StaminaComponent>();
  2198. if (stamina != nullptr) {
  2199. stamina_ratio = stamina->get_stamina_ratio();
  2200. is_running = stamina->is_running;
  2201. }
  2202. return true;
  2203. }
  2204. void GameEngine::on_unit_spawned(const Engine::Core::UnitSpawnedEvent &event) {
  2205. auto &owners = Game::Systems::OwnerRegistry::instance();
  2206. if (event.owner_id == m_runtime.local_owner_id) {
  2207. if (event.spawn_type == Game::Units::SpawnType::Barracks) {
  2208. m_entity_cache.player_barracks_alive = true;
  2209. } else {
  2210. int const production_cost =
  2211. Game::Units::TroopConfig::instance().getProductionCost(
  2212. event.spawn_type);
  2213. m_entity_cache.player_troop_count += production_cost;
  2214. }
  2215. } else if (owners.is_ai(event.owner_id)) {
  2216. if (event.spawn_type == Game::Units::SpawnType::Barracks) {
  2217. m_entity_cache.enemy_barracks_count++;
  2218. m_entity_cache.enemy_barracks_alive = true;
  2219. }
  2220. }
  2221. auto emit_if_changed = [&] {
  2222. if (m_entity_cache.player_troop_count != m_runtime.last_troop_count) {
  2223. m_runtime.last_troop_count = m_entity_cache.player_troop_count;
  2224. emit troop_count_changed();
  2225. }
  2226. };
  2227. emit_if_changed();
  2228. }
  2229. void GameEngine::on_unit_died(const Engine::Core::UnitDiedEvent &event) {
  2230. auto &owners = Game::Systems::OwnerRegistry::instance();
  2231. if (event.owner_id == m_runtime.local_owner_id) {
  2232. if (event.spawn_type == Game::Units::SpawnType::Barracks) {
  2233. m_entity_cache.player_barracks_alive = false;
  2234. } else {
  2235. int const production_cost =
  2236. Game::Units::TroopConfig::instance().getProductionCost(
  2237. event.spawn_type);
  2238. m_entity_cache.player_troop_count -= production_cost;
  2239. m_entity_cache.player_troop_count =
  2240. std::max(0, m_entity_cache.player_troop_count);
  2241. }
  2242. } else if (owners.is_ai(event.owner_id)) {
  2243. if (event.spawn_type == Game::Units::SpawnType::Barracks) {
  2244. m_entity_cache.enemy_barracks_count--;
  2245. m_entity_cache.enemy_barracks_count =
  2246. std::max(0, m_entity_cache.enemy_barracks_count);
  2247. m_entity_cache.enemy_barracks_alive =
  2248. (m_entity_cache.enemy_barracks_count > 0);
  2249. }
  2250. }
  2251. }
  2252. auto GameEngine::minimap_image() const -> QImage {
  2253. if (m_minimap_manager) {
  2254. return m_minimap_manager->get_image();
  2255. }
  2256. return QImage();
  2257. }
  2258. auto GameEngine::generate_map_preview(const QString &map_path,
  2259. const QVariantList &player_configs) const
  2260. -> QImage {
  2261. Game::Map::Minimap::MapPreviewGenerator generator;
  2262. return generator.generate_preview(map_path, player_configs);
  2263. }
  2264. float GameEngine::loading_progress() const {
  2265. if (m_loading_progress_tracker) {
  2266. return m_loading_progress_tracker->progress();
  2267. }
  2268. return 0.0F;
  2269. }
  2270. QString GameEngine::loading_stage_text() const {
  2271. if (m_loading_progress_tracker) {
  2272. auto stage = m_loading_progress_tracker->current_stage();
  2273. auto stage_name = m_loading_progress_tracker->stage_name(stage);
  2274. auto detail = m_loading_progress_tracker->current_detail();
  2275. if (!detail.isEmpty()) {
  2276. return stage_name + " - " + detail;
  2277. }
  2278. return stage_name;
  2279. }
  2280. return QString();
  2281. }