diff --git a/actionneededmanager.cpp b/actionneededmanager.cpp index f5d314f..6df1eb2 100644 --- a/actionneededmanager.cpp +++ b/actionneededmanager.cpp @@ -33,6 +33,9 @@ bool ActionNeededManager::loadConfigFile(const QString &filePath) if (node.hasChild("track_city_free_population")) { m_config.trackCityFreePopulation = node["track_city_free_population"].asBool(true); } + if (node.hasChild("track_no_research")) { + m_config.trackNoResearch = node["track_no_research"].asBool(true); + } if (node.hasChild("max_visible_items")) { m_config.maxVisibleItems = node["max_visible_items"].asInt(8); } @@ -70,6 +73,10 @@ bool ActionNeededManager::loadConfigFile(const QString &filePath) QString hex = node["city_free_pop_color"].asString("#46DC82"); m_config.cityFreePopColor = QColor(hex); } + if (node.hasChild("no_research_color")) { + QString hex = node["no_research_color"].asString("#38C8FF"); + m_config.noResearchColor = QColor(hex); + } if (node.hasChild("card_bg_color")) { QString hex = node["card_bg_color"].asString("#121822"); m_config.cardBgColor = QColor(hex); @@ -85,7 +92,8 @@ bool ActionNeededManager::loadConfigFile(const QString &filePath) void ActionNeededManager::update(float deltaTime, int activePlayerId, const std::vector> &units, - const std::vector &cities) + const std::vector &cities, + bool needsResearch) { if (!m_config.enabled) return; @@ -93,6 +101,25 @@ void ActionNeededManager::update(float deltaTime, int activePlayerId, std::set currentConditionKeys; + // 0. Player has no active research + if (m_config.trackNoResearch && needsResearch) { + QString key = QString("%1_%2").arg(static_cast(ActionNeededType::NoResearch)).arg(activePlayerId); + currentConditionKeys.insert(key); + + auto it = std::find_if(m_items.begin(), m_items.end(), [&](const ActionItem &item) { + return item.type == ActionNeededType::NoResearch && item.entityId == activePlayerId; + }); + + if (it == m_items.end()) { + ActionItem newItem; + newItem.type = ActionNeededType::NoResearch; + newItem.entityId = activePlayerId; + newItem.firstDetectedTime = m_elapsedGameTime; + newItem.dismissed = false; + m_items.push_back(newItem); + } + } + // 1. Idle Units waiting for action if (m_config.trackIdleUnits) { for (const auto &u : units) { diff --git a/actionneededmanager.h b/actionneededmanager.h index 896736f..c05455e 100644 --- a/actionneededmanager.h +++ b/actionneededmanager.h @@ -12,7 +12,8 @@ class City; enum class ActionNeededType { UnitIdle = 0, CityNoProduction = 1, - CityFreePopulation = 2 + CityFreePopulation = 2, + NoResearch = 3 }; struct ActionItem { @@ -33,6 +34,7 @@ struct ActionNeededConfig { bool trackIdleUnits = true; bool trackCityNoProduction = true; bool trackCityFreePopulation = true; + bool trackNoResearch = true; int maxVisibleItems = 8; int itemWidth = 155; @@ -46,6 +48,7 @@ struct ActionNeededConfig { QColor unitIdleColor = QColor(60, 160, 255); QColor cityNoProdColor = QColor(255, 160, 40); QColor cityFreePopColor = QColor(70, 220, 130); + QColor noResearchColor = QColor(56, 200, 255); QColor cardBgColor = QColor(18, 24, 34); int cardBgAlpha = 235; }; @@ -59,7 +62,8 @@ public: void update(float deltaTime, int activePlayerId, const std::vector> &units, - const std::vector &cities); + const std::vector &cities, + bool needsResearch = false); std::vector getVisibleItems() const; int getVisibleCount() const; diff --git a/buildingconfig.cpp b/buildingconfig.cpp index 8682ff2..e4a4444 100644 --- a/buildingconfig.cpp +++ b/buildingconfig.cpp @@ -53,6 +53,8 @@ bool BuildingConfigManager::loadConfigFile(const QString &filePath) bCfg.scienceBonus = it.value().hasChild("science_bonus") ? it.value()["science_bonus"].asInt(0) : 0; bCfg.faithBonus = it.value().hasChild("faith_bonus") ? it.value()["faith_bonus"].asInt(0) : 0; bCfg.goldBonus = it.value().hasChild("gold_bonus") ? it.value()["gold_bonus"].asInt(0) : 0; + bCfg.requiredTech = it.value().hasChild("required_tech") ? it.value()["required_tech"].asString("") : ""; + bCfg.obsoleteTech = it.value().hasChild("obsolete_tech") ? it.value()["obsolete_tech"].asString("") : ""; bCfg.iconPath = it.value().hasChild("icon") ? it.value()["icon"].asString() : ""; bCfg.description = it.value().hasChild("description") ? it.value()["description"].asString() : ""; m_buildings[bCfg.id] = bCfg; diff --git a/buildingconfig.h b/buildingconfig.h index 415f388..909ffe3 100644 --- a/buildingconfig.h +++ b/buildingconfig.h @@ -17,6 +17,8 @@ struct BuildingConfig { int scienceBonus; int faithBonus; int goldBonus; + QString requiredTech; + QString obsoleteTech; QString iconPath; QString description; @@ -24,6 +26,7 @@ struct BuildingConfig { : id(""), name(""), district("center"), laborCost(1000), foodModifier(0.0f), foodBonus(0), laborBonus(0), cultureBonus(0), scienceBonus(0), faithBonus(0), goldBonus(0), + requiredTech(""), obsoleteTech(""), iconPath(""), description("") {} }; diff --git a/config/action_needed.yaml b/config/action_needed.yaml index 1e6bff0..624f27c 100644 --- a/config/action_needed.yaml +++ b/config/action_needed.yaml @@ -4,6 +4,7 @@ action_needed: track_idle_units: true track_city_no_production: true track_city_free_population: true + track_no_research: true max_visible_items: 8 # Visual & Layout parameters @@ -19,5 +20,6 @@ action_needed: unit_idle_color: "#3CA0FF" city_no_prod_color: "#FFA028" city_free_pop_color: "#46DC82" + no_research_color: "#38C8FF" card_bg_color: "#121822" card_bg_alpha: 235 diff --git a/config/buildings.yaml b/config/buildings.yaml index 6770656..5be60c5 100644 --- a/config/buildings.yaml +++ b/config/buildings.yaml @@ -1,19 +1,55 @@ # City Buildings & Extensions configuration -# All gameplay values are configurable via YAML +# All gameplay values, required technologies, and obsolescence technologies are configurable via YAML districts: center: id: "center" name: "City Center" - max_buildings: 10 + max_buildings: 12 description: "The urban core and administrative center of the city." buildings: + monument: + id: "monument" + name: "Monument" + district: "center" + labor_cost: 800 + required_tech: "" + obsolete_tech: "" + food_modifier: 0.0 + food_bonus: 0 + labor_bonus: 0 + culture_bonus: 250 + science_bonus: 0 + faith_bonus: 0 + gold_bonus: 0 + icon: "images/improvements/monument.png" + description: "+250 Culture per tick" + + palisade: + id: "palisade" + name: "Wooden Palisade" + district: "center" + labor_cost: 700 + required_tech: "" + obsolete_tech: "masonry" + food_modifier: 0.0 + food_bonus: 0 + labor_bonus: 100 + culture_bonus: 100 + science_bonus: 0 + faith_bonus: 0 + gold_bonus: 0 + icon: "images/improvements/monument.png" + description: "+100 Production, +100 Culture/tick (Obsolete with Masonry)" + granary: id: "granary" name: "Granary" district: "center" labor_cost: 1000 + required_tech: "pottery" + obsolete_tech: "" food_modifier: 0.50 food_bonus: 0 labor_bonus: 0 @@ -24,17 +60,104 @@ buildings: icon: "images/improvements/granary.png" description: "+50% Food production in city" - monument: - id: "monument" - name: "Monument" + library: + id: "library" + name: "Library" district: "center" - labor_cost: 800 + labor_cost: 1200 + required_tech: "writing" + obsolete_tech: "" food_modifier: 0.0 food_bonus: 0 labor_bonus: 0 - culture_bonus: 250 + culture_bonus: 0 + science_bonus: 350 + faith_bonus: 0 + gold_bonus: 0 + icon: "images/resources/kennis.png" + description: "+350 Science per tick" + + walls: + id: "walls" + name: "Stone Walls" + district: "center" + labor_cost: 1500 + required_tech: "masonry" + obsolete_tech: "gunpowder" + food_modifier: 0.0 + food_bonus: 0 + labor_bonus: 200 + culture_bonus: 200 science_bonus: 0 faith_bonus: 0 gold_bonus: 0 icon: "images/improvements/monument.png" - description: "+250 Culture per tick" + description: "+200 Production, +200 Culture/tick (Obsolete with Gunpowder)" + + water_mill: + id: "water_mill" + name: "Water Mill" + district: "center" + labor_cost: 1600 + required_tech: "wheel" + obsolete_tech: "" + food_modifier: 0.0 + food_bonus: 200 + labor_bonus: 250 + culture_bonus: 0 + science_bonus: 0 + faith_bonus: 0 + gold_bonus: 0 + icon: "images/improvements/granary.png" + description: "+200 Food, +250 Production per tick" + + market: + id: "market" + name: "Market" + district: "center" + labor_cost: 1800 + required_tech: "currency" + obsolete_tech: "" + food_modifier: 0.0 + food_bonus: 0 + labor_bonus: 0 + culture_bonus: 0 + science_bonus: 0 + faith_bonus: 0 + gold_bonus: 400 + icon: "images/resources/goud.png" + description: "+400 Gold per tick" + + university: + id: "university" + name: "University" + district: "center" + labor_cost: 2800 + required_tech: "education" + obsolete_tech: "" + food_modifier: 0.0 + food_bonus: 0 + labor_bonus: 0 + culture_bonus: 100 + science_bonus: 700 + faith_bonus: 0 + gold_bonus: 0 + icon: "images/resources/kennis.png" + description: "+700 Science, +100 Culture per tick" + + workshop: + id: "workshop" + name: "Workshop" + district: "center" + labor_cost: 2600 + required_tech: "machinery" + obsolete_tech: "" + food_modifier: 0.0 + food_bonus: 0 + labor_bonus: 500 + culture_bonus: 0 + science_bonus: 0 + faith_bonus: 0 + gold_bonus: 0 + icon: "images/resources/arbeid.png" + description: "+500 Production per tick" diff --git a/config/strings_en.yaml b/config/strings_en.yaml index e2c2133..4360840 100644 --- a/config/strings_en.yaml +++ b/config/strings_en.yaml @@ -128,7 +128,7 @@ ui: management_title: "%1 - CITY MANAGEMENT" close_btn: "[X] Close" local_info: "Local: Population: %L1 (%2 Pop: %3 working, %4 free) | Labor: %L5" - national_info: "National: Culture: %L1 | Science: %L2 | Gold: %L3 | Faith: %L4" + national_info: "National: Culture: %L1 | Science: +%L2/tick | Gold: %L3 | Faith: %L4" help_text: "Click a slot to assign/free Pop | Hold a slot to choose resource | Buy tile with Culture." current_production: "CURRENT PRODUCTION" production_panel_title: "CITY PRODUCTION" @@ -160,6 +160,8 @@ ui: queued_item_removed: "%1: Removed %2 from queue" already_built: "%1 already built in this city!" already_queued: "%1 is already in the production queue!" + requires_tech: "%1 requires technology: %2!" + obsolete_item: "%1 is obsolete and can no longer be built!" growth_float: "+%1 Inhabitants | +%2 Culture" not_enough_culture: "Not enough Culture! (%1 needed)" worker_freed: "Pop worker freed" @@ -187,6 +189,42 @@ ui: unit_idle_desc: "Needs orders" city_no_production_desc: "No production" city_free_pop_desc: "+%1 Free Pop" + no_research_title: "RESEARCH" + no_research_desc: "Choose technology" dismiss_tip: "Right-click to dismiss" more_items: "+%1" + tech_tree: + title: "TECHNOLOGY TREE" + close_btn: "[X] Close" + btn_open: "Tech [T]" + no_research: "No Research" + choose_research: "Choose Tech [T]" + science_rate: "+%L1/tick" + active_research: "ACTIVE RESEARCH: %1" + no_active_research: "No active research - Click any technology node below to start researching" + queue_label: "QUEUE (%1):" + clear_queue_btn: "Clear Queue" + status_researched: "RESEARCHED" + status_researching: "ACTIVE #1" + status_queued: "QUEUED #%1" + status_available: "AVAILABLE" + status_locked: "LOCKED" + cost_format: "%L1 Science (~%2 %3)" + progress_format: "%L1 / %L2 (%3%)" + remaining_format: "%L1 left (~%2 %3)" + tick_singular: "tick" + tick_plural: "ticks" + prereqs_label: "Req: %1" + prereqs_none: "Base Technology" + unlocks_label: "Unlocks: %1" + obsoletes_label: "Obsoletes: %1" + help_text: "Left-click a tech to research (prerequisites auto-queued by least remaining work) | Shift+Click to append | Right-click to remove | Drag/Scroll to pan" + tech_completed_float: "Technology Researched: %1!" + tech_started_float: "Researching: %1" + tech_queued_float: "Research Queue Updated (%1 techs, target: %2)" + tech_already_researched: "%1 has already been researched!" + tech_cancelled_float: "Research queue cleared" + tech_removed_float: "Removed %1 from research queue" + + diff --git a/config/technologies.yaml b/config/technologies.yaml new file mode 100644 index 0000000..1460ba1 --- /dev/null +++ b/config/technologies.yaml @@ -0,0 +1,202 @@ +# Technology Tree Configuration +# All gameplay values, science costs, layout coordinates, and prerequisite chains are configurable via YAML. + +technologies: + pottery: + id: "pottery" + name: "Pottery" + science_cost: 1000 + column: 0 + row: 0 + icon: "images/improvements/granary.png" + description: "Ceramic vessels for storing grain and preserving food supplies." + + animal_husbandry: + id: "animal_husbandry" + name: "Animal Husbandry" + science_cost: 1000 + column: 0 + row: 1 + icon: "images/resources/voedsel.png" + description: "Domestication and breeding of livestock for food and labor." + + mining: + id: "mining" + name: "Mining" + science_cost: 1200 + column: 0 + row: 2 + icon: "images/resources/arbeid.png" + description: "Extraction of stone and metal ores from the earth." + + sailing: + id: "sailing" + name: "Sailing" + science_cost: 1500 + column: 1 + row: 0 + icon: "images/units/galley.png" + description: "Harnessing the wind to navigate coastal waters." + prerequisites: + - "pottery" + + writing: + id: "writing" + name: "Writing" + science_cost: 1800 + column: 1 + row: 1 + icon: "images/resources/kennis.png" + description: "Recording laws, history, and scientific knowledge in script." + prerequisites: + - "pottery" + + archery: + id: "archery" + name: "Archery" + science_cost: 1500 + column: 1 + row: 2 + icon: "images/units/slinger.png" + description: "Use of bows and arrows for superior ranged warfare." + prerequisites: + - "animal_husbandry" + + bronze_working: + id: "bronze_working" + name: "Bronze Working" + science_cost: 1800 + column: 1 + row: 3 + icon: "images/units/warrior.png" + description: "Smelting copper and tin into durable bronze weapons and armor." + prerequisites: + - "mining" + + masonry: + id: "masonry" + name: "Masonry" + science_cost: 1600 + column: 1 + row: 4 + icon: "images/improvements/monument.png" + description: "Cutting and laying stone for permanent fortifications and monuments." + prerequisites: + - "mining" + + celestial_navigation: + id: "celestial_navigation" + name: "Celestial Navigation" + science_cost: 2500 + column: 2 + row: 0 + icon: "images/units/galley.png" + description: "Using the stars to guide maritime trade and exploration." + prerequisites: + - "sailing" + - "writing" + + currency: + id: "currency" + name: "Currency" + science_cost: 2400 + column: 2 + row: 1 + icon: "images/resources/goud.png" + description: "Standardized coinage enabling thriving markets and trade." + prerequisites: + - "writing" + + wheel: + id: "wheel" + name: "The Wheel" + science_cost: 2200 + column: 2 + row: 2 + icon: "images/actions/road.png" + description: "Rotational mechanics for transport and water-powered mills." + prerequisites: + - "archery" + - "mining" + + horseback_riding: + id: "horseback_riding" + name: "Horseback Riding" + science_cost: 2500 + column: 2 + row: 3 + icon: "images/actions/move.png" + description: "Mounted cavalry tactics for rapid battlefield maneuvers." + prerequisites: + - "archery" + + iron_working: + id: "iron_working" + name: "Iron Working" + science_cost: 2800 + column: 2 + row: 4 + icon: "images/actions/attack.png" + description: "Forging strong iron blades and heavy infantry armor." + prerequisites: + - "bronze_working" + + education: + id: "education" + name: "Education" + science_cost: 3600 + column: 3 + row: 0 + icon: "images/resources/kennis.png" + description: "Higher learning institutions and systematic scientific inquiry." + prerequisites: + - "currency" + - "celestial_navigation" + + engineering: + id: "engineering" + name: "Engineering" + science_cost: 3400 + column: 3 + row: 2 + icon: "images/resources/arbeid.png" + description: "Advanced structural design, aqueducts, and mechanical contraptions." + prerequisites: + - "wheel" + - "masonry" + + stirrups: + id: "stirrups" + name: "Stirrups" + science_cost: 4000 + column: 3 + row: 3 + icon: "images/actions/move.png" + description: "Saddle stirrups enabling heavily armored shock cavalry." + prerequisites: + - "horseback_riding" + - "iron_working" + + machinery: + id: "machinery" + name: "Machinery" + science_cost: 4200 + column: 4 + row: 1 + icon: "images/resources/arbeid.png" + description: "Mechanical gears, crossbows, and specialized workshops." + prerequisites: + - "engineering" + - "iron_working" + + gunpowder: + id: "gunpowder" + name: "Gunpowder" + science_cost: 5200 + column: 4 + row: 3 + icon: "images/actions/attack.png" + description: "Explosive black powder revolutionizing firearms and siege warfare." + prerequisites: + - "machinery" + - "stirrups" diff --git a/config/units/archer.yaml b/config/units/archer.yaml new file mode 100644 index 0000000..e20f273 --- /dev/null +++ b/config/units/archer.yaml @@ -0,0 +1,16 @@ +id: archer +name: Archer +category: militair +domain: land +health: 85 +movement_speed: 2.0 +cost: 60 +labor_cost: 1200 +attack_damage: 25 +attack_range: 2 +required_tech: "archery" +obsolete_tech: "machinery" +icon: images/units/slinger.png +actions: + - verplaatsen + - aanvallen diff --git a/config/units/builder.yaml b/config/units/builder.yaml index d535f82..c7b7e60 100644 --- a/config/units/builder.yaml +++ b/config/units/builder.yaml @@ -6,7 +6,9 @@ health: 100 movement_speed: 2.0 cost: 50 labor_cost: 1000 -build_charges: -1 # -1 voor oneindig doorwerken +required_tech: "" +obsolete_tech: "" +build_charges: -1 # -1 for infinite build charges infinite_charges: true icon: images/units/builder.png actions: @@ -14,5 +16,5 @@ actions: - boerderij_bouwen - build_road action_speeds: - boerderij_bouwen: 10.0 # Duur in seconden die deze builder unit nodig heeft voor de actie boerderij_bouwen - build_road: 2 # Duur in ticks + boerderij_bouwen: 10.0 + build_road: 2 diff --git a/config/units/crossbowman.yaml b/config/units/crossbowman.yaml new file mode 100644 index 0000000..feafa7d --- /dev/null +++ b/config/units/crossbowman.yaml @@ -0,0 +1,16 @@ +id: crossbowman +name: Crossbowman +category: militair +domain: land +health: 110 +movement_speed: 2.0 +cost: 120 +labor_cost: 2400 +attack_damage: 42 +attack_range: 2 +required_tech: "machinery" +obsolete_tech: "" +icon: images/units/slinger.png +actions: + - verplaatsen + - aanvallen diff --git a/config/units/galley.yaml b/config/units/galley.yaml index 0eab5bf..7cbcc4f 100644 --- a/config/units/galley.yaml +++ b/config/units/galley.yaml @@ -8,6 +8,8 @@ cost: 60 labor_cost: 1500 attack_damage: 25 attack_range: 1 +required_tech: "sailing" +obsolete_tech: "" icon: images/units/galley.png actions: - verplaatsen diff --git a/config/units/horseman.yaml b/config/units/horseman.yaml new file mode 100644 index 0000000..5af115b --- /dev/null +++ b/config/units/horseman.yaml @@ -0,0 +1,16 @@ +id: horseman +name: Horseman +category: militair +domain: land +health: 125 +movement_speed: 4.0 +cost: 85 +labor_cost: 1700 +attack_damage: 34 +attack_range: 1 +required_tech: "horseback_riding" +obsolete_tech: "stirrups" +icon: images/units/warrior.png +actions: + - verplaatsen + - aanvallen diff --git a/config/units/knight.yaml b/config/units/knight.yaml new file mode 100644 index 0000000..87c89fd --- /dev/null +++ b/config/units/knight.yaml @@ -0,0 +1,16 @@ +id: knight +name: Knight +category: militair +domain: land +health: 180 +movement_speed: 4.0 +cost: 140 +labor_cost: 2800 +attack_damage: 52 +attack_range: 1 +required_tech: "stirrups" +obsolete_tech: "" +icon: images/units/warrior.png +actions: + - verplaatsen + - aanvallen diff --git a/config/units/musketman.yaml b/config/units/musketman.yaml new file mode 100644 index 0000000..e63fff0 --- /dev/null +++ b/config/units/musketman.yaml @@ -0,0 +1,16 @@ +id: musketman +name: Musketman +category: militair +domain: land +health: 200 +movement_speed: 2.0 +cost: 160 +labor_cost: 3200 +attack_damage: 60 +attack_range: 1 +required_tech: "gunpowder" +obsolete_tech: "" +icon: images/units/warrior.png +actions: + - verplaatsen + - aanvallen diff --git a/config/units/settler.yaml b/config/units/settler.yaml index 0b247d7..3fed497 100644 --- a/config/units/settler.yaml +++ b/config/units/settler.yaml @@ -6,6 +6,8 @@ health: 100 movement_speed: 2.0 cost: 80 labor_cost: 2000 +required_tech: "" +obsolete_tech: "" icon: images/units/settler.png actions: - verplaatsen diff --git a/config/units/slinger.yaml b/config/units/slinger.yaml index 6cdd99c..dff341e 100644 --- a/config/units/slinger.yaml +++ b/config/units/slinger.yaml @@ -8,6 +8,8 @@ cost: 35 labor_cost: 800 attack_damage: 15 attack_range: 2 +required_tech: "" +obsolete_tech: "archery" icon: images/units/slinger.png actions: - verplaatsen diff --git a/config/units/spearman.yaml b/config/units/spearman.yaml new file mode 100644 index 0000000..13400de --- /dev/null +++ b/config/units/spearman.yaml @@ -0,0 +1,16 @@ +id: spearman +name: Spearman +category: militair +domain: land +health: 120 +movement_speed: 2.0 +cost: 65 +labor_cost: 1300 +attack_damage: 28 +attack_range: 1 +required_tech: "bronze_working" +obsolete_tech: "gunpowder" +icon: images/units/warrior.png +actions: + - verplaatsen + - aanvallen diff --git a/config/units/swordsman.yaml b/config/units/swordsman.yaml new file mode 100644 index 0000000..c190caa --- /dev/null +++ b/config/units/swordsman.yaml @@ -0,0 +1,16 @@ +id: swordsman +name: Swordsman +category: militair +domain: land +health: 140 +movement_speed: 2.0 +cost: 90 +labor_cost: 1800 +attack_damage: 38 +attack_range: 1 +required_tech: "iron_working" +obsolete_tech: "gunpowder" +icon: images/units/warrior.png +actions: + - verplaatsen + - aanvallen diff --git a/config/units/warrior.yaml b/config/units/warrior.yaml index f596f47..0cebbc4 100644 --- a/config/units/warrior.yaml +++ b/config/units/warrior.yaml @@ -8,6 +8,8 @@ cost: 40 labor_cost: 1000 attack_damage: 20 attack_range: 1 +required_tech: "" +obsolete_tech: "iron_working" icon: images/units/warrior.png actions: - verplaatsen diff --git a/gamemanager.cpp b/gamemanager.cpp index 7b5c5f8..f489730 100644 --- a/gamemanager.cpp +++ b/gamemanager.cpp @@ -14,7 +14,7 @@ GameManager& GameManager::instance() } GameManager::GameManager() - : m_selectedUnitId(-1), m_activeCityViewId(-1), m_actionMode(ActionMode::None), + : m_selectedUnitId(-1), m_activeCityViewId(-1), m_isTechTreeOpen(false), m_actionMode(ActionMode::None), m_movementTickDuration(30.0f), m_tickTimer(0.0f), m_simSpeedMultiplier(1.0f), m_maxActionQueueSize(5), m_autoQueueMoveWhenBusy(true), m_allowActionQueueing(true), @@ -58,6 +58,10 @@ void GameManager::init(const QString &baseDir, Matrix2D *mapPoints QString roadsConfigPath = baseDir + "/config/roads.yaml"; m_roadConfigManager.loadConfigFile(roadsConfigPath); + // Load technology tree YAML configuration + QString techConfigPath = baseDir + "/config/technologies.yaml"; + m_techTreeManager.loadConfigFile(techConfigPath); + // Load action needed queue configuration QString actionNeededConfigPath = baseDir + "/config/action_needed.yaml"; m_actionNeededManager.loadConfigFile(actionNeededConfigPath); @@ -65,9 +69,10 @@ void GameManager::init(const QString &baseDir, Matrix2D *mapPoints const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); // Initialize player national resources from configuration + // Note: science is a per-tick generation rate and is not stockpiled for (auto &player : m_playerManager.getPlayersMutable()) { player.culture = cCfg.initialNationalResources.value("cultuur", 10000); - player.science = cCfg.initialNationalResources.value("kennis", 0); + player.science = 0; player.faith = cCfg.initialNationalResources.value("geloof", 0); player.gold = cCfg.initialNationalResources.value("goud", 0); } @@ -86,6 +91,11 @@ void GameManager::init(const QString &baseDir, Matrix2D *mapPoints m_cities.push_back(initialCity); + // Update initial per-tick science generation rate for all players + for (auto &player : m_playerManager.getPlayersMutable()) { + player.science = calculatePlayerSciencePerTick(player.id, mapPoints, numPoints05); + } + // Spawn initial demo units near starting camera position // Player 1 (Blue & Gold) spawnUnit("settler", 1, 36, 18, mapPoints, numPoints05); @@ -123,6 +133,11 @@ Unit* GameManager::spawnUnit(const QString &configId, int playerId, int tileX, i void GameManager::update(float deltaTime, Matrix2D *mapPoints, int numPoints05) { + // Keep player science rate synchronized with current city yields (science is generated per tick, not saved) + for (auto &player : m_playerManager.getPlayersMutable()) { + player.science = calculatePlayerSciencePerTick(player.id, mapPoints, numPoints05); + } + // Update movement tick timer m_tickTimer += deltaTime * m_simSpeedMultiplier; if (m_tickTimer >= m_movementTickDuration) { @@ -171,8 +186,347 @@ void GameManager::update(float deltaTime, Matrix2D *mapPoints, int } } + // Check if Player 1 needs to select a technology to research + const Player *p1 = m_playerManager.getPlayer(1); + bool p1HasCities = false; + for (const auto &c : m_cities) { + if (c.playerId == 1) { + p1HasCities = true; + break; + } + } + bool hasUnresearchedTechs = p1 && ((int)p1->researchedTechs.size() < m_techTreeManager.getAllTechs().size()); + bool needsResearch = p1 && p1HasCities && !p1->hasActiveResearch() && hasUnresearchedTechs; + // Update Action Needed Queue for active human player 1 - m_actionNeededManager.update(deltaTime, 1, m_units, m_cities); + m_actionNeededManager.update(deltaTime, 1, m_units, m_cities, needsResearch); +} + +void GameManager::openTechTree() +{ + m_isTechTreeOpen = true; + m_actionMode = ActionMode::None; +} + +void GameManager::closeTechTree() +{ + m_isTechTreeOpen = false; +} + +void GameManager::toggleTechTree() +{ + if (m_isTechTreeOpen) { + closeTechTree(); + } else { + openTechTree(); + } +} + +int GameManager::calculatePlayerSciencePerTick(int playerId, Matrix2D *mapPoints, int numPoints05) const +{ + int totalScience = 0; + for (const auto &city : m_cities) { + if (city.playerId == playerId) { + CityYieldSummary yields = calculateCityYields(city, mapPoints, numPoints05); + totalScience += yields.science; + } + } + return totalScience; +} + +int GameManager::getTechRemainingScience(const Player &player, const QString &techId) const +{ + const TechConfig *cfg = m_techTreeManager.getTech(techId); + if (!cfg) return 0; + if (player.hasResearched(techId)) return 0; + int progress = player.getTechProgress(techId); + return std::max(0, cfg->scienceCost - progress); +} + +bool GameManager::selectTechForPlayer(int playerId, const QString &targetTechId, bool appendToQueue) +{ + Player *player = m_playerManager.getPlayer(playerId); + if (!player) return false; + + const TechConfig *targetCfg = m_techTreeManager.getTech(targetTechId); + if (!targetCfg) return false; + + if (player->hasResearched(targetTechId)) { + return false; + } + + // Determine which techs are already satisfied (researched, or already queued if appending) + std::set satisfied = player->researchedTechs; + if (appendToQueue) { + for (const QString &qId : player->researchQueue) { + satisfied.insert(qId); + } + if (satisfied.find(targetTechId) != satisfied.end()) { + return false; + } + } + + // Collect all unresearched (and unqueued if appending) prerequisites of targetTechId + targetTechId itself + std::set requiredSet; + std::vector stack; + stack.push_back(targetTechId); + + while (!stack.empty()) { + QString cur = stack.back(); + stack.pop_back(); + + if (satisfied.find(cur) != satisfied.end() || requiredSet.find(cur) != requiredSet.end()) { + continue; + } + const TechConfig *cCfg = m_techTreeManager.getTech(cur); + if (!cCfg) continue; + + requiredSet.insert(cur); + for (const QString &pre : cCfg->prerequisites) { + if (satisfied.find(pre) == satisfied.end() && requiredSet.find(pre) == requiredSet.end()) { + stack.push_back(pre); + } + } + } + + // Build ordered queue: + // At each step, among all technologies in requiredSet whose prerequisites are all satisfied, + // choose the technology with the smallest remaining science work (scienceCost - currentProgress). + // Note: targetTechId itself only becomes available once all its prerequisites are placed in the queue. + std::vector orderedQueue; + orderedQueue.reserve(requiredSet.size()); + std::set currentSatisfied = satisfied; + std::set remainingToSchedule = requiredSet; + + while (!remainingToSchedule.empty()) { + std::vector readyTechs; + for (const QString &candId : remainingToSchedule) { + const TechConfig *candCfg = m_techTreeManager.getTech(candId); + if (!candCfg) continue; + + bool allPrereqsMet = true; + for (const QString &pre : candCfg->prerequisites) { + if (currentSatisfied.find(pre) == currentSatisfied.end()) { + allPrereqsMet = false; + break; + } + } + if (allPrereqsMet) { + readyTechs.push_back(candId); + } + } + + if (readyTechs.empty()) { + // Fallback in case of malformed cyclic config in YAML + for (const QString &candId : remainingToSchedule) { + readyTechs.push_back(candId); + } + } + + // Sort ready techs by smallest remaining science work (already partially researched techs have less work left) + std::sort(readyTechs.begin(), readyTechs.end(), [&](const QString &a, const QString &b) { + // Ensure targetTechId comes after any other ready tech if appending or tie-breaking + if (a == targetTechId && b != targetTechId) return false; + if (b == targetTechId && a != targetTechId) return true; + + int remA = getTechRemainingScience(*player, a); + int remB = getTechRemainingScience(*player, b); + if (remA != remB) return remA < remB; + + const TechConfig *ta = m_techTreeManager.getTech(a); + const TechConfig *tb = m_techTreeManager.getTech(b); + if (ta && tb) { + if (ta->column != tb->column) return ta->column < tb->column; + if (ta->row != tb->row) return ta->row < tb->row; + } + return a < b; + }); + + QString chosen = readyTechs.front(); + orderedQueue.push_back(chosen); + currentSatisfied.insert(chosen); + remainingToSchedule.erase(chosen); + } + + if (appendToQueue) { + player->researchQueue.insert(player->researchQueue.end(), orderedQueue.begin(), orderedQueue.end()); + } else { + player->researchQueue = orderedQueue; + } + + return true; +} + +void GameManager::removeTechFromPlayerQueue(int playerId, const QString &techId) +{ + Player *player = m_playerManager.getPlayer(playerId); + if (!player) return; + + // Remove techId and any queued tech that transitively depends on techId + std::set removed; + removed.insert(techId); + + bool changed = true; + while (changed) { + changed = false; + for (const QString &qId : player->researchQueue) { + if (removed.find(qId) != removed.end()) continue; + const TechConfig *cfg = m_techTreeManager.getTech(qId); + if (!cfg) continue; + for (const QString &pre : cfg->prerequisites) { + if (removed.find(pre) != removed.end()) { + removed.insert(qId); + changed = true; + break; + } + } + } + } + + player->researchQueue.erase( + std::remove_if(player->researchQueue.begin(), player->researchQueue.end(), + [&](const QString &id) { return removed.find(id) != removed.end(); }), + player->researchQueue.end() + ); +} + +void GameManager::clearPlayerResearchQueue(int playerId) +{ + Player *player = m_playerManager.getPlayer(playerId); + if (!player) return; + player->researchQueue.clear(); +} + +void GameManager::advancePlayerResearch(Player &player, int scienceGenerated) +{ + if (scienceGenerated <= 0 || player.researchQueue.empty()) { + // Science cannot be saved when no technology is being researched + return; + } + + int remainingScienceToApply = scienceGenerated; + + while (remainingScienceToApply > 0 && !player.researchQueue.empty()) { + QString activeId = player.researchQueue.front(); + const TechConfig *cfg = m_techTreeManager.getTech(activeId); + if (!cfg) { + player.researchQueue.erase(player.researchQueue.begin()); + continue; + } + + // Verify prerequisites are met before applying science + if (!player.arePrerequisitesMet(cfg->prerequisites)) { + // Re-run prerequisite ordering for the target tech + QString targetId = player.researchQueue.back(); + selectTechForPlayer(player.id, targetId, false); + if (player.researchQueue.empty()) break; + activeId = player.researchQueue.front(); + cfg = m_techTreeManager.getTech(activeId); + if (!cfg) break; + } + + int curProg = player.getTechProgress(activeId); + int needed = std::max(0, cfg->scienceCost - curProg); + + if (remainingScienceToApply >= needed) { + // Technology completed! + remainingScienceToApply -= needed; + player.techProgress[activeId] = cfg->scienceCost; + player.researchedTechs.insert(activeId); + player.researchQueue.erase(player.researchQueue.begin()); + + // Remove any newly obsolete unbuilt projects from this player's city queues + // (Already built buildings and existing units on the map do NOT disappear!) + for (auto &city : m_cities) { + if (city.playerId != player.id) continue; + + // Remove obsolete items from waiting projectQueue + city.projectQueue.erase( + std::remove_if(city.projectQueue.begin(), city.projectQueue.end(), [&](const CityProject &proj) { + if (proj.type == CityProjectType::Unit) { + const UnitConfig *uCfg = m_unitConfigManager.getConfig(proj.id); + return uCfg && player.isItemObsolete(uCfg->obsoleteTech); + } else if (proj.type == CityProjectType::Building) { + const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(proj.id); + return bCfg && player.isItemObsolete(bCfg->obsoleteTech); + } + return false; + }), + city.projectQueue.end() + ); + + // Check if currentProject became obsolete + if (city.hasActiveProject()) { + bool activeObsolete = false; + if (city.currentProject.type == CityProjectType::Unit) { + const UnitConfig *uCfg = m_unitConfigManager.getConfig(city.currentProject.id); + activeObsolete = (uCfg && player.isItemObsolete(uCfg->obsoleteTech)); + } else if (city.currentProject.type == CityProjectType::Building) { + const BuildingConfig *bCfg = m_buildingConfigManager.getBuilding(city.currentProject.id); + activeObsolete = (bCfg && player.isItemObsolete(bCfg->obsoleteTech)); + } + if (activeObsolete) { + city.cancelActiveProject(); + } + } + } + + // Show floating notification over player's capital / first city + for (const auto &city : m_cities) { + if (city.playerId == player.id) { + QString msg = Localization::instance().get("ui.tech_tree.tech_completed_float", "Technology Researched: %1!") + .arg(cfg->name); + addFloatingText(msg, city.worldPos + QVector3D(0.0f, 0.0f, 1.5f), QColor(60, 220, 255), 3.5f); + break; + } + } + } else { + player.techProgress[activeId] = curProg + remainingScienceToApply; + remainingScienceToApply = 0; + } + } +} + +QStringList GameManager::getTechUnlocks(const QString &techId) const +{ + QStringList unlocks; + if (techId.isEmpty()) return unlocks; + + const auto &buildings = m_buildingConfigManager.getAllBuildings(); + for (auto it = buildings.begin(); it != buildings.end(); ++it) { + if (it.value().requiredTech == techId) { + unlocks.append(it.value().name); + } + } + + const auto &units = m_unitConfigManager.getAllConfigs(); + for (auto it = units.begin(); it != units.end(); ++it) { + if (it.value().requiredTech == techId) { + unlocks.append(it.value().name); + } + } + return unlocks; +} + +QStringList GameManager::getTechObsoletes(const QString &techId) const +{ + QStringList obsoletes; + if (techId.isEmpty()) return obsoletes; + + const auto &buildings = m_buildingConfigManager.getAllBuildings(); + for (auto it = buildings.begin(); it != buildings.end(); ++it) { + if (it.value().obsoleteTech == techId) { + obsoletes.append(it.value().name); + } + } + + const auto &units = m_unitConfigManager.getAllConfigs(); + for (auto it = units.begin(); it != units.end(); ++it) { + if (it.value().obsoleteTech == techId) { + obsoletes.append(it.value().name); + } + } + return obsoletes; } Unit* GameManager::getSelectedUnit() @@ -251,6 +605,7 @@ const City* GameManager::getActiveCityView() const void GameManager::enterCityView(int cityId) { m_activeCityViewId = cityId; + m_isTechTreeOpen = false; m_selectedUnitId = -1; m_actionMode = ActionMode::None; } @@ -378,6 +733,7 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D *mapPoints, int numPoints05) { const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig(); + QMap playerTickScience; for (auto &city : m_cities) { CityYieldSummary yields = calculateCityYields(city, mapPoints, numPoints05); @@ -396,13 +752,13 @@ void GameManager::processTickYields(Matrix2D *mapPoints, int numPo city.accumulatedLabor += yields.labor; } - // 3. National resources go to the owner player: + // 3. National resources go to the owner player (science is applied to research, not stockpiled): Player *owner = m_playerManager.getPlayer(city.playerId); if (owner) { owner->culture += yields.culture; - owner->science += yields.science; owner->faith += yields.faith; owner->gold += yields.gold; + playerTickScience[city.playerId] += yields.science; } // Floating growth text over city @@ -412,6 +768,13 @@ void GameManager::processTickYields(Matrix2D *mapPoints, int numPo .arg(foodGrown).arg(yields.culture); addFloatingText(growthMsg, ftPos, QColor(100, 255, 140), 2.5f); } + + // 4. Apply generated science per tick directly to each player's active research queue + for (auto &player : m_playerManager.getPlayersMutable()) { + int generatedSci = playerTickScience.value(player.id, 0); + player.science = generatedSci; + advancePlayerResearch(player, generatedSci); + } } bool GameManager::isTileOwned(int tx, int ty, int *outOwnerCityId) const @@ -471,6 +834,7 @@ void GameManager::queueCityProject(int cityId, CityProjectType type, const QStri City *city = getCityById(cityId); if (!city) return; + const Player *player = m_playerManager.getPlayer(city->playerId); const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); int maxQueue = cCfg.maxProductionQueueSize; @@ -478,6 +842,23 @@ void GameManager::queueCityProject(int cityId, CityProjectType type, const QStri const UnitConfig *uCfg = m_unitConfigManager.getConfig(id); if (!uCfg) return; + if (player) { + if (!player->isItemUnlocked(uCfg->requiredTech)) { + const TechConfig *tCfg = m_techTreeManager.getTech(uCfg->requiredTech); + QString tName = tCfg ? tCfg->name : uCfg->requiredTech; + QString msg = Localization::instance().get("ui.city.requires_tech", "%1 requires technology: %2!") + .arg(uCfg->name).arg(tName); + addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f); + return; + } + if (player->isItemObsolete(uCfg->obsoleteTech)) { + QString msg = Localization::instance().get("ui.city.obsolete_item", "%1 is obsolete and can no longer be built!") + .arg(uCfg->name); + addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f); + return; + } + } + if (!city->hasActiveProject()) { city->startProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost); QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2") @@ -505,6 +886,23 @@ void GameManager::queueCityProject(int cityId, CityProjectType type, const QStri return; } + if (player) { + if (!player->isItemUnlocked(bCfg->requiredTech)) { + const TechConfig *tCfg = m_techTreeManager.getTech(bCfg->requiredTech); + QString tName = tCfg ? tCfg->name : bCfg->requiredTech; + QString msg = Localization::instance().get("ui.city.requires_tech", "%1 requires technology: %2!") + .arg(bCfg->name).arg(tName); + addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f); + return; + } + if (player->isItemObsolete(bCfg->obsoleteTech)) { + QString msg = Localization::instance().get("ui.city.obsolete_item", "%1 is obsolete and can no longer be built!") + .arg(bCfg->name); + addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 100, 100), 2.2f); + return; + } + } + if (city->isBuildingQueuedOrBuilt(id)) { QString msg = Localization::instance().get("ui.city.already_queued", "%1 is already in the production queue!").arg(bCfg->name); addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 160, 60), 2.2f); diff --git a/gamemanager.h b/gamemanager.h index f8be1c4..628d6d6 100644 --- a/gamemanager.h +++ b/gamemanager.h @@ -14,6 +14,7 @@ #include "terrain.h" #include "terrainyieldmanager.h" #include "actionneededmanager.h" +#include "techconfig.h" enum class ActionMode { None, @@ -48,13 +49,32 @@ public: void update(float deltaTime, Matrix2D *mapPoints, int numPoints05); PlayerManager& players() { return m_playerManager; } + const PlayerManager& players() const { return m_playerManager; } UnitConfigManager& unitConfigs() { return m_unitConfigManager; } + const UnitConfigManager& unitConfigs() const { return m_unitConfigManager; } BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; } const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; } RoadConfigManager& roadConfigs() { return m_roadConfigManager; } const RoadConfigManager& roadConfigs() const { return m_roadConfigManager; } ActionNeededManager& actionNeededManager() { return m_actionNeededManager; } const ActionNeededManager& actionNeededManager() const { return m_actionNeededManager; } + TechTreeManager& techTree() { return m_techTreeManager; } + const TechTreeManager& techTree() const { return m_techTreeManager; } + + // Tech Tree View & Research System + bool isTechTreeOpen() const { return m_isTechTreeOpen; } + void openTechTree(); + void closeTechTree(); + void toggleTechTree(); + + int calculatePlayerSciencePerTick(int playerId, Matrix2D *mapPoints, int numPoints05) const; + int getTechRemainingScience(const Player &player, const QString &techId) const; + bool selectTechForPlayer(int playerId, const QString &targetTechId, bool appendToQueue = false); + void removeTechFromPlayerQueue(int playerId, const QString &techId); + void clearPlayerResearchQueue(int playerId); + void advancePlayerResearch(Player &player, int scienceGenerated); + QStringList getTechUnlocks(const QString &techId) const; + QStringList getTechObsoletes(const QString &techId) const; const std::vector>& getUnits() const { return m_units; } const std::vector& getCities() const { return m_cities; } @@ -149,6 +169,7 @@ private: BuildingConfigManager m_buildingConfigManager; RoadConfigManager m_roadConfigManager; ActionNeededManager m_actionNeededManager; + TechTreeManager m_techTreeManager; std::vector> m_units; std::vector m_cities; std::vector m_floatingTexts; @@ -157,6 +178,7 @@ private: int m_selectedUnitId; int m_activeCityViewId; + bool m_isTechTreeOpen; ActionMode m_actionMode; float m_movementTickDuration; // Default 30.0s diff --git a/player.h b/player.h index 93b6c08..3a9881a 100644 --- a/player.h +++ b/player.h @@ -2,8 +2,12 @@ #define PLAYER_H #include +#include #include +#include #include +#include +#include struct Player { int id; @@ -12,11 +16,17 @@ struct Player { QColor secondaryColor; // National resources (shared across all cities of this nation) + // Note: science represents per-tick science generation rate (no longer stockpiled) int culture; int science; int gold; int faith; + // Technology Tree & Research Progression + std::set researchedTechs; + QMap techProgress; // Tech ID -> accumulated science towards that tech + std::vector researchQueue; // Ordered queue of Tech IDs (front is active research) + Player() : id(0), name("Unknown"), primaryColor(Qt::blue), secondaryColor(Qt::white), culture(10000), science(0), gold(0), faith(0) {} @@ -24,6 +34,55 @@ struct Player { int pCulture = 10000, int pScience = 0, int pGold = 0, int pFaith = 0) : id(pId), name(pName), primaryColor(primary), secondaryColor(secondary), culture(pCulture), science(pScience), gold(pGold), faith(pFaith) {} + + bool hasResearched(const QString &techId) const { + if (techId.isEmpty()) return true; + return researchedTechs.find(techId) != researchedTechs.end(); + } + + int getTechProgress(const QString &techId) const { + return techProgress.value(techId, 0); + } + + bool hasActiveResearch() const { + return !researchQueue.empty(); + } + + QString getActiveResearchTech() const { + return researchQueue.empty() ? QString() : researchQueue.front(); + } + + bool isTechQueued(const QString &techId) const { + return std::find(researchQueue.begin(), researchQueue.end(), techId) != researchQueue.end(); + } + + int getTechQueuePosition(const QString &techId) const { + for (size_t i = 0; i < researchQueue.size(); ++i) { + if (researchQueue[i] == techId) { + return static_cast(i) + 1; + } + } + return 0; + } + + bool arePrerequisitesMet(const QStringList &prerequisites) const { + for (const QString &pre : prerequisites) { + if (!hasResearched(pre)) return false; + } + return true; + } + + bool isItemUnlocked(const QString &requiredTech) const { + return requiredTech.isEmpty() || hasResearched(requiredTech); + } + + bool isItemObsolete(const QString &obsoleteTech) const { + return !obsoleteTech.isEmpty() && hasResearched(obsoleteTech); + } + + bool isItemBuildable(const QString &requiredTech, const QString &obsoleteTech) const { + return isItemUnlocked(requiredTech) && !isItemObsolete(obsoleteTech); + } }; class PlayerManager { diff --git a/qscenerenderer.cpp b/qscenerenderer.cpp index 61fae17..a3e0c1d 100644 --- a/qscenerenderer.cpp +++ b/qscenerenderer.cpp @@ -18,9 +18,9 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent) setMouseTracking(true); installEventFilter(this); - QImage img = QImage("/home/bart/project/tiletest/images/heightmap.jpg").convertToFormat(QImage::Format_RGB32); - QImage typeImg = QImage("/home/bart/project/tiletest/images/typemap.png").convertToFormat(QImage::Format_RGB32); - QImage riverImg = QImage("/home/bart/project/tiletest/images/rivermap.png").convertToFormat(QImage::Format_RGB32); + QImage img = QImage("/home/bart/git/Ascent-of-Man/images/heightmap.jpg").convertToFormat(QImage::Format_RGB32); + QImage typeImg = QImage("/home/bart/git/Ascent-of-Man/images/typemap.png").convertToFormat(QImage::Format_RGB32); + QImage riverImg = QImage("/home/bart/git/Ascent-of-Man/images/rivermap.png").convertToFormat(QImage::Format_RGB32); m_zoom = 4; m_point = new Matrix2D(img.width(), img.height()); m_numPoints05 = img.width() / MAP_WIDTH / 1.5 / 2; @@ -60,13 +60,13 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent) m_draggingMinimap = false; m_minimapZoom = 1.0f; - m_image_minimap = QImage("/home/bart/project/tiletest/images/minimap.png"); + m_image_minimap = QImage("/home/bart/git/Ascent-of-Man/images/minimap.png"); if (m_image_minimap.isNull()) { - m_image_minimap = QImage("/home/bart/project/tiletest/images/rivermap_preview.png"); + m_image_minimap = QImage("/home/bart/git/Ascent-of-Man/images/rivermap_preview.png"); } // Initialize GameManager and spawn demo units - GameManager::instance().init("/home/bart/project/tiletest", m_point, m_numPoints05); + GameManager::instance().init("/home/bart/git/Ascent-of-Man", m_point, m_numPoints05); m_lastFrameTime = 0.0f; m_elapsedTimer.start(); @@ -83,7 +83,7 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent) void QSceneRenderer::loadCameraConfig() { - QString configPath = "/home/bart/project/tiletest/config/game.yaml"; + QString configPath = "/home/bart/git/Ascent-of-Man/config/game.yaml"; YamlNode root = YamlParser::parseFile(configPath); if (root.hasChild("camera")) { YamlNode cam = root["camera"]; @@ -466,7 +466,7 @@ QImage QSceneRenderer::getTintedIcon(const QString &path, const QColor &color) } if (!m_iconCache.contains(path)) { - QString fullPath = path.startsWith("/") ? path : ("/home/bart/project/tiletest/" + path); + QString fullPath = path.startsWith("/") ? path : ("/home/bart/git/Ascent-of-Man/" + path); QImage loaded(fullPath); if (loaded.isNull()) { loaded = QImage(32, 32, QImage::Format_ARGB32); @@ -529,7 +529,7 @@ void QSceneRenderer::drawTickBar(QPainter &painter) { painter.setRenderHint(QPainter::Antialiasing, true); - int barW = 690, barH = 34; + int barW = 840, barH = 34; QRect barRect(16, 16, barW, barH); // Dark glass card @@ -557,27 +557,72 @@ void QSceneRenderer::drawTickBar(QPainter &painter) painter.setBrush(QColor(60, 180, 255)); painter.drawRoundedRect(progFg, 3, 3); - // 2. Player 1 National Resources (Empire-wide) + // 2. Player 1 National Resources (Empire-wide; Science shows per-tick rate, not saved stockpile) const Player *p1 = GameManager::instance().players().getPlayer(1); + m_scienceBarHitRect = QRect(); + m_techTreeBtnRect = QRect(); if (p1) { - struct ResItem { QString icon; int val; QColor col; }; + int sciRate = GameManager::instance().calculatePlayerSciencePerTick(1, m_point, m_numPoints05); + struct ResItem { QString icon; QString text; QColor col; bool isScience; }; std::vector natRes = { - {"images/resources/cultuur.png", p1->culture, QColor(215, 110, 255)}, - {"images/resources/kennis.png", p1->science, QColor(60, 190, 255)}, - {"images/resources/goud.png", p1->gold, QColor(255, 210, 50)}, - {"images/resources/geloof.png", p1->faith, QColor(255, 230, 80)} + {"images/resources/cultuur.png", QString("%L1").arg(p1->culture), QColor(215, 110, 255), false}, + {"images/resources/kennis.png", Localization::instance().get("ui.tech_tree.science_rate", "+%L1/tick").arg(sciRate), QColor(60, 190, 255), true}, + {"images/resources/goud.png", QString("%L1").arg(p1->gold), QColor(255, 210, 50), false}, + {"images/resources/geloof.png", QString("%L1").arg(p1->faith), QColor(255, 230, 80), false} }; - int startX = barRect.left() + 155; - int itemW = 86; + int startX = barRect.left() + 150; + int itemW = 92; for (size_t i = 0; i < natRes.size(); ++i) { int ix = startX + (int)i * itemW; + QRect slotRect(ix - 2, barRect.top() + 4, itemW - 2, 26); + if (natRes[i].isScience) { + m_scienceBarHitRect = slotRect; + } QImage icon = getTintedIcon(natRes[i].icon, natRes[i].col); painter.drawImage(QRect(ix, barRect.top() + 9, 16, 16), icon); painter.setFont(QFont("sans-serif", 8, QFont::Bold)); painter.setPen(natRes[i].col); - painter.drawText(QRect(ix + 20, barRect.top() + 8, itemW - 22, 18), Qt::AlignLeft | Qt::AlignVCenter, QString("%L1").arg(natRes[i].val)); + painter.drawText(QRect(ix + 20, barRect.top() + 8, itemW - 22, 18), Qt::AlignLeft | Qt::AlignVCenter, natRes[i].text); + } + + // 2b. Active Research / Tech Tree Button + m_techTreeBtnRect = QRect(barRect.left() + 520, barRect.top() + 5, 158, 24); + bool treeOpen = GameManager::instance().isTechTreeOpen(); + bool hasRes = p1->hasActiveResearch(); + painter.setBrush(treeOpen ? QColor(35, 80, 130, 245) : (hasRes ? QColor(22, 42, 68, 235) : QColor(45, 35, 25, 235))); + painter.setPen(QPen(treeOpen ? QColor(100, 220, 255) : (hasRes ? QColor(70, 175, 240) : QColor(255, 190, 60)), 1.3)); + painter.drawRoundedRect(m_techTreeBtnRect, 4, 4); + + if (hasRes) { + QString activeTechId = p1->getActiveResearchTech(); + const TechConfig *tCfg = GameManager::instance().techTree().getTech(activeTechId); + QString tName = tCfg ? tCfg->name : activeTechId; + int prog = p1->getTechProgress(activeTechId); + int cost = tCfg ? std::max(1, tCfg->scienceCost) : 1; + float frac = std::max(0.0f, std::min(1.0f, (float)prog / (float)cost)); + int pct = (int)(frac * 100.0f); + + // Mini progress bar at bottom of button + QRect miniBg(m_techTreeBtnRect.left() + 4, m_techTreeBtnRect.bottom() - 4, m_techTreeBtnRect.width() - 8, 3); + painter.setBrush(QColor(15, 22, 32)); + painter.setPen(Qt::NoPen); + painter.drawRoundedRect(miniBg, 1, 1); + + QRect miniFg(m_techTreeBtnRect.left() + 4, m_techTreeBtnRect.bottom() - 4, (int)((m_techTreeBtnRect.width() - 8) * frac), 3); + painter.setBrush(QColor(60, 210, 255)); + painter.drawRoundedRect(miniFg, 1, 1); + + painter.setFont(QFont("sans-serif", 7, QFont::Bold)); + painter.setPen(QColor(220, 245, 255)); + QString btnText = QString("%1 (%2%) [T]").arg(tName).arg(pct); + painter.drawText(m_techTreeBtnRect.adjusted(4, 0, -4, -3), Qt::AlignCenter, btnText); + } else { + painter.setFont(QFont("sans-serif", 8, QFont::Bold)); + painter.setPen(QColor(255, 215, 90)); + painter.drawText(m_techTreeBtnRect, Qt::AlignCenter, + Localization::instance().get("ui.tech_tree.choose_research", "Choose Tech [T]")); } } @@ -1872,12 +1917,16 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM // List of available projects under selected tab int itemH = 50; if (m_cityProjectTab == 0) { - // Buildings tab + // Buildings tab (only show unlocked, non-obsolete buildings) const auto &buildings = GameManager::instance().buildingConfigs().getAllBuildings(); for (auto it = buildings.begin(); it != buildings.end(); ++it) { if (curY + itemH > prodPanelRect.bottom() - 6) break; const BuildingConfig &bCfg = it.value(); + if (player) { + if (!player->isItemUnlocked(bCfg.requiredTech)) continue; + if (player->isItemObsolete(bCfg.obsoleteTech)) continue; + } QRect itemRect(innerX, curY, innerW, itemH); bool alreadyBuilt = city->hasBuilding(bCfg.id); @@ -1972,12 +2021,16 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM curY += itemH + 6; } } else { - // Units tab + // Units tab (only show unlocked, non-obsolete units) const auto &units = GameManager::instance().unitConfigs().getAllConfigs(); for (auto it = units.begin(); it != units.end(); ++it) { if (curY + itemH > prodPanelRect.bottom() - 6) break; const UnitConfig &uCfg = it.value(); + if (player) { + if (!player->isItemUnlocked(uCfg.requiredTech)) continue; + if (player->isItemObsolete(uCfg.obsoleteTech)) continue; + } QRect itemRect(innerX, curY, innerW, itemH); bool isActiveProj = (city->hasActiveProject() && city->currentProject.type == CityProjectType::Unit && city->currentProject.id == uCfg.id); @@ -2367,6 +2420,47 @@ void QSceneRenderer::mousePressEvent(QMouseEvent *event) return; // In City View, ignore all other clicks } + // 0b. If Tech Tree is open: handle Tech Tree interactions + if (GameManager::instance().isTechTreeOpen()) { + if (m_closeTechTreeBtnRect.contains(event->pos()) || m_techTreeBtnRect.contains(event->pos())) { + m_draggingTechTree = false; + GameManager::instance().closeTechTree(); + update(); + return; + } + if (m_clearTechQueueBtnRect.contains(event->pos()) && event->button() == Qt::LeftButton) { + GameManager::instance().clearPlayerResearchQueue(1); + update(); + return; + } + // Check queue chips in header + for (const auto &chip : m_techQueueChipHits) { + if (chip.rect.contains(event->pos())) { + if (event->button() == Qt::RightButton || event->button() == Qt::LeftButton) { + GameManager::instance().removeTechFromPlayerQueue(1, chip.techId); + update(); + } + return; + } + } + // Start potential drag or click inside tech tree canvas + if (m_techTreeCanvasRect.contains(event->pos())) { + m_draggingTechTree = true; + m_techTreeDragMoved = false; + m_techTreeDragStartPos = event->pos(); + lastPos = event->pos(); + return; + } + return; // Consume clicks while Tech Tree modal is open + } + + // 0c. Top bar Tech Tree button or Science slot click + if ((m_techTreeBtnRect.contains(event->pos()) || m_scienceBarHitRect.contains(event->pos())) && event->button() == Qt::LeftButton) { + GameManager::instance().toggleTechTree(); + update(); + return; + } + // Action Queue items (Bottom HUD notification bar) for (const auto &hit : m_actionItemHits) { if (hit.dismissRect.contains(event->pos()) && event->button() == Qt::LeftButton) { @@ -2593,6 +2687,21 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event) { m_mouseCurrentPos = event->pos(); + if (GameManager::instance().isTechTreeOpen()) { + if (m_draggingTechTree && (event->buttons() & (Qt::LeftButton | Qt::MiddleButton | Qt::RightButton))) { + int dx = event->x() - lastPos.x(); + int dy = event->y() - lastPos.y(); + if ((event->pos() - m_techTreeDragStartPos).manhattanLength() > 6) { + m_techTreeDragMoved = true; + } + m_techTreeScrollX = std::max(0, m_techTreeScrollX - dx); + m_techTreeScrollY = std::max(0, m_techTreeScrollY - dy); + lastPos = event->pos(); + } + update(); + return; + } + // Disable panning, rotation and minimap drag in City View if (GameManager::instance().isInCityView()) { if (m_slotPressPending) { @@ -2631,7 +2740,7 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event) break; } } - if (onActionItem) { + if (onActionItem || m_techTreeBtnRect.contains(event->pos()) || m_scienceBarHitRect.contains(event->pos())) { setCursor(Qt::PointingHandCursor); } else { unsetCursor(); @@ -2642,9 +2751,29 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event) void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event) { - Q_UNUSED(event); m_draggingMinimap = false; + if (GameManager::instance().isTechTreeOpen()) { + if (m_draggingTechTree && !m_techTreeDragMoved) { + // Clicked (without dragging) inside the Tech Tree canvas + for (const auto &nodeHit : m_techNodeHits) { + if (nodeHit.rect.contains(event->pos())) { + if (event->button() == Qt::LeftButton) { + bool append = (event->modifiers() & Qt::ShiftModifier); + GameManager::instance().selectTechForPlayer(1, nodeHit.techId, append); + } else if (event->button() == Qt::RightButton) { + GameManager::instance().removeTechFromPlayerQueue(1, nodeHit.techId); + } + break; + } + } + } + m_draggingTechTree = false; + m_techTreeDragMoved = false; + update(); + return; + } + if (m_slotLongPressTimer && m_slotLongPressTimer->isActive()) { m_slotLongPressTimer->stop(); } @@ -2684,6 +2813,20 @@ void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event) void QSceneRenderer::keyPressEvent(QKeyEvent *e) { + if (e->key() == Qt::Key_T && !GameManager::instance().isInCityView()) { + GameManager::instance().toggleTechTree(); + update(); + return; + } + + if (GameManager::instance().isTechTreeOpen()) { + if (e->key() == Qt::Key_Escape) { + GameManager::instance().closeTechTree(); + update(); + } + return; + } + // In City View: disable WASD panning and Q/E rotation; only allow Escape to exit if (GameManager::instance().isInCityView()) { if (e->key() == Qt::Key_Escape) { @@ -2796,6 +2939,16 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e) void QSceneRenderer::wheelEvent(QWheelEvent *e) { + if (GameManager::instance().isTechTreeOpen()) { + if (e->modifiers() & Qt::ShiftModifier) { + m_techTreeScrollY = std::max(0, m_techTreeScrollY - e->delta() / 2); + } else { + m_techTreeScrollX = std::max(0, m_techTreeScrollX - e->delta() / 2); + } + update(); + return; + } + // Disable zooming in City View if (GameManager::instance().isInCityView()) { return; @@ -2973,6 +3126,9 @@ void QSceneRenderer::paintGL() drawUnitCard(painter, selected); } drawActionNeededQueue(painter); + if (GameManager::instance().isTechTreeOpen()) { + drawTechTreeOverlay(painter); + } } else { drawUnitsOverlay(painter, vMatrix); drawFloatingTexts(painter, vMatrix); @@ -3079,6 +3235,11 @@ void QSceneRenderer::drawActionNeededQueue(QPainter &painter) subtitle = Localization::instance().get("ui.action_needed.city_free_pop_desc", "+%1 Free Pop").arg(item.auxValue); accent = cfg.cityFreePopColor; icon = getTintedIcon("images/resources/citizen.png", accent); + } else if (item.type == ActionNeededType::NoResearch) { + title = Localization::instance().get("ui.action_needed.no_research_title", "RESEARCH"); + subtitle = Localization::instance().get("ui.action_needed.no_research_desc", "Choose technology"); + accent = cfg.noResearchColor; + icon = getTintedIcon("images/resources/kennis.png", accent); } // Draw card background @@ -3140,10 +3301,21 @@ void QSceneRenderer::drawActionNeededQueue(QPainter &painter) void QSceneRenderer::handleActionItemClick(ActionNeededType type, int entityId) { + if (type == ActionNeededType::NoResearch) { + if (GameManager::instance().isInCityView()) { + GameManager::instance().exitCityView(); + } + GameManager::instance().openTechTree(); + return; + } + if (type == ActionNeededType::UnitIdle) { if (GameManager::instance().isInCityView()) { GameManager::instance().exitCityView(); } + if (GameManager::instance().isTechTreeOpen()) { + GameManager::instance().closeTechTree(); + } Unit *u = GameManager::instance().getUnitById(entityId); if (u) { GameManager::instance().selectUnit(entityId); @@ -3167,3 +3339,386 @@ void QSceneRenderer::handleActionItemClick(ActionNeededType type, int entityId) } } } + +void QSceneRenderer::drawTechTreeOverlay(QPainter &painter) +{ + m_techNodeHits.clear(); + m_techQueueChipHits.clear(); + m_closeTechTreeBtnRect = QRect(); + m_clearTechQueueBtnRect = QRect(); + + const Player *player = GameManager::instance().players().getPlayer(1); + if (!player) return; + + painter.setRenderHint(QPainter::Antialiasing, true); + + // Modal container below the top tick bar + QRect modalRect(16, 56, width() - 32, height() - 72); + painter.setBrush(QColor(12, 17, 26, 248)); + painter.setPen(QPen(QColor(70, 140, 210, 220), 2.0)); + painter.drawRoundedRect(modalRect, 10, 10); + + int headerH = 84; + QRect headerRect(modalRect.left() + 14, modalRect.top() + 10, modalRect.width() - 28, headerH); + + // 1. Title & Science per tick rate + painter.setFont(QFont("sans-serif", 13, QFont::Bold)); + painter.setPen(QColor(235, 245, 255)); + painter.drawText(QRect(headerRect.left(), headerRect.top(), 240, 26), + Qt::AlignLeft | Qt::AlignVCenter, + Localization::instance().get("ui.tech_tree.title", "TECHNOLOGY TREE")); + + int sciRate = GameManager::instance().calculatePlayerSciencePerTick(1, m_point, m_numPoints05); + QRect sciBadgeRect(headerRect.left() + 220, headerRect.top() + 2, 130, 24); + painter.setBrush(QColor(20, 40, 65, 230)); + painter.setPen(QPen(QColor(60, 190, 255), 1.2)); + painter.drawRoundedRect(sciBadgeRect, 5, 5); + + QImage sciIcon = getTintedIcon("images/resources/kennis.png", QColor(60, 200, 255)); + painter.drawImage(QRect(sciBadgeRect.left() + 6, sciBadgeRect.top() + 4, 16, 16), sciIcon); + painter.setFont(QFont("sans-serif", 8, QFont::Bold)); + painter.setPen(QColor(90, 215, 255)); + painter.drawText(QRect(sciBadgeRect.left() + 26, sciBadgeRect.top(), sciBadgeRect.width() - 30, 24), + Qt::AlignLeft | Qt::AlignVCenter, + Localization::instance().get("ui.tech_tree.science_rate", "+%L1/tick").arg(sciRate)); + + // Close button [X] + m_closeTechTreeBtnRect = QRect(headerRect.right() - 88, headerRect.top() + 2, 88, 26); + painter.setBrush(QColor(45, 30, 40, 230)); + painter.setPen(QPen(QColor(220, 110, 120), 1.3)); + painter.drawRoundedRect(m_closeTechTreeBtnRect, 5, 5); + painter.setFont(QFont("sans-serif", 8, QFont::Bold)); + painter.setPen(QColor(255, 210, 215)); + painter.drawText(m_closeTechTreeBtnRect, Qt::AlignCenter, + Localization::instance().get("ui.tech_tree.close_btn", "[X] Close")); + + // 2. Active Research & Queue Strip in Header + QRect queueStripRect(headerRect.left(), headerRect.top() + 34, headerRect.width(), 44); + painter.setBrush(QColor(18, 25, 38, 230)); + painter.setPen(QPen(QColor(50, 80, 120, 160), 1.0)); + painter.drawRoundedRect(queueStripRect, 6, 6); + + if (player->researchQueue.empty()) { + painter.setFont(QFont("sans-serif", 8, QFont::StyleItalic)); + painter.setPen(QColor(175, 195, 220)); + painter.drawText(queueStripRect.adjusted(14, 0, -14, 0), Qt::AlignLeft | Qt::AlignVCenter, + Localization::instance().get("ui.tech_tree.no_active_research", + "No active research - Click any technology node below to start researching")); + } else { + painter.setFont(QFont("sans-serif", 8, QFont::Bold)); + painter.setPen(QColor(160, 200, 240)); + QString qLabel = Localization::instance().get("ui.tech_tree.queue_label", "QUEUE (%1):") + .arg(player->researchQueue.size()); + painter.drawText(QRect(queueStripRect.left() + 10, queueStripRect.top(), 85, queueStripRect.height()), + Qt::AlignLeft | Qt::AlignVCenter, qLabel); + + int chipX = queueStripRect.left() + 96; + int maxChipRight = queueStripRect.right() - 100; + + for (size_t qi = 0; qi < player->researchQueue.size(); ++qi) { + QString qTechId = player->researchQueue[qi]; + const TechConfig *qCfg = GameManager::instance().techTree().getTech(qTechId); + if (!qCfg) continue; + + int prog = player->getTechProgress(qTechId); + int rem = std::max(0, qCfg->scienceCost - prog); + int pct = (int)(100.0f * (float)prog / (float)std::max(1, qCfg->scienceCost)); + QString chipText = QString("#%1 %2 (%3% | %L4 left)").arg(qi + 1).arg(qCfg->name).arg(pct).arg(rem); + + painter.setFont(QFont("sans-serif", 7, QFont::Bold)); + int chipW = painter.fontMetrics().horizontalAdvance(chipText) + 18; + if (chipX + chipW > maxChipRight) break; + + QRect chipRect(chipX, queueStripRect.top() + 8, chipW, 28); + bool isFirst = (qi == 0); + painter.setBrush(isFirst ? QColor(28, 65, 105, 245) : QColor(24, 38, 60, 230)); + painter.setPen(QPen(isFirst ? QColor(80, 215, 255) : QColor(70, 120, 180), isFirst ? 1.5 : 1.0)); + painter.drawRoundedRect(chipRect, 4, 4); + + painter.setPen(isFirst ? QColor(230, 250, 255) : QColor(190, 215, 245)); + painter.drawText(chipRect, Qt::AlignCenter, chipText); + + m_techQueueChipHits.push_back({chipRect, qTechId}); + chipX += chipW + 8; + } + + // Clear Queue Button + m_clearTechQueueBtnRect = QRect(queueStripRect.right() - 90, queueStripRect.top() + 8, 82, 28); + painter.setBrush(QColor(55, 28, 34, 225)); + painter.setPen(QPen(QColor(210, 90, 95), 1.0)); + painter.drawRoundedRect(m_clearTechQueueBtnRect, 4, 4); + painter.setFont(QFont("sans-serif", 7, QFont::Bold)); + painter.setPen(QColor(255, 165, 170)); + painter.drawText(m_clearTechQueueBtnRect, Qt::AlignCenter, + Localization::instance().get("ui.tech_tree.clear_queue_btn", "Clear Queue")); + } + + // 3. Tech Tree Canvas (Scrollable Grid of Technology Cards + Dependency Connectors) + int footerH = 26; + m_techTreeCanvasRect = QRect(modalRect.left() + 14, + headerRect.bottom() + 8, + modalRect.width() - 28, + modalRect.height() - headerH - footerH - 26); + + painter.setBrush(QColor(9, 13, 20, 245)); + painter.setPen(QPen(QColor(45, 70, 105, 180), 1.2)); + painter.drawRoundedRect(m_techTreeCanvasRect, 8, 8); + + // Layout constants for tech cards + const int nodeW = 236; + const int nodeH = 104; + const int colSpacing = 66; + const int rowSpacing = 22; + const int padX = 24; + const int padY = 20; + + int maxCol = GameManager::instance().techTree().getMaxColumn(); + int maxRow = GameManager::instance().techTree().getMaxRow(); + int contentW = padX * 2 + (maxCol + 1) * nodeW + maxCol * colSpacing; + int contentH = padY * 2 + (maxRow + 1) * nodeH + maxRow * rowSpacing; + + int maxScrollX = std::max(0, contentW - m_techTreeCanvasRect.width()); + int maxScrollY = std::max(0, contentH - m_techTreeCanvasRect.height()); + m_techTreeScrollX = std::max(0, std::min(m_techTreeScrollX, maxScrollX)); + m_techTreeScrollY = std::max(0, std::min(m_techTreeScrollY, maxScrollY)); + + const auto &allTechs = GameManager::instance().techTree().getAllTechs(); + const auto &orderedIds = GameManager::instance().techTree().getOrderedTechIds(); + + // Compute screen rectangles for all tech nodes + QMap nodeRects; + for (const QString &tId : orderedIds) { + const TechConfig &cfg = allTechs[tId]; + int nx = m_techTreeCanvasRect.left() + padX + cfg.column * (nodeW + colSpacing) - m_techTreeScrollX; + int ny = m_techTreeCanvasRect.top() + padY + cfg.row * (nodeH + rowSpacing) - m_techTreeScrollY; + nodeRects[tId] = QRect(nx, ny, nodeW, nodeH); + } + + painter.save(); + painter.setClipRect(m_techTreeCanvasRect.adjusted(2, 2, -2, -2)); + + // Subtle column tier background stripes + for (int c = 0; c <= maxCol; ++c) { + int colLeft = m_techTreeCanvasRect.left() + padX + c * (nodeW + colSpacing) - m_techTreeScrollX - 10; + QRect colBand(colLeft, m_techTreeCanvasRect.top() + 4, nodeW + 20, m_techTreeCanvasRect.height() - 8); + if (c % 2 == 1) { + painter.fillRect(colBand, QColor(18, 26, 40, 65)); + } + } + + // 3a. Draw Prerequisite Connector Curves (from prereq right edge to dependent left edge) + for (const QString &tId : orderedIds) { + const TechConfig &cfg = allTechs[tId]; + QRect targetR = nodeRects[tId]; + bool targetResearched = player->hasResearched(tId); + bool targetQueued = player->isTechQueued(tId); + + for (const QString &preId : cfg.prerequisites) { + if (!nodeRects.contains(preId)) continue; + QRect srcR = nodeRects[preId]; + + QPointF pStart(srcR.right(), srcR.center().y()); + QPointF pEnd(targetR.left(), targetR.center().y()); + float ctrlOffset = std::max(28.0f, (float)(pEnd.x() - pStart.x()) * 0.48f); + + QPainterPath curve; + curve.moveTo(pStart); + curve.cubicTo(QPointF(pStart.x() + ctrlOffset, pStart.y()), + QPointF(pEnd.x() - ctrlOffset, pEnd.y()), + pEnd); + + bool preResearched = player->hasResearched(preId); + bool preQueued = player->isTechQueued(preId); + + QColor lineCol; + float lineW = 2.0f; + if (preResearched && targetResearched) { + lineCol = QColor(70, 215, 115, 220); + lineW = 2.5f; + } else if (preResearched) { + lineCol = QColor(80, 195, 255, 210); + lineW = 2.2f; + } else if (preQueued && targetQueued) { + lineCol = QColor(255, 210, 70, 210); + lineW = 2.2f; + } else { + lineCol = QColor(70, 95, 130, 150); + lineW = 1.6f; + } + + painter.setPen(QPen(lineCol, lineW)); + painter.setBrush(Qt::NoBrush); + painter.drawPath(curve); + + // Arrowhead at pEnd + QPolygonF arrowHead; + arrowHead << pEnd + << QPointF(pEnd.x() - 7.0f, pEnd.y() - 4.5f) + << QPointF(pEnd.x() - 7.0f, pEnd.y() + 4.5f); + painter.setBrush(lineCol); + painter.setPen(Qt::NoPen); + painter.drawPolygon(arrowHead); + } + } + + // 3b. Draw Technology Cards + int effectiveSciRate = std::max(1, sciRate); + for (const QString &tId : orderedIds) { + const TechConfig &cfg = allTechs[tId]; + QRect nRect = nodeRects[tId]; + if (!m_techTreeCanvasRect.intersects(nRect)) continue; + + m_techNodeHits.push_back({nRect, tId}); + + bool isResearched = player->hasResearched(tId); + int qPos = player->getTechQueuePosition(tId); + bool isActive = (qPos == 1); + bool isQueued = (qPos > 1); + bool isAvailable = !isResearched && player->arePrerequisitesMet(cfg.prerequisites); + bool isHovered = nRect.contains(m_mouseCurrentPos); + + int curProg = isResearched ? cfg.scienceCost : player->getTechProgress(tId); + int remSci = std::max(0, cfg.scienceCost - curProg); + float progFrac = std::max(0.0f, std::min(1.0f, (float)curProg / (float)std::max(1, cfg.scienceCost))); + int pct = (int)(progFrac * 100.0f); + int estTicks = (int)std::ceil((float)remSci / (float)effectiveSciRate); + + // Card background & border based on state + QColor bgCol, borderCol, badgeBgCol, badgeTextCol; + QString statusText; + + if (isResearched) { + bgCol = QColor(20, 42, 30, 240); + borderCol = QColor(75, 220, 120); + badgeBgCol = QColor(30, 75, 45); + badgeTextCol = QColor(120, 255, 155); + statusText = Localization::instance().get("ui.tech_tree.status_researched", "RESEARCHED"); + } else if (isActive) { + bgCol = QColor(22, 46, 74, 245); + borderCol = QColor(80, 225, 255); + badgeBgCol = QColor(35, 95, 140); + badgeTextCol = QColor(160, 245, 255); + statusText = Localization::instance().get("ui.tech_tree.status_researching", "ACTIVE #1"); + } else if (isQueued) { + bgCol = QColor(24, 38, 62, 240); + borderCol = QColor(255, 205, 65); + badgeBgCol = QColor(75, 60, 20); + badgeTextCol = QColor(255, 225, 100); + statusText = Localization::instance().get("ui.tech_tree.status_queued", "QUEUED #%1").arg(qPos); + } else if (isAvailable) { + bgCol = QColor(22, 34, 52, 235); + borderCol = QColor(90, 165, 235); + badgeBgCol = QColor(30, 55, 85); + badgeTextCol = QColor(160, 215, 255); + statusText = Localization::instance().get("ui.tech_tree.status_available", "AVAILABLE"); + } else { + bgCol = QColor(18, 23, 34, 225); + borderCol = QColor(65, 80, 105); + badgeBgCol = QColor(32, 38, 52); + badgeTextCol = QColor(150, 165, 190); + statusText = Localization::instance().get("ui.tech_tree.status_locked", "LOCKED"); + } + + if (isHovered && !isResearched) { + bgCol = bgCol.lighter(122); + borderCol = borderCol.lighter(125); + } + + painter.setBrush(bgCol); + painter.setPen(QPen(borderCol, (isActive || isQueued || isHovered) ? 2.0 : 1.3)); + painter.drawRoundedRect(nRect, 7, 7); + + // Icon + QImage tIcon = getTintedIcon(cfg.iconPath, isResearched ? QColor(100, 245, 140) : QColor(90, 205, 255)); + painter.drawImage(QRect(nRect.left() + 8, nRect.top() + 8, 22, 22), tIcon); + + // Tech Name + painter.setFont(QFont("sans-serif", 8, QFont::Bold)); + painter.setPen(QColor(240, 248, 255)); + painter.drawText(QRect(nRect.left() + 34, nRect.top() + 6, nRect.width() - 112, 16), + Qt::AlignLeft | Qt::AlignVCenter, cfg.name); + + // Status Pill (Top-Right) + QRect statusRect(nRect.right() - 74, nRect.top() + 6, 68, 16); + painter.setBrush(badgeBgCol); + painter.setPen(QPen(borderCol, 0.8)); + painter.drawRoundedRect(statusRect, 3, 3); + painter.setFont(QFont("sans-serif", 6, QFont::Bold)); + painter.setPen(badgeTextCol); + painter.drawText(statusRect, Qt::AlignCenter, statusText); + + // Science Cost / Partial Progress Bar + QRect progBarBg(nRect.left() + 34, nRect.top() + 24, nRect.width() - 42, 13); + painter.setBrush(QColor(12, 18, 26)); + painter.setPen(QPen(QColor(45, 65, 95), 0.8)); + painter.drawRoundedRect(progBarBg, 3, 3); + + if (curProg > 0) { + QRect progBarFg(progBarBg.left(), progBarBg.top(), (int)(progBarBg.width() * progFrac), progBarBg.height()); + painter.setBrush(isResearched ? QColor(50, 175, 90) : QColor(45, 155, 225)); + painter.setPen(Qt::NoPen); + painter.drawRoundedRect(progBarFg, 3, 3); + } + + painter.setFont(QFont("sans-serif", 6, QFont::Bold)); + painter.setPen(Qt::white); + QString costLabel; + if (isResearched) { + costLabel = QString("%L1 / %L2 (100%)").arg(cfg.scienceCost).arg(cfg.scienceCost); + } else if (curProg > 0) { + costLabel = QString("%L1 / %L2 (%3%) - ~%4t") + .arg(curProg).arg(cfg.scienceCost).arg(pct).arg(estTicks); + } else { + QString tickWord = (estTicks == 1) + ? Localization::instance().get("ui.tech_tree.tick_singular", "tick") + : Localization::instance().get("ui.tech_tree.tick_plural", "ticks"); + costLabel = Localization::instance().get("ui.tech_tree.cost_format", "%L1 Science (~%2 %3)") + .arg(cfg.scienceCost).arg(estTicks).arg(tickWord); + } + painter.drawText(progBarBg, Qt::AlignCenter, costLabel); + + // Unlocks & Obsoletes lines + int infoY = nRect.top() + 42; + QStringList unlocks = GameManager::instance().getTechUnlocks(tId); + QStringList obsoletes = GameManager::instance().getTechObsoletes(tId); + + if (!unlocks.isEmpty()) { + painter.setFont(QFont("sans-serif", 7, QFont::Bold)); + painter.setPen(QColor(110, 240, 150)); + QString uStr = Localization::instance().get("ui.tech_tree.unlocks_label", "Unlocks: %1") + .arg(unlocks.join(", ")); + painter.drawText(QRect(nRect.left() + 8, infoY, nRect.width() - 16, 14), + Qt::AlignLeft | Qt::AlignVCenter, uStr); + infoY += 14; + } + + if (!obsoletes.isEmpty()) { + painter.setFont(QFont("sans-serif", 7, QFont::Bold)); + painter.setPen(QColor(255, 155, 95)); + QString oStr = Localization::instance().get("ui.tech_tree.obsoletes_label", "Obsoletes: %1") + .arg(obsoletes.join(", ")); + painter.drawText(QRect(nRect.left() + 8, infoY, nRect.width() - 16, 14), + Qt::AlignLeft | Qt::AlignVCenter, oStr); + infoY += 14; + } + + // Description text in remaining bottom space + if (infoY + 12 <= nRect.bottom() - 2) { + painter.setFont(QFont("sans-serif", 6)); + painter.setPen(QColor(165, 185, 210)); + painter.drawText(QRect(nRect.left() + 8, infoY, nRect.width() - 16, nRect.bottom() - infoY - 3), + Qt::AlignLeft | Qt::AlignTop | Qt::TextWordWrap, cfg.description); + } + } + + painter.restore(); + + // 4. Footer Help Text + QRect footerRect(modalRect.left() + 16, modalRect.bottom() - footerH - 4, modalRect.width() - 32, footerH); + painter.setFont(QFont("sans-serif", 7, QFont::StyleItalic)); + painter.setPen(QColor(150, 180, 215)); + painter.drawText(footerRect, Qt::AlignCenter, + Localization::instance().get("ui.tech_tree.help_text", + "Left-click a tech to research (prerequisites auto-queued by least remaining work) | Shift+Click to append | Right-click to remove | Drag/Scroll to pan")); +} + diff --git a/qscenerenderer.h b/qscenerenderer.h index 0e2a2bc..69cf0e6 100644 --- a/qscenerenderer.h +++ b/qscenerenderer.h @@ -191,6 +191,31 @@ protected: void drawActionNeededQueue(QPainter &painter); void handleActionItemClick(ActionNeededType type, int entityId); + + // Technology Tree UI Overlay + void drawTechTreeOverlay(QPainter &painter); + QRect m_techTreeBtnRect; + QRect m_scienceBarHitRect; + QRect m_closeTechTreeBtnRect; + QRect m_clearTechQueueBtnRect; + QRect m_techTreeCanvasRect; + int m_techTreeScrollX = 0; + int m_techTreeScrollY = 0; + bool m_draggingTechTree = false; + bool m_techTreeDragMoved = false; + QPoint m_techTreeDragStartPos; + + struct TechNodeHit { + QRect rect; + QString techId; + }; + std::vector m_techNodeHits; + + struct TechQueueChipHit { + QRect rect; + QString techId; + }; + std::vector m_techQueueChipHits; }; #endif // QSCENERENDERER_H diff --git a/techconfig.cpp b/techconfig.cpp new file mode 100644 index 0000000..26883e2 --- /dev/null +++ b/techconfig.cpp @@ -0,0 +1,131 @@ +#include "techconfig.h" +#include "yamlparser.h" +#include +#include + +TechTreeManager::TechTreeManager() + : m_maxColumn(0), m_maxRow(0) +{ +} + +int TechTreeManager::computeTechDepth(const QString &techId, QMap &visited) const +{ + if (visited.contains(techId)) { + return visited.value(techId); + } + visited[techId] = 0; // Guard against cycles + + auto it = m_techs.find(techId); + if (it == m_techs.end()) { + return 0; + } + + int maxPrereqDepth = -1; + for (const QString &pre : it.value().prerequisites) { + if (m_techs.contains(pre)) { + int d = computeTechDepth(pre, visited); + if (d > maxPrereqDepth) { + maxPrereqDepth = d; + } + } + } + + int depth = maxPrereqDepth + 1; + visited[techId] = depth; + return depth; +} + +bool TechTreeManager::loadConfigFile(const QString &filePath) +{ + YamlNode root = YamlParser::parseFile(filePath); + if (root.children.isEmpty()) { + qWarning() << "Failed to parse technologies config YAML:" << filePath; + return false; + } + + m_techs.clear(); + m_orderedTechIds.clear(); + m_maxColumn = 0; + m_maxRow = 0; + + const YamlNode &techsNode = root.hasChild("technologies") ? root["technologies"] : root; + QMap hasExplicitColumn; + QMap hasExplicitRow; + + for (auto it = techsNode.children.begin(); it != techsNode.children.end(); ++it) { + const YamlNode &node = it.value(); + TechConfig cfg; + cfg.id = node.hasChild("id") ? node["id"].asString() : it.key(); + cfg.name = node.hasChild("name") ? node["name"].asString() : cfg.id; + cfg.scienceCost = node.hasChild("science_cost") ? node["science_cost"].asInt(1000) : 1000; + if (node.hasChild("cost") && !node.hasChild("science_cost")) { + cfg.scienceCost = node["cost"].asInt(1000); + } + cfg.scienceCost = std::max(1, cfg.scienceCost); + + if (node.hasChild("prerequisites")) { + cfg.prerequisites = node["prerequisites"].listValues; + } + if (node.hasChild("icon")) { + cfg.iconPath = node["icon"].asString("images/resources/kennis.png"); + } + if (node.hasChild("description")) { + cfg.description = node["description"].asString(""); + } + + if (node.hasChild("column")) { + cfg.column = node["column"].asInt(0); + hasExplicitColumn[cfg.id] = true; + } + if (node.hasChild("row")) { + cfg.row = node["row"].asInt(0); + hasExplicitRow[cfg.id] = true; + } + + m_techs[cfg.id] = cfg; + } + + // Auto-calculate column and row for any tech without explicit coordinates + QMap depthCache; + QMap nextRowInCol; + + for (auto it = m_techs.begin(); it != m_techs.end(); ++it) { + if (!hasExplicitColumn.value(it.key(), false)) { + it.value().column = computeTechDepth(it.key(), depthCache); + } + if (hasExplicitRow.value(it.key(), false)) { + nextRowInCol[it.value().column] = std::max(nextRowInCol.value(it.value().column, 0), it.value().row + 1); + } + } + + for (auto it = m_techs.begin(); it != m_techs.end(); ++it) { + if (!hasExplicitRow.value(it.key(), false)) { + int col = it.value().column; + it.value().row = nextRowInCol.value(col, 0); + nextRowInCol[col] = it.value().row + 1; + } + m_maxColumn = std::max(m_maxColumn, it.value().column); + m_maxRow = std::max(m_maxRow, it.value().row); + m_orderedTechIds.push_back(it.key()); + } + + std::sort(m_orderedTechIds.begin(), m_orderedTechIds.end(), [&](const QString &a, const QString &b) { + const TechConfig &ta = m_techs[a]; + const TechConfig &tb = m_techs[b]; + if (ta.column != tb.column) return ta.column < tb.column; + if (ta.row != tb.row) return ta.row < tb.row; + return ta.id < tb.id; + }); + + qDebug() << "TechTreeManager loaded" << m_techs.size() << "technologies from" << filePath; + return !m_techs.isEmpty(); +} + +const TechConfig* TechTreeManager::getTech(const QString &id) const +{ + auto it = m_techs.find(id); + if (it != m_techs.end()) { + return &it.value(); + } + return nullptr; +} diff --git a/techconfig.h b/techconfig.h new file mode 100644 index 0000000..2a21545 --- /dev/null +++ b/techconfig.h @@ -0,0 +1,45 @@ +#ifndef TECHCONFIG_H +#define TECHCONFIG_H + +#include +#include +#include +#include + +struct TechConfig { + QString id; + QString name; + int scienceCost; + QStringList prerequisites; + int column; + int row; + QString iconPath; + QString description; + + TechConfig() + : id(""), name(""), scienceCost(1000), column(0), row(0), + iconPath("images/resources/kennis.png"), description("") {} +}; + +class TechTreeManager { +public: + TechTreeManager(); + + bool loadConfigFile(const QString &filePath); + const TechConfig* getTech(const QString &id) const; + const QMap& getAllTechs() const { return m_techs; } + const std::vector& getOrderedTechIds() const { return m_orderedTechIds; } + + int getMaxColumn() const { return m_maxColumn; } + int getMaxRow() const { return m_maxRow; } + +private: + int computeTechDepth(const QString &techId, QMap &visited) const; + + QMap m_techs; + std::vector m_orderedTechIds; + int m_maxColumn; + int m_maxRow; +}; + +#endif // TECHCONFIG_H diff --git a/tiletest.pro b/tiletest.pro index 64493f8..ba3183a 100644 --- a/tiletest.pro +++ b/tiletest.pro @@ -33,7 +33,8 @@ SOURCES += main.cpp \ localization.cpp \ terrainmovementmanager.cpp \ roadconfig.cpp \ - actionneededmanager.cpp + actionneededmanager.cpp \ + techconfig.cpp HEADERS += mainwindow.h \ qscenerenderer.h \ @@ -56,7 +57,8 @@ HEADERS += mainwindow.h \ localization.h \ terrainmovementmanager.h \ roadconfig.h \ - actionneededmanager.h + actionneededmanager.h \ + techconfig.h FORMS += mainwindow.ui diff --git a/unitconfig.cpp b/unitconfig.cpp index a70d49c..28c2b58 100644 --- a/unitconfig.cpp +++ b/unitconfig.cpp @@ -53,6 +53,8 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath) cfg.attackRange = root.hasChild("attack_range") ? root["attack_range"].asInt(1) : 1; cfg.buildCharges = root.hasChild("build_charges") ? root["build_charges"].asInt(-1) : -1; cfg.infiniteCharges = root.hasChild("infinite_charges") ? root["infinite_charges"].asBool(cfg.buildCharges < 0) : (cfg.buildCharges < 0); + cfg.requiredTech = root.hasChild("required_tech") ? root["required_tech"].asString("") : ""; + cfg.obsoleteTech = root.hasChild("obsolete_tech") ? root["obsolete_tech"].asString("") : ""; cfg.iconPath = root.hasChild("icon") ? root["icon"].asString() : ""; if (root.hasChild("domain")) { diff --git a/unitconfig.h b/unitconfig.h index 4cfdac5..f71bdc6 100644 --- a/unitconfig.h +++ b/unitconfig.h @@ -44,6 +44,8 @@ struct UnitConfig { int attackRange; // 1 for melee, >1 for ranged int buildCharges; bool infiniteCharges; + QString requiredTech; + QString obsoleteTech; QString iconPath; QStringList actions; QMap actionSpeeds; // Duration in seconds per action (e.g. "boerderij_bouwen": 10.0f) @@ -54,7 +56,7 @@ struct UnitConfig { : id(""), name(""), category(UnitCategory::Builder), movementDomain(MovementDomain::Land), health(100), movementSpeed(2.0f), cost(10), laborCost(1000), attackDamage(0), attackRange(1), - buildCharges(-1), infiniteCharges(true), iconPath("") {} + buildCharges(-1), infiniteCharges(true), requiredTech(""), obsoleteTech(""), iconPath("") {} bool isRanged() const { return attackRange > 1; } bool isMilitary() const { return category == UnitCategory::Military; }