From 8c24b112d41d3f78cff30053f931e2067cfb4db0 Mon Sep 17 00:00:00 2001 From: brupje Date: Tue, 10 Nov 2026 20:31:33 +0100 Subject: [PATCH] ticks --- AGENTS.md | 30 +++++++++++++++++++++++++++--- config/strings_en.yaml | 2 +- config/units/archer.yaml | 5 ++--- config/units/builder.yaml | 7 +++---- config/units/crossbowman.yaml | 5 ++--- config/units/galley.yaml | 5 ++--- config/units/horseman.yaml | 5 ++--- config/units/knight.yaml | 5 ++--- config/units/musketman.yaml | 5 ++--- config/units/settler.yaml | 5 ++--- config/units/slinger.yaml | 5 ++--- config/units/spearman.yaml | 5 ++--- config/units/swordsman.yaml | 5 ++--- config/units/warrior.yaml | 5 ++--- gamemanager.cpp | 11 ++++++----- qscenerenderer.cpp | 4 ++-- unit.cpp | 15 +++++++++------ unit.h | 3 +++ unitconfig.cpp | 4 ++-- unitconfig.h | 18 ++++++++---------- 20 files changed, 83 insertions(+), 66 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9e6988f..5249921 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -104,6 +104,30 @@ To implement the RTS Civilization game, develop modular subsystems decoupled fro +-----------------+ ``` +### 3.1 Simulation Time & Tick System + +The game combines continuous real-time rendering and interpolation (`deltaTime`) with a configurable **Simulation Tick** (`movement_tick_duration` in `config/game.yaml`, defaulting to `30.0` seconds at $1\times$ simulation speed): + +1. **Tick Timer & Speed Multiplier:** + - `GameManager::update` advances an internal tick timer by `deltaTime * simSpeedMultiplier`. + - When the timer reaches `movement_tick_duration`, a simulation tick fires (`processTickYields`). +2. **Discrete Per-Tick Economy & Progression (`processTickYields`):** + - **Food & Population:** City food yield is converted into inhabitants (`food * foodToInhabitantsRatio`). + - **Labor (Production):** City labor yield is added to the active construction project (`currentLabor`), completing units or buildings when `laborCost` is reached, or accumulating locally if idle. + - **National Stockpiles:** Culture, Faith, and Gold yields from all owned cities are added to the player's national treasury. + - **Science & Research:** Total science generated per tick across a player's cities is applied directly to the active technology in the player's `researchQueue`. +3. **Continuous Real-Time Unit Movement (`movement_speed`, Scaled by Tick Duration):** + - Units move smoothly every frame, and their movement rate is defined in **movement points (base hexes) per tick** (`movement_speed` in `config/units/*.yaml`, stored in `UnitConfig::movementSpeed` — higher values mean faster movement, e.g., `2.0` for infantry/civilian, `3.0` for galley, `4.0` for cavalry). + - Effective speed in hexes per second is calculated dynamically per hex segment as: + $$\text{hexesPerSec} = \frac{\text{movementSpeed}}{\text{tickDuration} \times \text{stepCost}} \times \text{simSpeedMultiplier}$$ + - On flat terrain (`stepCost = 1.0`), a unit with `movementSpeed = 2.0` traverses $2.0$ hexes during one full simulation tick ($0.5$ ticks per hex). +4. **Unit Construction & Action Durations (`action_durations`):** + - Unlike `movement_speed` (which is a rate per tick), unit build actions (such as `boerderij_bouwen` and `build_road` under `action_durations` in `config/units/builder.yaml`) are configured as **durations in simulation ticks** (ticks per action, accessed via `UnitConfig::getActionDuration`). + - Total real-time build duration in seconds is calculated as: + $$\text{duration} = \text{ticks} \times \text{movement\_tick\_duration}$$ + - While building, `buildProgress` interpolates continuously from $0.0$ to $1.0$ over `duration` seconds (scaled by `simSpeedMultiplier`). + - Similarly, city production (`labor_cost` in `config/units/*.yaml` and `config/buildings.yaml`) and technology research (`science_cost` in `config/technologies.yaml`) progress discretely each tick based on the city/empire's per-tick yield. + --- ## 4. Agents & Unit Specifications @@ -124,7 +148,7 @@ Units are autonomous or player-directed entities traversing the hex map in real - **Siege:** Catapult $\rightarrow$ Trebuchet $\rightarrow$ Bombard (high bonus damage against city walls). - **Naval:** Galley $\rightarrow$ Caravel $\rightarrow$ Frigate $\rightarrow$ Battleship (operates only on water hexes). -### 4.2 Unit Data Model (`Unit.h`) +### 4.2 Unit Data Model (`unit.h` / `unitconfig.h`) ```cpp enum class UnitType { Settler, Worker, Warrior, Archer, Horseman, Galley }; enum class UnitState { Idle, Moving, Attacking, Harvesting, Building, Fortified }; @@ -146,14 +170,14 @@ public: float health, maxHealth; float attackDamage; float defense; - float movementSpeed; // hexes per second + float movementSpeed; // movement points (base hexes) per simulation tick float visionRange; // in hex radius // Path & Target std::vector> currentPath; int targetEntityId; // target unit or city - void update(float deltaTime); + void update(float deltaTime, float tickDuration, float simSpeedMultiplier); void commandMoveTo(int destX, int destY); void commandAttack(int targetId); void commandBuildImprovement(int improvementType); diff --git a/config/strings_en.yaml b/config/strings_en.yaml index f37c7e0..c08202f 100644 --- a/config/strings_en.yaml +++ b/config/strings_en.yaml @@ -53,7 +53,7 @@ ui: action_scheduled_after_current: "%1 scheduled after current action" action_cancelled: "%1 cancelled" farm_queued: "Build Farm queued" - farm_started: "Constructing Farm..." + farm_started: "Constructing Farm... (%1 ticks)" farm_completed: "Farm completed!" farm_completed_consumed: "Farm completed! (Builder consumed)" farm_completed_charges: "Farm completed! (%1 %2 remaining)" diff --git a/config/units/archer.yaml b/config/units/archer.yaml index e7468c1..fc70671 100644 --- a/config/units/archer.yaml +++ b/config/units/archer.yaml @@ -11,9 +11,8 @@ attack_range: 2 required_tech: "archery" obsolete_tech: "machinery" icon: images/units/slinger.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/config/units/builder.yaml b/config/units/builder.yaml index 636da54..7cd498c 100644 --- a/config/units/builder.yaml +++ b/config/units/builder.yaml @@ -16,8 +16,7 @@ actions: - boerderij_bouwen - build_road recharge_time: 500 -recharge_duration: 300 attack_flash_duration: 400 -action_speeds: - boerderij_bouwen: 10.0 - build_road: 2 +action_durations: + boerderij_bouwen: 2.0 + build_road: 2.0 diff --git a/config/units/crossbowman.yaml b/config/units/crossbowman.yaml index a1d834f..db79816 100644 --- a/config/units/crossbowman.yaml +++ b/config/units/crossbowman.yaml @@ -11,9 +11,8 @@ attack_range: 2 required_tech: "machinery" obsolete_tech: "" icon: images/units/slinger.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/config/units/galley.yaml b/config/units/galley.yaml index 5809c34..276781f 100644 --- a/config/units/galley.yaml +++ b/config/units/galley.yaml @@ -11,9 +11,8 @@ attack_range: 1 required_tech: "sailing" obsolete_tech: "" icon: images/units/galley.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/config/units/horseman.yaml b/config/units/horseman.yaml index 3b3157c..c5ebd7b 100644 --- a/config/units/horseman.yaml +++ b/config/units/horseman.yaml @@ -11,9 +11,8 @@ attack_range: 1 required_tech: "horseback_riding" obsolete_tech: "stirrups" icon: images/units/warrior.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/config/units/knight.yaml b/config/units/knight.yaml index 476728e..f89f141 100644 --- a/config/units/knight.yaml +++ b/config/units/knight.yaml @@ -11,9 +11,8 @@ attack_range: 1 required_tech: "stirrups" obsolete_tech: "" icon: images/units/warrior.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/config/units/musketman.yaml b/config/units/musketman.yaml index ef59f82..54d633f 100644 --- a/config/units/musketman.yaml +++ b/config/units/musketman.yaml @@ -11,9 +11,8 @@ attack_range: 1 required_tech: "gunpowder" obsolete_tech: "" icon: images/units/warrior.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/config/units/settler.yaml b/config/units/settler.yaml index 60c7b10..15ec872 100644 --- a/config/units/settler.yaml +++ b/config/units/settler.yaml @@ -9,9 +9,8 @@ labor_cost: 2000 required_tech: "" obsolete_tech: "" icon: images/units/settler.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - steden_bouwen diff --git a/config/units/slinger.yaml b/config/units/slinger.yaml index d07fe41..8c5cbe3 100644 --- a/config/units/slinger.yaml +++ b/config/units/slinger.yaml @@ -11,9 +11,8 @@ attack_range: 2 required_tech: "" obsolete_tech: "archery" icon: images/units/slinger.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/config/units/spearman.yaml b/config/units/spearman.yaml index 30dd57d..15eb901 100644 --- a/config/units/spearman.yaml +++ b/config/units/spearman.yaml @@ -11,9 +11,8 @@ attack_range: 1 required_tech: "bronze_working" obsolete_tech: "gunpowder" icon: images/units/warrior.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/config/units/swordsman.yaml b/config/units/swordsman.yaml index 41da0c1..be2e46b 100644 --- a/config/units/swordsman.yaml +++ b/config/units/swordsman.yaml @@ -11,9 +11,8 @@ attack_range: 1 required_tech: "iron_working" obsolete_tech: "gunpowder" icon: images/units/warrior.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/config/units/warrior.yaml b/config/units/warrior.yaml index 3e451ca..88c1eb3 100644 --- a/config/units/warrior.yaml +++ b/config/units/warrior.yaml @@ -11,9 +11,8 @@ attack_range: 1 required_tech: "" obsolete_tech: "iron_working" icon: images/units/warrior.png -recharge_time: 500 -recharge_duration: 300 -attack_flash_duration: 400 +recharge_ticks: 2 +attack_flash_ticks: 1 actions: - verplaatsen - aanvallen diff --git a/gamemanager.cpp b/gamemanager.cpp index 6976222..1831788 100644 --- a/gamemanager.cpp +++ b/gamemanager.cpp @@ -1265,9 +1265,10 @@ void GameManager::continueCurrentUnitAction(Unit &unit, Matrix2D * } // Unit is on the target tile: start constructing the improvement - float duration = unit.config.getActionDuration("boerderij_bouwen", 10.0f); + float ticks = unit.config.getActionDuration("boerderij_bouwen", 2.0f); + float duration = ticks * m_movementTickDuration; unit.startBuilding(impId, duration); - addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm..."), + addFloatingText(Localization::instance().get("ui.unit.farm_started", "Constructing Farm... (%1 ticks)").arg(ticks, 0, 'g', 3), unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f); break; } @@ -1789,10 +1790,10 @@ void GameManager::advanceRoadRoute(Unit &unit, Matrix2D *mapPoints QString defaultType = m_roadConfigManager.defaultRoadType(); const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType); QString roadName = rCfg ? rCfg->name : "Road"; - int ticks = m_roadConfigManager.buildDurationTicks(); - float duration = (float)ticks * m_movementTickDuration; + float ticks = unit.config.getActionDuration("build_road", (float)m_roadConfigManager.buildDurationTicks()); + float duration = ticks * m_movementTickDuration; unit.startBuilding(QString("road:%1").arg(defaultType), duration); - addFloatingText(Localization::instance().get("ui.unit.road_started", "Constructing %1... (%2 ticks)").arg(roadName).arg(ticks), + addFloatingText(Localization::instance().get("ui.unit.road_started", "Constructing %1... (%2 ticks)").arg(roadName).arg(ticks, 0, 'g', 3), unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f); return; } diff --git a/qscenerenderer.cpp b/qscenerenderer.cpp index 4abbeb1..8ce50a7 100644 --- a/qscenerenderer.cpp +++ b/qscenerenderer.cpp @@ -770,8 +770,8 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr } // Recharge bar below military units when recharging - if (unit->config.canAttack() && unit->isRecharging()) { - float rechargePercent = std::max(0.0f, std::min(1.0f, unit->rechargeTimer / unit->config.rechargeTime)); + if (unit->isRecharging()) { + float rechargePercent = std::max(0.0f, std::min(1.0f, unit->rechargeTimer / unit->config.rechargeTicks)); QRectF recBg(headCenter.x() - 14.0f, headCenter.y() - R - 5.5f, 28.0f, 3.5f); painter.setBrush(QColor(30, 30, 30, 220)); painter.setPen(Qt::NoPen); diff --git a/unit.cpp b/unit.cpp index 7538b62..3f03761 100644 --- a/unit.cpp +++ b/unit.cpp @@ -12,7 +12,8 @@ Unit::Unit(int uId, int pId, const UnitConfig &cfg, int startX, int startY, floa buildCharges(cfg.buildCharges), state(UnitState::Idle), pathIndex(0), moveProgress(0.0f), targetTileX(startX), targetTileY(startY), attackFlashTimer(0.0f), rechargeTimer(0.0f), - buildProgress(0.0f), buildDuration(0.0f), buildingImprovementId("") + buildProgress(0.0f), buildDuration(0.0f), buildingImprovementId(""), + movement_tick_duration(30.0f) // Default: 30 seconds per tick at 1x speed { QVector2D w = Pathfinder::hexToWorld(startX, startY); worldPos = QVector3D(w.x(), w.y(), groundZ); @@ -43,8 +44,8 @@ void Unit::performAttack(Unit &target) { if (!canAttack(target)) return; - attackFlashTimer = config.attackFlashDuration / 1000.0f; // Convert ms to seconds - rechargeTimer = config.rechargeTime / 1000.0f; // Convert ms to seconds + attackFlashTimer = config.attackFlashTicks * movement_tick_duration; // Convert ticks to seconds + rechargeTimer = config.rechargeTicks * movement_tick_duration; // Convert ticks to seconds target.takeDamage((float)config.attackDamage); @@ -60,8 +61,8 @@ void Unit::performAttack(Unit &target) void Unit::takeDamage(float damage) { health = std::max(0.0f, health - damage); - attackFlashTimer = 0.4f; - rechargeTimer = config.rechargeTime / 1000.0f; // Reset recharge on taking damage + attackFlashTimer = movement_tick_duration; // Flash lasts 1 tick + rechargeTimer = config.rechargeTicks * movement_tick_duration; // Reset recharge to full duration } float Unit::getTerrainHeight(float worldX, float worldY, Matrix2D *mapPoints, int numPoints05) @@ -247,8 +248,10 @@ void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier, } // Update recharge timer (cooldown between attacks) + // Recharge decays based on simulation ticks, not real-time multiplier if (rechargeTimer > 0.0f) { - rechargeTimer = std::max(0.0f, rechargeTimer - deltaTime * simSpeedMultiplier); + float tickDurationSec = deltaTime / movement_tick_duration; + rechargeTimer = std::max(0.0f, rechargeTimer - tickDurationSec); } if (state == UnitState::Building) { diff --git a/unit.h b/unit.h index 5a6b876..3cf967b 100644 --- a/unit.h +++ b/unit.h @@ -56,6 +56,9 @@ public: float health; float maxHealth; int buildCharges; + + // Simulation parameters (set during unit creation or via GameManager) + float movement_tick_duration; // Seconds per simulation tick UnitState state; // Movement Path diff --git a/unitconfig.cpp b/unitconfig.cpp index 28c2b58..5a05668 100644 --- a/unitconfig.cpp +++ b/unitconfig.cpp @@ -82,11 +82,11 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath) if (root.hasChild(key)) { const YamlNode &sNode = root[key]; for (auto it = sNode.children.begin(); it != sNode.children.end(); ++it) { - cfg.actionSpeeds[it.key()] = it.value().asFloat(10.0f); + cfg.actionSpeeds[it.key()] = it.value().asFloat(2.0f); } } }; - parseActionMap("action_speeds"); + parseActionMap("action_durations"); parseActionMap("action_durations"); m_configs[cfg.id] = cfg; diff --git a/unitconfig.h b/unitconfig.h index 30e0877..7db14ae 100644 --- a/unitconfig.h +++ b/unitconfig.h @@ -48,23 +48,21 @@ struct UnitConfig { QString obsoleteTech; QString iconPath; QStringList actions; - QMap actionSpeeds; // Duration in seconds per action (e.g. "boerderij_bouwen": 10.0f) + QMap actionSpeeds; // Duration in simulation ticks per action (e.g. "boerderij_bouwen": 2.0f) QStringList impassableTerrains; QStringList passableTerrains; - // Recharge / Cooldown configuration for military units - int rechargeTime; // Time in milliseconds between attacks - float rechargeDuration; // Duration of recharge state (optional, for visual indication) - int attackFlashDuration; // How long the flash effect lasts (in ms) + // Recharge / Cooldown configuration for military units (in simulation ticks) + int rechargeTicks; // Ticks between attacks for military units + int attackFlashTicks; // Ticks for visual attack flash effect 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), requiredTech(""), obsoleteTech(""), iconPath(""), - rechargeTime(500), // Default recharge time: 500ms (0.5 seconds) - rechargeDuration(300), // Visual cooldown bar duration: 300ms - attackFlashDuration(400) // Attack flash effect duration: 400ms + rechargeTicks(2), // Default recharge: 2 simulation ticks + attackFlashTicks(1) // Attack flash effect: 1 tick {} bool isRanged() const { return attackRange > 1; } @@ -77,11 +75,11 @@ struct UnitConfig { bool canBuildRoad() const { return actions.contains("build_road") || actions.contains("weg_bouwen"); } bool isBuilder() const { return canBuildFarm() || canBuildRoad(); } - float getActionDuration(const QString &actionName, float fallback = 10.0f) const { + float getActionDuration(const QString &actionName, float fallbackTicks = 2.0f) const { if (actionSpeeds.contains(actionName)) { return actionSpeeds.value(actionName); } - return fallback; + return fallbackTicks; } };