From 4b237872eb46c562118537aa86e8241123d1dec4 Mon Sep 17 00:00:00 2001 From: bart Date: Sun, 8 Nov 2026 21:31:56 +0100 Subject: [PATCH] Implement unit action queueing and busy state validation --- config/game.yaml | 6 + config/strings_en.yaml | 41 ++++++ gamemanager.cpp | 304 ++++++++++++++++++++++++++++++++++++++--- gamemanager.h | 9 ++ qscenerenderer.cpp | 83 +++++++---- unit.cpp | 69 +++++++++- unit.h | 40 ++++++ 7 files changed, 502 insertions(+), 50 deletions(-) diff --git a/config/game.yaml b/config/game.yaml index 0aa618a..c225933 100644 --- a/config/game.yaml +++ b/config/game.yaml @@ -1,5 +1,11 @@ movement_tick_duration: 30.0 +# Action queue configuration +action_queue: + max_size: 5 + auto_queue_move_when_busy: true + allow_action_queueing: true + # Camera settings camera: min_zoom: 2.0 diff --git a/config/strings_en.yaml b/config/strings_en.yaml index 6c48e13..6e86313 100644 --- a/config/strings_en.yaml +++ b/config/strings_en.yaml @@ -45,6 +45,27 @@ ui: unit: moving: "Moving" + move_queued: "Movement queued" + queue_cleared: "Queued actions cancelled" + queue_full: "Action queue full!" + action_queued: "Action queued: %1" + action_cancelled: "%1 cancelled" + farm_queued: "Build Farm queued on arrival" + farm_started: "Constructing Farm..." + farm_completed: "Farm completed!" + farm_completed_consumed: "Farm completed! (Builder consumed)" + farm_completed_charges: "Farm completed! (%1 %2 remaining)" + city_queued: "Found City queued on arrival" + city_founded: "%1 founded!" + already_building: "Already busy building!" + busy: "Unit is busy!" + cannot_act_busy: "Unit cannot perform action while busy!" + tile_occupied: "Tile already has an improvement!" + cannot_build_here: "Cannot build here! Allowed on: %1" + cannot_build_destination: "Cannot build on destination terrain! Allowed on: %1" + no_build_charges: "No build charges remaining!" + charge_singular: "charge" + charge_plural: "charges" impassable_terrain: "Impassable terrain!" naval_only_water: "Ships can only sail on water!" no_path: "No reachable path!" @@ -53,6 +74,26 @@ ui: defeated: "Defeated!" city_already_here: "A city already exists here!" unsuitable_terrain: "Unsuitable terrain!" + civilian_unit: "Civilian unit" + military_unit: "Military unit" + speed_format: "Speed: %1 hex/tick" + cost_format: "Cost: %1 pts" + combat_format: "Attack: %1 | Range: %2 hex" + building_pct: "Building... %1%" + charges_format: "Charges: %1" + status_queued: "Queued: %1" + btn_move: "Move [M]" + btn_attack: "Attack [A]" + btn_build_city: "Settle [B]" + btn_build_farm: "Farm [F]" + btn_farm_queued: "Farm (Queued)" + btn_farm_building: "Building %1%" + tip_move: "Move (M)" + tip_attack: "Attack (A) [Range: %1]" + tip_build_city: "Found City (B)" + tip_build_farm: "Build Farm (F)" + tip_farm_building: "Busy building... %1%" + tip_farm_queued: "Build Farm (Queued on arrival - click to cancel)" city: center: "City Center" diff --git a/gamemanager.cpp b/gamemanager.cpp index 3dea832..7687dab 100644 --- a/gamemanager.cpp +++ b/gamemanager.cpp @@ -16,7 +16,9 @@ GameManager& GameManager::instance() GameManager::GameManager() : m_selectedUnitId(-1), m_activeCityViewId(-1), m_actionMode(ActionMode::None), m_movementTickDuration(30.0f), m_tickTimer(0.0f), - m_simSpeedMultiplier(1.0f), m_nextUnitId(0), m_nextCityId(0), + m_simSpeedMultiplier(1.0f), + m_maxActionQueueSize(5), m_autoQueueMoveWhenBusy(true), m_allowActionQueueing(true), + m_nextUnitId(0), m_nextCityId(0), m_initialized(false) { } @@ -37,6 +39,12 @@ void GameManager::init(const QString &baseDir, Matrix2D *mapPoints if (gameRoot.hasChild("movement_tick_duration")) { m_movementTickDuration = gameRoot["movement_tick_duration"].asFloat(30.0f); } + if (gameRoot.hasChild("action_queue")) { + const YamlNode &aqNode = gameRoot["action_queue"]; + m_maxActionQueueSize = aqNode["max_size"].asInt(5); + m_autoQueueMoveWhenBusy = aqNode["auto_queue_move_when_busy"].asBool(true); + m_allowActionQueueing = aqNode["allow_action_queueing"].asBool(true); + } // Load unit YAML configurations QString unitsConfigDir = baseDir + "/config/units"; @@ -102,6 +110,7 @@ Unit* GameManager::spawnUnit(const QString &configId, int playerId, int tileX, i int id = getNextUnitId(); auto unit = std::make_shared(id, playerId, *cfg, tileX, tileY, gz); + unit->maxQueueSize = m_maxActionQueueSize; m_units.push_back(unit); return unit.get(); } @@ -122,6 +131,9 @@ void GameManager::update(float deltaTime, Matrix2D *mapPoints, int if (u->isBuilding() && u->buildProgress >= 1.0f) { completeUnitBuilding(*u, mapPoints, numPoints05); } + if (!u->isDead() && u->state == UnitState::Idle && u->hasQueuedAction()) { + processUnitActionQueue(*u, mapPoints, numPoints05); + } } } @@ -504,12 +516,30 @@ void GameManager::commandMoveSelected(int targetX, int targetY, Matrix2DisBuilding()) { + if (m_allowActionQueueing && m_autoQueueMoveWhenBusy) { + if (selected->queueAction(UnitAction(UnitActionType::Move, targetX, targetY))) { + addFloatingText(Localization::instance().get("ui.unit.move_queued", "Movement queued"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80)); + } else { + addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100)); + } + } else { + addFloatingText(Localization::instance().get("ui.unit.busy", "Unit is busy!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100)); + } + return; + } + std::vector> path = Pathfinder::findPath( selected->tileX, selected->tileY, targetX, targetY, mapPoints, numPoints05, selected->config.movementDomain, &selected->config ); if (path.size() > 1) { + selected->clearActionQueue(); selected->setPath(path); addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"), selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80)); @@ -524,8 +554,15 @@ void GameManager::commandAttack(Unit *target) Unit *selected = getSelectedUnit(); if (!selected || !target) return; + if (selected->isBuilding()) { + addFloatingText(Localization::instance().get("ui.unit.cannot_act_busy", "Unit cannot perform action while busy!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90)); + return; + } + if (!selected->canAttack(*target)) { - addFloatingText("Buiten bereik!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); + addFloatingText(Localization::instance().get("ui.unit.out_of_range", "Out of range!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); return; } @@ -536,7 +573,8 @@ void GameManager::commandAttack(Unit *target) addFloatingText(dmgText, target->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 50, 50)); if (target->isDead()) { - addFloatingText("Verslagen!", target->worldPos + QVector3D(0, 0, 1.2f), QColor(255, 200, 50), 2.2f); + addFloatingText(Localization::instance().get("ui.unit.defeated", "Defeated!"), + target->worldPos + QVector3D(0, 0, 1.2f), QColor(255, 200, 50), 2.2f); } m_actionMode = ActionMode::None; @@ -547,32 +585,62 @@ void GameManager::commandBuildCitySelected(Matrix2D *mapPoints, in Unit *selected = getSelectedUnit(); if (!selected || !selected->config.canBuildCity()) return; + if (selected->isBuilding()) { + addFloatingText(Localization::instance().get("ui.unit.cannot_act_busy", "Unit cannot perform action while busy!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90)); + return; + } + + // If unit is moving, queue city founding upon arrival + if (selected->state == UnitState::Moving) { + if (selected->hasQueuedActionType(UnitActionType::BuildCity)) { + selected->removeQueuedActionsOfType(UnitActionType::BuildCity); + addFloatingText(Localization::instance().get("ui.unit.action_cancelled", "%1 cancelled").arg("Found City"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80)); + return; + } + + if (selected->queueAction(UnitAction(UnitActionType::BuildCity, selected->targetTileX, selected->targetTileY))) { + addFloatingText(Localization::instance().get("ui.unit.city_queued", "Found City queued on arrival"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(80, 220, 120), 2.2f); + } else { + addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100)); + } + return; + } + + int tx = selected->tileX; + int ty = selected->tileY; + // Check if tile already has a city for (const auto &c : m_cities) { - if (c.tileX == selected->tileX && c.tileY == selected->tileY) { - addFloatingText("Al een stad hier!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); + if (c.tileX == tx && c.tileY == ty) { + addFloatingText(Localization::instance().get("ui.unit.city_already_here", "A city already exists here!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); return; } } const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); int cId = getNextCityId(); - QString cityName = QString("Stad %1").arg(cId); - QVector2D w = Pathfinder::hexToWorld(selected->tileX, selected->tileY); + QString cityName = QString("City %1").arg(cId); + QVector2D w = Pathfinder::hexToWorld(tx, ty); float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05); - City newCity(cId, selected->playerId, cityName, selected->tileX, selected->tileY, + City newCity(cId, selected->playerId, cityName, tx, ty, QVector3D(w.x(), w.y(), gz), cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius); // Assign initial worker to center tile - QString tType = TerrainYieldManager::instance().getTerrainType(selected->tileX, selected->tileY, mapPoints, numPoints05); + QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType); QString defRes = yCfg ? yCfg->primaryResource : "voedsel"; - newCity.assignWorker(selected->tileX, selected->tileY, defRes); + newCity.assignWorker(tx, ty, defRes); m_cities.push_back(newCity); - addFloatingText(QString("%1 gesticht!").arg(cityName), QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f); + addFloatingText(Localization::instance().get("ui.unit.city_founded", "%1 founded!").arg(cityName), + QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f); // Consuming settler selected->takeDamage(99999.0f); @@ -605,7 +673,55 @@ void GameManager::commandBuildFarmSelected(Matrix2D *mapPoints, in if (!selected || !selected->config.canBuildFarm()) return; if (selected->isBuilding()) { - addFloatingText("Al bezig met bouwen!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 200, 70)); + addFloatingText(Localization::instance().get("ui.unit.already_building", "Already busy building!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 200, 70)); + return; + } + + if (!selected->config.infiniteCharges && selected->buildCharges <= 0) { + addFloatingText(Localization::instance().get("ui.unit.no_build_charges", "No build charges remaining!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90)); + return; + } + + // If unit is currently moving: toggle or queue build action on arrival! + if (selected->state == UnitState::Moving) { + if (selected->hasQueuedActionType(UnitActionType::BuildImprovement)) { + selected->removeQueuedActionsOfType(UnitActionType::BuildImprovement); + addFloatingText(Localization::instance().get("ui.unit.action_cancelled", "%1 cancelled").arg("Build Farm"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80)); + return; + } + + int destX = selected->targetTileX; + int destY = selected->targetTileY; + + // Check if destination tile already has an improvement + if (hasImprovementAt(destX, destY)) { + addFloatingText(Localization::instance().get("ui.unit.tile_occupied", "Tile already has an improvement!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90)); + return; + } + + // Check destination terrain type against allowed terrains + QString tType = TerrainYieldManager::instance().getTerrainType(destX, destY, mapPoints, numPoints05); + const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig("farm"); + QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"}; + + if (!validTerrains.contains(tType)) { + QString validStr = validTerrains.join(", "); + addFloatingText(Localization::instance().get("ui.unit.cannot_build_destination", "Cannot build on destination terrain! Allowed on: %1").arg(validStr), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); + return; + } + + if (selected->queueAction(UnitAction(UnitActionType::BuildImprovement, destX, destY, "farm"))) { + addFloatingText(Localization::instance().get("ui.unit.farm_queued", "Build Farm queued on arrival"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(80, 220, 120), 2.2f); + } else { + addFloatingText(Localization::instance().get("ui.unit.queue_full", "Action queue full!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100)); + } return; } @@ -614,7 +730,8 @@ void GameManager::commandBuildFarmSelected(Matrix2D *mapPoints, in // Check if tile already has an improvement if (hasImprovementAt(tx, ty)) { - addFloatingText("Er staat hier al een boerderij!", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90)); + addFloatingText(Localization::instance().get("ui.unit.tile_occupied", "Tile already has an improvement!"), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90)); return; } @@ -624,14 +741,17 @@ void GameManager::commandBuildFarmSelected(Matrix2D *mapPoints, in QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"}; if (!validTerrains.contains(tType)) { - addFloatingText("Kan hier niet bouwen! Alleen op gras en toendra.", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); + QString validStr = validTerrains.join(", "); + addFloatingText(Localization::instance().get("ui.unit.cannot_build_here", "Cannot build here! Allowed on: %1").arg(validStr), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); return; } // Start building process (duration comes from the builder unit's action speed) float duration = selected->config.getActionDuration("boerderij_bouwen", 10.0f); selected->startBuilding("farm", duration); - addFloatingText("Boerderij in aanbouw...", selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f); + addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm..."), + selected->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f); } void GameManager::completeUnitBuilding(Unit &unit, Matrix2D *mapPoints, int numPoints05) @@ -662,7 +782,8 @@ void GameManager::completeUnitBuilding(Unit &unit, Matrix2D *mapPo if (!unit.config.infiniteCharges && unit.buildCharges > 0) { unit.buildCharges -= 1; if (unit.buildCharges <= 0) { - addFloatingText("Boerderij voltooid! (Builder verbruikt)", QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f); + addFloatingText(Localization::instance().get("ui.unit.farm_completed_consumed", "Farm completed! (Builder consumed)"), + QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f); unit.takeDamage(99999.0f); if (unit.id == m_selectedUnitId) { m_selectedUnitId = -1; @@ -670,14 +791,159 @@ void GameManager::completeUnitBuilding(Unit &unit, Matrix2D *mapPo } return; } else { - QString msg = QString("Boerderij voltooid! (%1 %2 over)") + QString chargeWord = unit.buildCharges == 1 + ? Localization::instance().get("ui.unit.charge_singular", "charge") + : Localization::instance().get("ui.unit.charge_plural", "charges"); + QString msg = Localization::instance().get("ui.unit.farm_completed_charges", "Farm completed! (%1 %2 remaining)") .arg(unit.buildCharges) - .arg(unit.buildCharges == 1 ? "lading" : "ladingen"); + .arg(chargeWord); addFloatingText(msg, QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f); return; } } - addFloatingText("Boerderij voltooid!", QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f); + addFloatingText(Localization::instance().get("ui.unit.farm_completed", "Farm completed!"), + QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.8f); +} + +void GameManager::processUnitActionQueue(Unit &unit, Matrix2D *mapPoints, int numPoints05) +{ + if (unit.isDead() || unit.state != UnitState::Idle || !unit.hasQueuedAction()) { + return; + } + + UnitAction action = unit.popNextQueuedAction(); + + switch (action.type) { + case UnitActionType::Move: { + int tx = action.targetX; + int ty = action.targetY; + if (tx == unit.tileX && ty == unit.tileY) { + return; + } + if (!TerrainMovementManager::instance().isHexPassable( + tx, ty, unit.config.movementDomain, mapPoints, numPoints05, &unit.config)) { + addFloatingText(Localization::instance().get("ui.unit.impassable_terrain", "Impassable terrain!"), + unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f); + return; + } + + std::vector> path = Pathfinder::findPath( + unit.tileX, unit.tileY, tx, ty, mapPoints, numPoints05, + unit.config.movementDomain, &unit.config + ); + + if (path.size() > 1) { + unit.setPath(path); + addFloatingText(Localization::instance().get("ui.unit.moving", "Moving"), + unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 230, 80)); + } else { + addFloatingText(Localization::instance().get("ui.unit.no_path", "No reachable path!"), + unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 100, 100), 2.5f); + } + break; + } + case UnitActionType::BuildImprovement: { + QString impId = action.extraData.isEmpty() ? "farm" : action.extraData; + int tx = unit.tileX; + int ty = unit.tileY; + + if (!unit.config.canBuildFarm()) { + return; + } + + if (!unit.config.infiniteCharges && unit.buildCharges <= 0) { + addFloatingText(Localization::instance().get("ui.unit.no_build_charges", "No build charges remaining!"), + unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90)); + return; + } + + if (hasImprovementAt(tx, ty)) { + addFloatingText(Localization::instance().get("ui.unit.tile_occupied", "Tile already has an improvement!"), + unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 90, 90)); + return; + } + + QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); + const ImprovementConfig *farmCfg = TerrainYieldManager::instance().getImprovementConfig(impId); + QStringList validTerrains = farmCfg ? farmCfg->validTerrains : QStringList{"grasland", "tundra"}; + + if (!validTerrains.contains(tType)) { + QString validStr = validTerrains.join(", "); + addFloatingText(Localization::instance().get("ui.unit.cannot_build_here", "Cannot build here! Allowed on: %1").arg(validStr), + unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); + return; + } + + float duration = unit.config.getActionDuration("boerderij_bouwen", 10.0f); + unit.startBuilding(impId, duration); + addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm..."), + unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f); + break; + } + case UnitActionType::BuildCity: { + if (!unit.config.canBuildCity()) { + return; + } + int tx = unit.tileX; + int ty = unit.tileY; + + // Check if tile already has a city + for (const auto &c : m_cities) { + if (c.tileX == tx && c.tileY == ty) { + addFloatingText(Localization::instance().get("ui.unit.city_already_here", "A city already exists here!"), + unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 80, 80)); + return; + } + } + + const CityConfig &cCfg = TerrainYieldManager::instance().getCityConfig(); + int cId = getNextCityId(); + QString cityName = QString("City %1").arg(cId); + QVector2D w = Pathfinder::hexToWorld(tx, ty); + float gz = Unit::getTerrainHeight(w.x(), w.y(), mapPoints, numPoints05); + + City newCity(cId, unit.playerId, cityName, tx, ty, + QVector3D(w.x(), w.y(), gz), + cCfg.initialInhabitants, cCfg.inhabitantsPerPop, cCfg.initialBorderRadius); + + QString tType = TerrainYieldManager::instance().getTerrainType(tx, ty, mapPoints, numPoints05); + const TerrainYieldConfig *yCfg = TerrainYieldManager::instance().getYieldConfig(tType); + QString defRes = yCfg ? yCfg->primaryResource : "voedsel"; + newCity.assignWorker(tx, ty, defRes); + + m_cities.push_back(newCity); + addFloatingText(Localization::instance().get("ui.unit.city_founded", "%1 founded!").arg(cityName), + QVector3D(w.x(), w.y(), gz + 1.2f), QColor(80, 255, 120), 2.5f); + + unit.takeDamage(99999.0f); + if (unit.id == m_selectedUnitId) { + m_selectedUnitId = -1; + m_actionMode = ActionMode::None; + } + break; + } + case UnitActionType::Attack: { + Unit *target = nullptr; + for (auto &other : m_units) { + if (other->id == action.targetUnitId && !other->isDead()) { + target = other.get(); + break; + } + } + if (target && unit.canAttack(*target)) { + unit.performAttack(*target); + addFloatingText(QString("-%1 HP").arg(unit.config.attackDamage), + target->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 50, 50)); + if (target->isDead()) { + addFloatingText(Localization::instance().get("ui.unit.defeated", "Defeated!"), + target->worldPos + QVector3D(0, 0, 1.2f), QColor(255, 200, 50), 2.2f); + } + } + break; + } + default: + break; + } } diff --git a/gamemanager.h b/gamemanager.h index 16f66fc..1be2bef 100644 --- a/gamemanager.h +++ b/gamemanager.h @@ -91,6 +91,7 @@ public: void commandBuildCitySelected(Matrix2D *mapPoints, int numPoints05); void commandBuildFarmSelected(Matrix2D *mapPoints, int numPoints05); void completeUnitBuilding(Unit &unit, Matrix2D *mapPoints, int numPoints05); + void processUnitActionQueue(Unit &unit, Matrix2D *mapPoints, int numPoints05); const std::map, TileImprovement>& getTileImprovements() const { return m_tileImprovements; } const TileImprovement* getImprovementAt(int tx, int ty) const; @@ -103,6 +104,10 @@ public: float getSimSpeed() const { return m_simSpeedMultiplier; } void setSimSpeed(float s) { m_simSpeedMultiplier = std::max(0.2f, std::min(10.0f, s)); } + int getMaxActionQueueSize() const { return m_maxActionQueueSize; } + bool isActionQueueingAllowed() const { return m_allowActionQueueing; } + bool isAutoQueueMoveWhenBusy() const { return m_autoQueueMoveWhenBusy; } + void addFloatingText(const QString &text, const QVector3D &worldPos, const QColor &color, float duration = 1.8f); int getNextUnitId() { return ++m_nextUnitId; } @@ -131,6 +136,10 @@ private: float m_tickTimer; float m_simSpeedMultiplier; // 1.0x, 2.0x, etc. + int m_maxActionQueueSize; + bool m_autoQueueMoveWhenBusy; + bool m_allowActionQueueing; + int m_nextUnitId; int m_nextCityId; bool m_initialized; diff --git a/qscenerenderer.cpp b/qscenerenderer.cpp index 9ecfb6f..2bf8ab8 100644 --- a/qscenerenderer.cpp +++ b/qscenerenderer.cpp @@ -715,19 +715,25 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr } else if (act == "aanvallen") { isActive = (GameManager::instance().currentActionMode() == ActionMode::Attack); iconFile = "images/actions/attack.png"; - tip = QString("Aanvallen (A) [Bereik: %1]").arg(unit->config.attackRange); + tip = Localization::instance().get("ui.unit.tip_attack", "Attack (A) [Range: %1]").arg(unit->config.attackRange); } else if (act == "steden_bouwen" || act == "build_city") { - isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity); + bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildCity); + isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity) || isQueued; iconFile = "images/actions/build_city.png"; - tip = "Steden bouwen (B)"; + tip = isQueued + ? Localization::instance().get("ui.unit.city_queued", "Found City queued on arrival") + : Localization::instance().get("ui.unit.tip_build_city", "Found City (B)"); } else if (act == "boerderij_bouwen" || act == "build_farm") { - isActive = unit->isBuilding(); + bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement); + isActive = unit->isBuilding() || isQueued; iconFile = "images/actions/farm.png"; if (unit->isBuilding()) { int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f); - tip = QString("Aan het bouwen... %1%").arg(pct); + tip = Localization::instance().get("ui.unit.tip_farm_building", "Busy building... %1%").arg(pct); + } else if (isQueued) { + tip = Localization::instance().get("ui.unit.tip_farm_queued", "Build Farm (Queued on arrival - click to cancel)"); } else { - tip = "Boerderij bouwen (F)"; + tip = Localization::instance().get("ui.unit.tip_build_farm", "Build Farm (F)"); } } @@ -918,28 +924,39 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit) painter.setFont(QFont("sans-serif", 8)); painter.setPen(QColor(200, 220, 240)); - QString speedInfo = QString("Snelheid: %1 hex/tick").arg(unit->config.movementSpeed, 0, 'f', 1); + QString speedInfo = Localization::instance().get("ui.unit.speed_format", "Speed: %1 hex/tick").arg(unit->config.movementSpeed, 0, 'f', 1); painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 76, 140, 14), Qt::AlignLeft | Qt::AlignVCenter, speedInfo); - QString costInfo = QString("Kosten: %1 pnt").arg(unit->config.cost); + QString costInfo = Localization::instance().get("ui.unit.cost_format", "Cost: %1 pts").arg(unit->config.cost); painter.drawText(QRect(cardRect.left() + 160, cardRect.top() + 76, 130, 14), Qt::AlignLeft | Qt::AlignVCenter, costInfo); if (unit->config.isMilitary()) { - QString combatInfo = QString("Aanval: %1 | Bereik: %2 hex").arg(unit->config.attackDamage).arg(unit->config.attackRange); + QString combatInfo = Localization::instance().get("ui.unit.combat_format", "Attack: %1 | Range: %2 hex") + .arg(unit->config.attackDamage).arg(unit->config.attackRange); painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, combatInfo); } else if (unit->isBuilding()) { int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f); - QString buildInfo = QString("Aan het bouwen... %1%").arg(pct); + QString buildInfo = Localization::instance().get("ui.unit.building_pct", "Building... %1%").arg(pct); if (!unit->config.infiniteCharges && unit->buildCharges > 0) { - buildInfo += QString(" | Ladingen: %1").arg(unit->buildCharges); + buildInfo += QString(" | ") + Localization::instance().get("ui.unit.charges_format", "Charges: %1").arg(unit->buildCharges); } painter.setPen(QColor(255, 210, 80)); painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, buildInfo); + } else if (unit->hasQueuedAction()) { + QString qSummary = unit->getQueuedActionSummary(); + QString queuedInfo = Localization::instance().get("ui.unit.status_queued", "Queued: %1").arg(qSummary); + if (unit->config.canBuildFarm() && !unit->config.infiniteCharges && unit->buildCharges > 0) { + queuedInfo += QString(" | ") + Localization::instance().get("ui.unit.charges_format", "Charges: %1").arg(unit->buildCharges); + } + painter.setPen(QColor(100, 220, 140)); + painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, queuedInfo); } else if (unit->config.canBuildFarm() && !unit->config.infiniteCharges && unit->buildCharges > 0) { - QString builderInfo = QString("Civiele eenheid | Ladingen: %1").arg(unit->buildCharges); + QString builderInfo = Localization::instance().get("ui.unit.civilian_unit", "Civilian unit") + QString(" | ") + + Localization::instance().get("ui.unit.charges_format", "Charges: %1").arg(unit->buildCharges); painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, builderInfo); } else { - painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, "Civiele eenheid"); + painter.drawText(QRect(cardRect.left() + 12, cardRect.top() + 94, 280, 14), Qt::AlignLeft | Qt::AlignVCenter, + Localization::instance().get("ui.unit.civilian_unit", "Civilian unit")); } // Action Bubbles Row at bottom of card @@ -958,34 +975,41 @@ void QSceneRenderer::drawUnitCard(QPainter &painter, Unit *unit) bool isActive = false; QString iconFile = "images/actions/move.png"; - QString label = "Verplaats"; - QString tip = "Verplaatsen (M)"; + QString label = Localization::instance().get("ui.unit.btn_move", "Move [M]"); + QString tip = Localization::instance().get("ui.unit.tip_move", "Move (M)"); if (act == "verplaatsen") { isActive = (GameManager::instance().currentActionMode() == ActionMode::Move); iconFile = "images/actions/move.png"; - label = "Beweeg [M]"; - tip = "Verplaatsen (M)"; + label = Localization::instance().get("ui.unit.btn_move", "Move [M]"); + tip = Localization::instance().get("ui.unit.tip_move", "Move (M)"); } else if (act == "aanvallen") { isActive = (GameManager::instance().currentActionMode() == ActionMode::Attack); iconFile = "images/actions/attack.png"; - label = "Aanval [A]"; - tip = QString("Aanvallen (A) [Bereik: %1]").arg(unit->config.attackRange); + label = Localization::instance().get("ui.unit.btn_attack", "Attack [A]"); + tip = Localization::instance().get("ui.unit.tip_attack", "Attack (A) [Range: %1]").arg(unit->config.attackRange); } else if (act == "steden_bouwen" || act == "build_city") { - isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity); + bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildCity); + isActive = (GameManager::instance().currentActionMode() == ActionMode::BuildCity) || isQueued; iconFile = "images/actions/build_city.png"; - label = "Sticht [B]"; - tip = "Steden bouwen (B)"; + label = isQueued ? "Queued" : Localization::instance().get("ui.unit.btn_build_city", "Settle [B]"); + tip = isQueued + ? Localization::instance().get("ui.unit.city_queued", "Found City queued on arrival") + : Localization::instance().get("ui.unit.tip_build_city", "Found City (B)"); } else if (act == "boerderij_bouwen" || act == "build_farm") { - isActive = unit->isBuilding(); + bool isQueued = unit->hasQueuedActionType(UnitActionType::BuildImprovement); + isActive = unit->isBuilding() || isQueued; iconFile = "images/actions/farm.png"; if (unit->isBuilding()) { int pct = (int)(std::min(1.0f, unit->buildProgress) * 100.0f); - label = QString("Bouwt %1%").arg(pct); - tip = QString("Bezig met bouwen... %1%").arg(pct); + label = Localization::instance().get("ui.unit.btn_farm_building", "Building %1%").arg(pct); + tip = Localization::instance().get("ui.unit.tip_farm_building", "Busy building... %1%").arg(pct); + } else if (isQueued) { + label = Localization::instance().get("ui.unit.btn_farm_queued", "Farm (Queued)"); + tip = Localization::instance().get("ui.unit.tip_farm_queued", "Build Farm (Queued on arrival - click to cancel)"); } else { - label = "Boerderij [F]"; - tip = "Boerderij bouwen (F)"; + label = Localization::instance().get("ui.unit.btn_build_farm", "Farm [F]"); + tip = Localization::instance().get("ui.unit.tip_build_farm", "Build Farm (F)"); } } @@ -2046,6 +2070,11 @@ void QSceneRenderer::keyPressEvent(QKeyEvent *e) else if (e->key() == Qt::Key_Escape) { if (GameManager::instance().currentActionMode() != ActionMode::None) { GameManager::instance().setActionMode(ActionMode::None); + } else if (sel && sel->hasQueuedAction()) { + sel->clearActionQueue(); + GameManager::instance().addFloatingText( + Localization::instance().get("ui.unit.queue_cleared", "Queued actions cancelled"), + sel->worldPos + QVector3D(0, 0, 0.8f), QColor(255, 180, 80)); } else { GameManager::instance().deselectUnit(); } diff --git a/unit.cpp b/unit.cpp index ced0f44..934fdc0 100644 --- a/unit.cpp +++ b/unit.cpp @@ -35,12 +35,72 @@ float Unit::getTerrainHeight(float worldX, float worldY, Matrix2D return (h / 255.0f) * 3.5f + 1.0f; } +bool Unit::queueAction(const UnitAction &action) +{ + if ((int)actionQueue.size() >= maxQueueSize) { + return false; + } + actionQueue.push_back(action); + return true; +} + +void Unit::clearActionQueue() +{ + actionQueue.clear(); +} + +bool Unit::hasQueuedActionType(UnitActionType type) const +{ + for (const auto &act : actionQueue) { + if (act.type == type) return true; + } + return false; +} + +void Unit::removeQueuedActionsOfType(UnitActionType type) +{ + for (auto it = actionQueue.begin(); it != actionQueue.end(); ) { + if (it->type == type) { + it = actionQueue.erase(it); + } else { + ++it; + } + } +} + +UnitAction Unit::popNextQueuedAction() +{ + if (actionQueue.empty()) return UnitAction(); + UnitAction act = actionQueue.front(); + actionQueue.pop_front(); + return act; +} + +const UnitAction* Unit::peekNextQueuedAction() const +{ + if (actionQueue.empty()) return nullptr; + return &actionQueue.front(); +} + +QString Unit::getQueuedActionSummary() const +{ + if (actionQueue.empty()) return ""; + switch (actionQueue.front().type) { + case UnitActionType::Move: + return "Move"; + case UnitActionType::BuildImprovement: + return "Build Farm"; + case UnitActionType::BuildCity: + return "Found City"; + case UnitActionType::Attack: + return "Attack"; + default: + return "Action"; + } +} + void Unit::setPath(const std::vector> &newPath) { - if (state == UnitState::Building) { - cancelBuilding(); - } - path = newPath; pathIndex = 0; moveProgress = 0.0f; @@ -60,6 +120,7 @@ void Unit::stopMovement() pathIndex = 0; moveProgress = 0.0f; state = UnitState::Idle; + clearActionQueue(); } void Unit::startBuilding(const QString &impId, float duration) diff --git a/unit.h b/unit.h index aa4d69d..c365d22 100644 --- a/unit.h +++ b/unit.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include "unitconfig.h" #include "terrain.h" @@ -16,6 +17,31 @@ enum class UnitState { Building }; +enum class UnitActionType { + None, + Move, + BuildImprovement, + BuildCity, + Attack +}; + +struct UnitAction { + UnitActionType type = UnitActionType::None; + int targetX = -1; + int targetY = -1; + int targetUnitId = -1; + QString extraData = ""; // e.g. improvementId ("farm") + + UnitAction() = default; + UnitAction(UnitActionType t) : type(t) {} + UnitAction(UnitActionType t, int x, int y, const QString &data = "") + : type(t), targetX(x), targetY(y), extraData(data) {} + UnitAction(UnitActionType t, int targetId) + : type(t), targetUnitId(targetId) {} + + bool isValid() const { return type != UnitActionType::None; } +}; + class Unit { public: Unit(int uId, int pId, const UnitConfig &cfg, int startX, int startY, float groundZ); @@ -38,6 +64,19 @@ public: int targetTileX; int targetTileY; + // Action Queue + std::deque actionQueue; + int maxQueueSize = 5; + + bool queueAction(const UnitAction &action); + void clearActionQueue(); + bool hasQueuedAction() const { return !actionQueue.empty(); } + bool hasQueuedActionType(UnitActionType type) const; + void removeQueuedActionsOfType(UnitActionType type); + UnitAction popNextQueuedAction(); + const UnitAction* peekNextQueuedAction() const; + QString getQueuedActionSummary() const; + // Attack cooldown / flash float attackFlashTimer; @@ -54,6 +93,7 @@ public: void startBuilding(const QString &impId, float duration); void cancelBuilding(); bool isBuilding() const { return state == UnitState::Building; } + bool isBusy() const { return state == UnitState::Building; } bool canAttack(const Unit &other) const; void performAttack(Unit &target);