Implement unit action queueing and busy state validation

This commit is contained in:
bart
2026-11-08 21:31:56 +01:00
parent cbba54969a
commit 4b237872eb
7 changed files with 502 additions and 50 deletions
+285 -19
View File
@@ -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<TerrainPoint> *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<Unit>(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<TerrainPoint> *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, Matrix2D<Terrain
return;
}
// If unit is busy building, it CANNOT walk right now. Queue the move action!
if (selected->isBuilding()) {
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<std::pair<int, int>> 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<TerrainPoint> *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<TerrainPoint> *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<TerrainPoint> *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<TerrainPoint> *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<TerrainPoint> *mapPoints, int numPoints05)
@@ -662,7 +782,8 @@ void GameManager::completeUnitBuilding(Unit &unit, Matrix2D<TerrainPoint> *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<TerrainPoint> *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<TerrainPoint> *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<std::pair<int, int>> 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;
}
}