Implement technology tree, research queue by least remaining work, and unit/building tech availability & obsolescence
This commit is contained in:
+28
-1
@@ -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<std::shared_ptr<Unit>> &units,
|
||||
const std::vector<City> &cities)
|
||||
const std::vector<City> &cities,
|
||||
bool needsResearch)
|
||||
{
|
||||
if (!m_config.enabled) return;
|
||||
|
||||
@@ -93,6 +101,25 @@ void ActionNeededManager::update(float deltaTime, int activePlayerId,
|
||||
|
||||
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
|
||||
if (m_config.trackIdleUnits) {
|
||||
for (const auto &u : units) {
|
||||
|
||||
@@ -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<std::shared_ptr<Unit>> &units,
|
||||
const std::vector<City> &cities);
|
||||
const std::vector<City> &cities,
|
||||
bool needsResearch = false);
|
||||
|
||||
std::vector<ActionItem> getVisibleItems() const;
|
||||
int getVisibleCount() const;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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("") {}
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+131
-8
@@ -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"
|
||||
|
||||
+39
-1
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
+403
-5
@@ -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<TerrainPoint> *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<TerrainPoint> *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<TerrainPoint> *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<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
|
||||
m_tickTimer += deltaTime * m_simSpeedMultiplier;
|
||||
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
|
||||
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()
|
||||
@@ -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<Ter
|
||||
void GameManager::processTickYields(Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
const CityConfig &cfg = TerrainYieldManager::instance().getCityConfig();
|
||||
QMap<int, int> playerTickScience;
|
||||
|
||||
for (auto &city : m_cities) {
|
||||
CityYieldSummary yields = calculateCityYields(city, mapPoints, numPoints05);
|
||||
@@ -396,13 +752,13 @@ void GameManager::processTickYields(Matrix2D<TerrainPoint> *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<TerrainPoint> *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);
|
||||
|
||||
@@ -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<TerrainPoint> *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<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<City>& getCities() const { return m_cities; }
|
||||
@@ -149,6 +169,7 @@ private:
|
||||
BuildingConfigManager m_buildingConfigManager;
|
||||
RoadConfigManager m_roadConfigManager;
|
||||
ActionNeededManager m_actionNeededManager;
|
||||
TechTreeManager m_techTreeManager;
|
||||
std::vector<std::shared_ptr<Unit>> m_units;
|
||||
std::vector<City> m_cities;
|
||||
std::vector<FloatingText> 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
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
#define PLAYER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
#include <QColor>
|
||||
#include <QMap>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <algorithm>
|
||||
|
||||
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<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()
|
||||
: 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<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 {
|
||||
|
||||
+577
-22
@@ -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<TerrainPoint>(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<ResItem> 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<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"));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<TechNodeHit> m_techNodeHits;
|
||||
|
||||
struct TechQueueChipHit {
|
||||
QRect rect;
|
||||
QString techId;
|
||||
};
|
||||
std::vector<TechQueueChipHit> m_techQueueChipHits;
|
||||
};
|
||||
|
||||
#endif // QSCENERENDERER_H
|
||||
|
||||
+131
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
+3
-1
@@ -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<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),
|
||||
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; }
|
||||
|
||||
Reference in New Issue
Block a user