Implement technology tree, research queue by least remaining work, and unit/building tech availability & obsolescence
This commit is contained in:
+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"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user