game_engine.cpp 91 KB

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