Implement technology tree, research queue by least remaining work, and unit/building tech availability & obsolescence

This commit is contained in:
2026-11-09 20:28:53 +01:00
parent d4bd9f4711
commit d266715418
30 changed files with 1809 additions and 45 deletions
+28 -1
View File
@@ -33,6 +33,9 @@ bool ActionNeededManager::loadConfigFile(const QString &filePath)
if (node.hasChild("track_city_free_population")) { if (node.hasChild("track_city_free_population")) {
m_config.trackCityFreePopulation = node["track_city_free_population"].asBool(true); 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")) { if (node.hasChild("max_visible_items")) {
m_config.maxVisibleItems = node["max_visible_items"].asInt(8); 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"); QString hex = node["city_free_pop_color"].asString("#46DC82");
m_config.cityFreePopColor = QColor(hex); 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")) { if (node.hasChild("card_bg_color")) {
QString hex = node["card_bg_color"].asString("#121822"); QString hex = node["card_bg_color"].asString("#121822");
m_config.cardBgColor = QColor(hex); m_config.cardBgColor = QColor(hex);
@@ -85,7 +92,8 @@ bool ActionNeededManager::loadConfigFile(const QString &filePath)
void ActionNeededManager::update(float deltaTime, int activePlayerId, void ActionNeededManager::update(float deltaTime, int activePlayerId,
const std::vector<std::shared_ptr<Unit>> &units, const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities) const std::vector<City> &cities,
bool needsResearch)
{ {
if (!m_config.enabled) return; if (!m_config.enabled) return;
@@ -93,6 +101,25 @@ void ActionNeededManager::update(float deltaTime, int activePlayerId,
std::set<QString> currentConditionKeys; std::set<QString> currentConditionKeys;
// 0. Player has no active research
if (m_config.trackNoResearch && needsResearch) {
QString key = QString("%1_%2").arg(static_cast<int>(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 // 1. Idle Units waiting for action
if (m_config.trackIdleUnits) { if (m_config.trackIdleUnits) {
for (const auto &u : units) { for (const auto &u : units) {
+6 -2
View File
@@ -12,7 +12,8 @@ class City;
enum class ActionNeededType { enum class ActionNeededType {
UnitIdle = 0, UnitIdle = 0,
CityNoProduction = 1, CityNoProduction = 1,
CityFreePopulation = 2 CityFreePopulation = 2,
NoResearch = 3
}; };
struct ActionItem { struct ActionItem {
@@ -33,6 +34,7 @@ struct ActionNeededConfig {
bool trackIdleUnits = true; bool trackIdleUnits = true;
bool trackCityNoProduction = true; bool trackCityNoProduction = true;
bool trackCityFreePopulation = true; bool trackCityFreePopulation = true;
bool trackNoResearch = true;
int maxVisibleItems = 8; int maxVisibleItems = 8;
int itemWidth = 155; int itemWidth = 155;
@@ -46,6 +48,7 @@ struct ActionNeededConfig {
QColor unitIdleColor = QColor(60, 160, 255); QColor unitIdleColor = QColor(60, 160, 255);
QColor cityNoProdColor = QColor(255, 160, 40); QColor cityNoProdColor = QColor(255, 160, 40);
QColor cityFreePopColor = QColor(70, 220, 130); QColor cityFreePopColor = QColor(70, 220, 130);
QColor noResearchColor = QColor(56, 200, 255);
QColor cardBgColor = QColor(18, 24, 34); QColor cardBgColor = QColor(18, 24, 34);
int cardBgAlpha = 235; int cardBgAlpha = 235;
}; };
@@ -59,7 +62,8 @@ public:
void update(float deltaTime, int activePlayerId, void update(float deltaTime, int activePlayerId,
const std::vector<std::shared_ptr<Unit>> &units, const std::vector<std::shared_ptr<Unit>> &units,
const std::vector<City> &cities); const std::vector<City> &cities,
bool needsResearch = false);
std::vector<ActionItem> getVisibleItems() const; std::vector<ActionItem> getVisibleItems() const;
int getVisibleCount() const; int getVisibleCount() const;
+2
View File
@@ -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.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.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.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.iconPath = it.value().hasChild("icon") ? it.value()["icon"].asString() : "";
bCfg.description = it.value().hasChild("description") ? it.value()["description"].asString() : ""; bCfg.description = it.value().hasChild("description") ? it.value()["description"].asString() : "";
m_buildings[bCfg.id] = bCfg; m_buildings[bCfg.id] = bCfg;
+3
View File
@@ -17,6 +17,8 @@ struct BuildingConfig {
int scienceBonus; int scienceBonus;
int faithBonus; int faithBonus;
int goldBonus; int goldBonus;
QString requiredTech;
QString obsoleteTech;
QString iconPath; QString iconPath;
QString description; QString description;
@@ -24,6 +26,7 @@ struct BuildingConfig {
: id(""), name(""), district("center"), laborCost(1000), : id(""), name(""), district("center"), laborCost(1000),
foodModifier(0.0f), foodBonus(0), laborBonus(0), cultureBonus(0), foodModifier(0.0f), foodBonus(0), laborBonus(0), cultureBonus(0),
scienceBonus(0), faithBonus(0), goldBonus(0), scienceBonus(0), faithBonus(0), goldBonus(0),
requiredTech(""), obsoleteTech(""),
iconPath(""), description("") {} iconPath(""), description("") {}
}; };
+2
View File
@@ -4,6 +4,7 @@ action_needed:
track_idle_units: true track_idle_units: true
track_city_no_production: true track_city_no_production: true
track_city_free_population: true track_city_free_population: true
track_no_research: true
max_visible_items: 8 max_visible_items: 8
# Visual & Layout parameters # Visual & Layout parameters
@@ -19,5 +20,6 @@ action_needed:
unit_idle_color: "#3CA0FF" unit_idle_color: "#3CA0FF"
city_no_prod_color: "#FFA028" city_no_prod_color: "#FFA028"
city_free_pop_color: "#46DC82" city_free_pop_color: "#46DC82"
no_research_color: "#38C8FF"
card_bg_color: "#121822" card_bg_color: "#121822"
card_bg_alpha: 235 card_bg_alpha: 235
+131 -8
View File
@@ -1,19 +1,55 @@
# City Buildings & Extensions configuration # City Buildings & Extensions configuration
# All gameplay values are configurable via YAML # All gameplay values, required technologies, and obsolescence technologies are configurable via YAML
districts: districts:
center: center:
id: "center" id: "center"
name: "City Center" name: "City Center"
max_buildings: 10 max_buildings: 12
description: "The urban core and administrative center of the city." description: "The urban core and administrative center of the city."
buildings: 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: granary:
id: "granary" id: "granary"
name: "Granary" name: "Granary"
district: "center" district: "center"
labor_cost: 1000 labor_cost: 1000
required_tech: "pottery"
obsolete_tech: ""
food_modifier: 0.50 food_modifier: 0.50
food_bonus: 0 food_bonus: 0
labor_bonus: 0 labor_bonus: 0
@@ -24,17 +60,104 @@ buildings:
icon: "images/improvements/granary.png" icon: "images/improvements/granary.png"
description: "+50% Food production in city" description: "+50% Food production in city"
monument: library:
id: "monument" id: "library"
name: "Monument" name: "Library"
district: "center" district: "center"
labor_cost: 800 labor_cost: 1200
required_tech: "writing"
obsolete_tech: ""
food_modifier: 0.0 food_modifier: 0.0
food_bonus: 0 food_bonus: 0
labor_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 science_bonus: 0
faith_bonus: 0 faith_bonus: 0
gold_bonus: 0 gold_bonus: 0
icon: "images/improvements/monument.png" 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"
+39 -1
View File
@@ -128,7 +128,7 @@ ui:
management_title: "%1 - CITY MANAGEMENT" management_title: "%1 - CITY MANAGEMENT"
close_btn: "[X] Close" close_btn: "[X] Close"
local_info: "Local: Population: %L1 (%2 Pop: %3 working, %4 free) | Labor: %L5" 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." help_text: "Click a slot to assign/free Pop | Hold a slot to choose resource | Buy tile with Culture."
current_production: "CURRENT PRODUCTION" current_production: "CURRENT PRODUCTION"
production_panel_title: "CITY PRODUCTION" production_panel_title: "CITY PRODUCTION"
@@ -160,6 +160,8 @@ ui:
queued_item_removed: "%1: Removed %2 from queue" queued_item_removed: "%1: Removed %2 from queue"
already_built: "%1 already built in this city!" already_built: "%1 already built in this city!"
already_queued: "%1 is already in the production queue!" 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" growth_float: "+%1 Inhabitants | +%2 Culture"
not_enough_culture: "Not enough Culture! (%1 needed)" not_enough_culture: "Not enough Culture! (%1 needed)"
worker_freed: "Pop worker freed" worker_freed: "Pop worker freed"
@@ -187,6 +189,42 @@ ui:
unit_idle_desc: "Needs orders" unit_idle_desc: "Needs orders"
city_no_production_desc: "No production" city_no_production_desc: "No production"
city_free_pop_desc: "+%1 Free Pop" city_free_pop_desc: "+%1 Free Pop"
no_research_title: "RESEARCH"
no_research_desc: "Choose technology"
dismiss_tip: "Right-click to dismiss" dismiss_tip: "Right-click to dismiss"
more_items: "+%1" 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"
+202
View File
@@ -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"
+16
View File
@@ -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
+5 -3
View File
@@ -6,7 +6,9 @@ health: 100
movement_speed: 2.0 movement_speed: 2.0
cost: 50 cost: 50
labor_cost: 1000 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 infinite_charges: true
icon: images/units/builder.png icon: images/units/builder.png
actions: actions:
@@ -14,5 +16,5 @@ actions:
- boerderij_bouwen - boerderij_bouwen
- build_road - build_road
action_speeds: action_speeds:
boerderij_bouwen: 10.0 # Duur in seconden die deze builder unit nodig heeft voor de actie boerderij_bouwen boerderij_bouwen: 10.0
build_road: 2 # Duur in ticks build_road: 2
+16
View File
@@ -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
+2
View File
@@ -8,6 +8,8 @@ cost: 60
labor_cost: 1500 labor_cost: 1500
attack_damage: 25 attack_damage: 25
attack_range: 1 attack_range: 1
required_tech: "sailing"
obsolete_tech: ""
icon: images/units/galley.png icon: images/units/galley.png
actions: actions:
- verplaatsen - verplaatsen
+16
View File
@@ -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
+16
View File
@@ -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
+16
View File
@@ -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
+2
View File
@@ -6,6 +6,8 @@ health: 100
movement_speed: 2.0 movement_speed: 2.0
cost: 80 cost: 80
labor_cost: 2000 labor_cost: 2000
required_tech: ""
obsolete_tech: ""
icon: images/units/settler.png icon: images/units/settler.png
actions: actions:
- verplaatsen - verplaatsen
+2
View File
@@ -8,6 +8,8 @@ cost: 35
labor_cost: 800 labor_cost: 800
attack_damage: 15 attack_damage: 15
attack_range: 2 attack_range: 2
required_tech: ""
obsolete_tech: "archery"
icon: images/units/slinger.png icon: images/units/slinger.png
actions: actions:
- verplaatsen - verplaatsen
+16
View File
@@ -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
+16
View File
@@ -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
+2
View File
@@ -8,6 +8,8 @@ cost: 40
labor_cost: 1000 labor_cost: 1000
attack_damage: 20 attack_damage: 20
attack_range: 1 attack_range: 1
required_tech: ""
obsolete_tech: "iron_working"
icon: images/units/warrior.png icon: images/units/warrior.png
actions: actions:
- verplaatsen - verplaatsen
+403 -5
View File
@@ -14,7 +14,7 @@ GameManager& GameManager::instance()
} }
GameManager::GameManager() 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_movementTickDuration(30.0f), m_tickTimer(0.0f),
m_simSpeedMultiplier(1.0f), m_simSpeedMultiplier(1.0f),
m_maxActionQueueSize(5), m_autoQueueMoveWhenBusy(true), m_allowActionQueueing(true), m_maxActionQueueSize(5), m_autoQueueMoveWhenBusy(true), m_allowActionQueueing(true),
@@ -58,6 +58,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
QString roadsConfigPath = baseDir + "/config/roads.yaml"; QString roadsConfigPath = baseDir + "/config/roads.yaml";
m_roadConfigManager.loadConfigFile(roadsConfigPath); m_roadConfigManager.loadConfigFile(roadsConfigPath);
// Load technology tree YAML configuration
QString techConfigPath = baseDir + "/config/technologies.yaml";
m_techTreeManager.loadConfigFile(techConfigPath);
// Load action needed queue configuration // Load action needed queue configuration
QString actionNeededConfigPath = baseDir + "/config/action_needed.yaml"; QString actionNeededConfigPath = baseDir + "/config/action_needed.yaml";
m_actionNeededManager.loadConfigFile(actionNeededConfigPath); m_actionNeededManager.loadConfigFile(actionNeededConfigPath);
@@ -65,9 +69,10 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
// Initialize player national resources from configuration // Initialize player national resources from configuration
// Note: science is a per-tick generation rate and is not stockpiled
for (auto &player : m_playerManager.getPlayersMutable()) { for (auto &player : m_playerManager.getPlayersMutable()) {
player.culture = cCfg.initialNationalResources.value("cultuur", 10000); 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.faith = cCfg.initialNationalResources.value("geloof", 0);
player.gold = cCfg.initialNationalResources.value("goud", 0); player.gold = cCfg.initialNationalResources.value("goud", 0);
} }
@@ -86,6 +91,11 @@ void GameManager::init(const QString &baseDir, Matrix2D<TerrainPoint> *mapPoints
m_cities.push_back(initialCity); 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 // Spawn initial demo units near starting camera position
// Player 1 (Blue & Gold) // Player 1 (Blue & Gold)
spawnUnit("settler", 1, 36, 18, mapPoints, numPoints05); 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<TerrainPoint> *mapPoints, int numPoints05) void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *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 // Update movement tick timer
m_tickTimer += deltaTime * m_simSpeedMultiplier; m_tickTimer += deltaTime * m_simSpeedMultiplier;
if (m_tickTimer >= m_movementTickDuration) { if (m_tickTimer >= m_movementTickDuration) {
@@ -171,8 +186,347 @@ void GameManager::update(float deltaTime, Matrix2D<TerrainPoint> *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 // 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<TerrainPoint> *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<QString> 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<QString> requiredSet;
std::vector<QString> 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<QString> orderedQueue;
orderedQueue.reserve(requiredSet.size());
std::set<QString> currentSatisfied = satisfied;
std::set<QString> remainingToSchedule = requiredSet;
while (!remainingToSchedule.empty()) {
std::vector<QString> 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<QString> 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() Unit* GameManager::getSelectedUnit()
@@ -251,6 +605,7 @@ const City* GameManager::getActiveCityView() const
void GameManager::enterCityView(int cityId) void GameManager::enterCityView(int cityId)
{ {
m_activeCityViewId = cityId; m_activeCityViewId = cityId;
m_isTechTreeOpen = false;
m_selectedUnitId = -1; m_selectedUnitId = -1;
m_actionMode = ActionMode::None; m_actionMode = ActionMode::None;
} }
@@ -378,6 +733,7 @@ CityYieldSummary GameManager::calculateCityYields(const City &city, Matrix2D<Ter
void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPoints05) void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{ {
const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig(); const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig();
QMap<int, int> playerTickScience;
for (auto &city : m_cities) { for (auto &city : m_cities) {
CityYieldSummary yields = calculateCityYields(city, mapPoints, numPoints05); CityYieldSummary yields = calculateCityYields(city, mapPoints, numPoints05);
@@ -396,13 +752,13 @@ void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPo
city.accumulatedLabor += yields.labor; 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); Player *owner = m_playerManager.getPlayer(city.playerId);
if (owner) { if (owner) {
owner->culture += yields.culture; owner->culture += yields.culture;
owner->science += yields.science;
owner->faith += yields.faith; owner->faith += yields.faith;
owner->gold += yields.gold; owner->gold += yields.gold;
playerTickScience[city.playerId] += yields.science;
} }
// Floating growth text over city // Floating growth text over city
@@ -412,6 +768,13 @@ void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPo
.arg(foodGrown).arg(yields.culture); .arg(foodGrown).arg(yields.culture);
addFloatingText(growthMsg, ftPos, QColor(100, 255, 140), 2.5f); 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 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); City *city = getCityById(cityId);
if (!city) return; if (!city) return;
const Player *player = m_playerManager.getPlayer(city->playerId);
const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig();
int maxQueue = cCfg.maxProductionQueueSize; int maxQueue = cCfg.maxProductionQueueSize;
@@ -478,6 +842,23 @@ void GameManager::queueCityProject(int cityId, CityProjectType type, const QStri
const UnitConfig *uCfg = m_unitConfigManager.getConfig(id); const UnitConfig *uCfg = m_unitConfigManager.getConfig(id);
if (!uCfg) return; 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()) { if (!city->hasActiveProject()) {
city->startProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost); city->startProject(CityProjectType::Unit, id, uCfg->name, uCfg->laborCost);
QString msg = Localization::instance().get("ui.city.project_started", "%1: Started %2") 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; 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)) { if (city->isBuildingQueuedOrBuilt(id)) {
QString msg = Localization::instance().get("ui.city.already_queued", "%1 is already in the production queue!").arg(bCfg->name); 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); addFloatingText(msg, city->worldPos + QVector3D(0.0f, 0.0f, 1.2f), QColor(255, 160, 60), 2.2f);
+22
View File
@@ -14,6 +14,7 @@
#include "terrain.h" #include "terrain.h"
#include "terrainyieldmanager.h" #include "terrainyieldmanager.h"
#include "actionneededmanager.h" #include "actionneededmanager.h"
#include "techconfig.h"
enum class ActionMode { enum class ActionMode {
None, None,
@@ -48,13 +49,32 @@ public:
void update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int numPoints05); void update(float deltaTime, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
PlayerManager& players() { return m_playerManager; } PlayerManager& players() { return m_playerManager; }
const PlayerManager& players() const { return m_playerManager; }
UnitConfigManager& unitConfigs() { return m_unitConfigManager; } UnitConfigManager& unitConfigs() { return m_unitConfigManager; }
const UnitConfigManager& unitConfigs() const { return m_unitConfigManager; }
BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; } BuildingConfigManager& buildingConfigs() { return m_buildingConfigManager; }
const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; } const BuildingConfigManager& buildingConfigs() const { return m_buildingConfigManager; }
RoadConfigManager& roadConfigs() { return m_roadConfigManager; } RoadConfigManager& roadConfigs() { return m_roadConfigManager; }
const RoadConfigManager& roadConfigs() const { return m_roadConfigManager; } const RoadConfigManager& roadConfigs() const { return m_roadConfigManager; }
ActionNeededManager& actionNeededManager() { return m_actionNeededManager; } ActionNeededManager& actionNeededManager() { return m_actionNeededManager; }
const ActionNeededManager& actionNeededManager() const { 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<TerrainPoint> *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<std::shared_ptr<Unit>>& getUnits() const { return m_units; } const std::vector<std::shared_ptr<Unit>>& getUnits() const { return m_units; }
const std::vector<City>& getCities() const { return m_cities; } const std::vector<City>& getCities() const { return m_cities; }
@@ -149,6 +169,7 @@ private:
BuildingConfigManager m_buildingConfigManager; BuildingConfigManager m_buildingConfigManager;
RoadConfigManager m_roadConfigManager; RoadConfigManager m_roadConfigManager;
ActionNeededManager m_actionNeededManager; ActionNeededManager m_actionNeededManager;
TechTreeManager m_techTreeManager;
std::vector<std::shared_ptr<Unit>> m_units; std::vector<std::shared_ptr<Unit>> m_units;
std::vector<City> m_cities; std::vector<City> m_cities;
std::vector<FloatingText> m_floatingTexts; std::vector<FloatingText> m_floatingTexts;
@@ -157,6 +178,7 @@ private:
int m_selectedUnitId; int m_selectedUnitId;
int m_activeCityViewId; int m_activeCityViewId;
bool m_isTechTreeOpen;
ActionMode m_actionMode; ActionMode m_actionMode;
float m_movementTickDuration; // Default 30.0s float m_movementTickDuration; // Default 30.0s
+59
View File
@@ -2,8 +2,12 @@
#define PLAYER_H #define PLAYER_H
#include <QString> #include <QString>
#include <QStringList>
#include <QColor> #include <QColor>
#include <QMap>
#include <vector> #include <vector>
#include <set>
#include <algorithm>
struct Player { struct Player {
int id; int id;
@@ -12,11 +16,17 @@ struct Player {
QColor secondaryColor; QColor secondaryColor;
// National resources (shared across all cities of this nation) // National resources (shared across all cities of this nation)
// Note: science represents per-tick science generation rate (no longer stockpiled)
int culture; int culture;
int science; int science;
int gold; int gold;
int faith; int faith;
// Technology Tree & Research Progression
std::set<QString> researchedTechs;
QMap<QString, int> techProgress; // Tech ID -> accumulated science towards that tech
std::vector<QString> researchQueue; // Ordered queue of Tech IDs (front is active research)
Player() Player()
: id(0), name("Unknown"), primaryColor(Qt::blue), secondaryColor(Qt::white), : id(0), name("Unknown"), primaryColor(Qt::blue), secondaryColor(Qt::white),
culture(10000), science(0), gold(0), faith(0) {} 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) int pCulture = 10000, int pScience = 0, int pGold = 0, int pFaith = 0)
: id(pId), name(pName), primaryColor(primary), secondaryColor(secondary), : id(pId), name(pName), primaryColor(primary), secondaryColor(secondary),
culture(pCulture), science(pScience), gold(pGold), faith(pFaith) {} 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<int>(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 { class PlayerManager {
+577 -22
View File
@@ -18,9 +18,9 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
setMouseTracking(true); setMouseTracking(true);
installEventFilter(this); installEventFilter(this);
QImage img = QImage("/home/bart/project/tiletest/images/heightmap.jpg").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/project/tiletest/images/typemap.png").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/project/tiletest/images/rivermap.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_zoom = 4;
m_point = new Matrix2D<TerrainPoint>(img.width(), img.height()); m_point = new Matrix2D<TerrainPoint>(img.width(), img.height());
m_numPoints05 = img.width() / MAP_WIDTH / 1.5 / 2; m_numPoints05 = img.width() / MAP_WIDTH / 1.5 / 2;
@@ -60,13 +60,13 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
m_draggingMinimap = false; m_draggingMinimap = false;
m_minimapZoom = 1.0f; 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()) { 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 // 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_lastFrameTime = 0.0f;
m_elapsedTimer.start(); m_elapsedTimer.start();
@@ -83,7 +83,7 @@ QSceneRenderer::QSceneRenderer(QWidget* parent): QOpenGLWidget(parent)
void QSceneRenderer::loadCameraConfig() 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); YamlNode root = YamlParser::parseFile(configPath);
if (root.hasChild("camera")) { if (root.hasChild("camera")) {
YamlNode cam = root["camera"]; YamlNode cam = root["camera"];
@@ -466,7 +466,7 @@ QImage QSceneRenderer::getTintedIcon(const QString &path, const QColor &color)
} }
if (!m_iconCache.contains(path)) { 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); QImage loaded(fullPath);
if (loaded.isNull()) { if (loaded.isNull()) {
loaded = QImage(32, 32, QImage::Format_ARGB32); loaded = QImage(32, 32, QImage::Format_ARGB32);
@@ -529,7 +529,7 @@ void QSceneRenderer::drawTickBar(QPainter &painter)
{ {
painter.setRenderHint(QPainter::Antialiasing, true); painter.setRenderHint(QPainter::Antialiasing, true);
int barW = 690, barH = 34; int barW = 840, barH = 34;
QRect barRect(16, 16, barW, barH); QRect barRect(16, 16, barW, barH);
// Dark glass card // Dark glass card
@@ -557,27 +557,72 @@ void QSceneRenderer::drawTickBar(QPainter &painter)
painter.setBrush(QColor(60, 180, 255)); painter.setBrush(QColor(60, 180, 255));
painter.drawRoundedRect(progFg, 3, 3); 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); const Player *p1 = GameManager::instance().players().getPlayer(1);
m_scienceBarHitRect = QRect();
m_techTreeBtnRect = QRect();
if (p1) { 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<ResItem> natRes = { std::vector<ResItem> natRes = {
{"images/resources/cultuur.png", p1->culture, QColor(215, 110, 255)}, {"images/resources/cultuur.png", QString("%L1").arg(p1->culture), QColor(215, 110, 255), false},
{"images/resources/kennis.png", p1->science, QColor(60, 190, 255)}, {"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", p1->gold, QColor(255, 210, 50)}, {"images/resources/goud.png", QString("%L1").arg(p1->gold), QColor(255, 210, 50), false},
{"images/resources/geloof.png", p1->faith, QColor(255, 230, 80)} {"images/resources/geloof.png", QString("%L1").arg(p1->faith), QColor(255, 230, 80), false}
}; };
int startX = barRect.left() + 155; int startX = barRect.left() + 150;
int itemW = 86; int itemW = 92;
for (size_t i = 0; i < natRes.size(); ++i) { for (size_t i = 0; i < natRes.size(); ++i) {
int ix = startX + (int)i * itemW; 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); QImage icon = getTintedIcon(natRes[i].icon, natRes[i].col);
painter.drawImage(QRect(ix, barRect.top() + 9, 16, 16), icon); painter.drawImage(QRect(ix, barRect.top() + 9, 16, 16), icon);
painter.setFont(QFont("sans-serif", 8, QFont::Bold)); painter.setFont(QFont("sans-serif", 8, QFont::Bold));
painter.setPen(natRes[i].col); 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 // List of available projects under selected tab
int itemH = 50; int itemH = 50;
if (m_cityProjectTab == 0) { if (m_cityProjectTab == 0) {
// Buildings tab // Buildings tab (only show unlocked, non-obsolete buildings)
const auto &buildings = GameManager::instance().buildingConfigs().getAllBuildings(); const auto &buildings = GameManager::instance().buildingConfigs().getAllBuildings();
for (auto it = buildings.begin(); it != buildings.end(); ++it) { for (auto it = buildings.begin(); it != buildings.end(); ++it) {
if (curY + itemH > prodPanelRect.bottom() - 6) break; if (curY + itemH > prodPanelRect.bottom() - 6) break;
const BuildingConfig &bCfg = it.value(); 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); QRect itemRect(innerX, curY, innerW, itemH);
bool alreadyBuilt = city->hasBuilding(bCfg.id); bool alreadyBuilt = city->hasBuilding(bCfg.id);
@@ -1972,12 +2021,16 @@ void QSceneRenderer::drawCityViewOverlay(QPainter &painter, const QMatrix4x4 &vM
curY += itemH + 6; curY += itemH + 6;
} }
} else { } else {
// Units tab // Units tab (only show unlocked, non-obsolete units)
const auto &units = GameManager::instance().unitConfigs().getAllConfigs(); const auto &units = GameManager::instance().unitConfigs().getAllConfigs();
for (auto it = units.begin(); it != units.end(); ++it) { for (auto it = units.begin(); it != units.end(); ++it) {
if (curY + itemH > prodPanelRect.bottom() - 6) break; if (curY + itemH > prodPanelRect.bottom() - 6) break;
const UnitConfig &uCfg = it.value(); 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); QRect itemRect(innerX, curY, innerW, itemH);
bool isActiveProj = (city->hasActiveProject() && city->currentProject.type == CityProjectType::Unit && city->currentProject.id == uCfg.id); 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 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) // Action Queue items (Bottom HUD notification bar)
for (const auto &hit : m_actionItemHits) { for (const auto &hit : m_actionItemHits) {
if (hit.dismissRect.contains(event->pos()) && event->button() == Qt::LeftButton) { if (hit.dismissRect.contains(event->pos()) && event->button() == Qt::LeftButton) {
@@ -2593,6 +2687,21 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
{ {
m_mouseCurrentPos = event->pos(); 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 // Disable panning, rotation and minimap drag in City View
if (GameManager::instance().isInCityView()) { if (GameManager::instance().isInCityView()) {
if (m_slotPressPending) { if (m_slotPressPending) {
@@ -2631,7 +2740,7 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
break; break;
} }
} }
if (onActionItem) { if (onActionItem || m_techTreeBtnRect.contains(event->pos()) || m_scienceBarHitRect.contains(event->pos())) {
setCursor(Qt::PointingHandCursor); setCursor(Qt::PointingHandCursor);
} else { } else {
unsetCursor(); unsetCursor();
@@ -2642,9 +2751,29 @@ void QSceneRenderer::mouseMoveEvent(QMouseEvent *event)
void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event) void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event)
{ {
Q_UNUSED(event);
m_draggingMinimap = false; 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()) { if (m_slotLongPressTimer && m_slotLongPressTimer->isActive()) {
m_slotLongPressTimer->stop(); m_slotLongPressTimer->stop();
} }
@@ -2684,6 +2813,20 @@ void QSceneRenderer::mouseReleaseEvent(QMouseEvent *event)
void QSceneRenderer::keyPressEvent(QKeyEvent *e) 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 // In City View: disable WASD panning and Q/E rotation; only allow Escape to exit
if (GameManager::instance().isInCityView()) { if (GameManager::instance().isInCityView()) {
if (e->key() == Qt::Key_Escape) { if (e->key() == Qt::Key_Escape) {
@@ -2796,6 +2939,16 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e)
void QSceneRenderer::wheelEvent(QWheelEvent *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 // Disable zooming in City View
if (GameManager::instance().isInCityView()) { if (GameManager::instance().isInCityView()) {
return; return;
@@ -2973,6 +3126,9 @@ void QSceneRenderer::paintGL()
drawUnitCard(painter, selected); drawUnitCard(painter, selected);
} }
drawActionNeededQueue(painter); drawActionNeededQueue(painter);
if (GameManager::instance().isTechTreeOpen()) {
drawTechTreeOverlay(painter);
}
} else { } else {
drawUnitsOverlay(painter, vMatrix); drawUnitsOverlay(painter, vMatrix);
drawFloatingTexts(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); subtitle = Localization::instance().get("ui.action_needed.city_free_pop_desc", "+%1 Free Pop").arg(item.auxValue);
accent = cfg.cityFreePopColor; accent = cfg.cityFreePopColor;
icon = getTintedIcon("images/resources/citizen.png", accent); 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 // Draw card background
@@ -3140,10 +3301,21 @@ void QSceneRenderer::drawActionNeededQueue(QPainter &painter)
void QSceneRenderer::handleActionItemClick(ActionNeededType type, int entityId) 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 (type == ActionNeededType::UnitIdle) {
if (GameManager::instance().isInCityView()) { if (GameManager::instance().isInCityView()) {
GameManager::instance().exitCityView(); GameManager::instance().exitCityView();
} }
if (GameManager::instance().isTechTreeOpen()) {
GameManager::instance().closeTechTree();
}
Unit *u = GameManager::instance().getUnitById(entityId); Unit *u = GameManager::instance().getUnitById(entityId);
if (u) { if (u) {
GameManager::instance().selectUnit(entityId); 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<QString, QRect> 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"));
}
+25
View File
@@ -191,6 +191,31 @@ protected:
void drawActionNeededQueue(QPainter &painter); void drawActionNeededQueue(QPainter &painter);
void handleActionItemClick(ActionNeededType type, int entityId); 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<TechNodeHit> m_techNodeHits;
struct TechQueueChipHit {
QRect rect;
QString techId;
};
std::vector<TechQueueChipHit> m_techQueueChipHits;
}; };
#endif // QSCENERENDERER_H #endif // QSCENERENDERER_H
+131
View File
@@ -0,0 +1,131 @@
#include "techconfig.h"
#include "yamlparser.h"
#include <QDebug>
#include <algorithm>
TechTreeManager::TechTreeManager()
: m_maxColumn(0), m_maxRow(0)
{
}
int TechTreeManager::computeTechDepth(const QString &techId, QMap<QString, int> &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<QString, bool> hasExplicitColumn;
QMap<QString, bool> 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<QString, int> depthCache;
QMap<int, int> 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;
}
+45
View File
@@ -0,0 +1,45 @@
#ifndef TECHCONFIG_H
#define TECHCONFIG_H
#include <QString>
#include <QStringList>
#include <QMap>
#include <vector>
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<QString, TechConfig>& getAllTechs() const { return m_techs; }
const std::vector<QString>& getOrderedTechIds() const { return m_orderedTechIds; }
int getMaxColumn() const { return m_maxColumn; }
int getMaxRow() const { return m_maxRow; }
private:
int computeTechDepth(const QString &techId, QMap<QString, int> &visited) const;
QMap<QString, TechConfig> m_techs;
std::vector<QString> m_orderedTechIds;
int m_maxColumn;
int m_maxRow;
};
#endif // TECHCONFIG_H
+4 -2
View File
@@ -33,7 +33,8 @@ SOURCES += main.cpp \
localization.cpp \ localization.cpp \
terrainmovementmanager.cpp \ terrainmovementmanager.cpp \
roadconfig.cpp \ roadconfig.cpp \
actionneededmanager.cpp actionneededmanager.cpp \
techconfig.cpp
HEADERS += mainwindow.h \ HEADERS += mainwindow.h \
qscenerenderer.h \ qscenerenderer.h \
@@ -56,7 +57,8 @@ HEADERS += mainwindow.h \
localization.h \ localization.h \
terrainmovementmanager.h \ terrainmovementmanager.h \
roadconfig.h \ roadconfig.h \
actionneededmanager.h actionneededmanager.h \
techconfig.h
FORMS += mainwindow.ui FORMS += mainwindow.ui
+2
View File
@@ -53,6 +53,8 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
cfg.attackRange = root.hasChild("attack_range") ? root["attack_range"].asInt(1) : 1; 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.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.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() : ""; cfg.iconPath = root.hasChild("icon") ? root["icon"].asString() : "";
if (root.hasChild("domain")) { if (root.hasChild("domain")) {
+3 -1
View File
@@ -44,6 +44,8 @@ struct UnitConfig {
int attackRange; // 1 for melee, >1 for ranged int attackRange; // 1 for melee, >1 for ranged
int buildCharges; int buildCharges;
bool infiniteCharges; bool infiniteCharges;
QString requiredTech;
QString obsoleteTech;
QString iconPath; QString iconPath;
QStringList actions; QStringList actions;
QMap<QString, float> actionSpeeds; // Duration in seconds per action (e.g. "boerderij_bouwen": 10.0f) QMap<QString, float> actionSpeeds; // Duration in seconds per action (e.g. "boerderij_bouwen": 10.0f)
@@ -54,7 +56,7 @@ struct UnitConfig {
: id(""), name(""), category(UnitCategory::Builder), : id(""), name(""), category(UnitCategory::Builder),
movementDomain(MovementDomain::Land), health(100), movementDomain(MovementDomain::Land), health(100),
movementSpeed(2.0f), cost(10), laborCost(1000), attackDamage(0), attackRange(1), 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 isRanged() const { return attackRange > 1; }
bool isMilitary() const { return category == UnitCategory::Military; } bool isMilitary() const { return category == UnitCategory::Military; }