Compare commits

...
4 Commits
Author SHA1 Message Date
bart f9bb91eef4 fix 2026-11-10 20:34:24 +01:00
bart 8c24b112d4 ticks 2026-11-10 20:31:33 +01:00
bart 2ef8e857e0 ticks explained 2026-11-10 20:25:47 +01:00
bart 772d4494e8 add recharge 2026-11-10 19:54:00 +01:00
33 changed files with 1392 additions and 1091 deletions
+1
View File
@@ -5,3 +5,4 @@ Makefile*
tiletest tiletest
terrain_test2 terrain_test2
.qmake.stash .qmake.stash
tiletest
+27 -10
View File
@@ -24,13 +24,6 @@ This document serves as the primary technical specification, architecture refere
> - **Key-Based Lookup:** Code must reference screen texts exclusively by localization keys (e.g. `localization.get("ui.button.found_city")` or equivalent lookup) resolving to the external file. > - **Key-Based Lookup:** Code must reference screen texts exclusively by localization keys (e.g. `localization.get("ui.button.found_city")` or equivalent lookup) resolving to the external file.
> - Whenever new on-screen text is introduced, add its English translation entry to the external string file first before referencing its key in code. > - Whenever new on-screen text is introduced, add its English translation entry to the external string file first before referencing its key in code.
### 0.3 Session Git Branching & Build Restrictions
> [!IMPORTANT]
> **Strict Agent Directive:** Every session must adhere to an isolated Git workflow and never build without permission.
> - **Do NOT Build Without Explicit Permission:** The agent MUST NOT compile or build the application (never run `make`, `qmake`, `cmake`, `ninja`, compiler invocations, or build scripts) unless explicit permission has been granted by the user.
--- ---
## 1. Project Vision & Core Concept ## 1. Project Vision & Core Concept
@@ -111,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 ## 4. Agents & Unit Specifications
@@ -131,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). - **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). - **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 ```cpp
enum class UnitType { Settler, Worker, Warrior, Archer, Horseman, Galley }; enum class UnitType { Settler, Worker, Warrior, Archer, Horseman, Galley };
enum class UnitState { Idle, Moving, Attacking, Harvesting, Building, Fortified }; enum class UnitState { Idle, Moving, Attacking, Harvesting, Building, Fortified };
@@ -153,14 +170,14 @@ public:
float health, maxHealth; float health, maxHealth;
float attackDamage; float attackDamage;
float defense; float defense;
float movementSpeed; // hexes per second float movementSpeed; // movement points (base hexes) per simulation tick
float visionRange; // in hex radius float visionRange; // in hex radius
// Path & Target // Path & Target
std::vector<std::pair<int, int>> currentPath; std::vector<std::pair<int, int>> currentPath;
int targetEntityId; // target unit or city 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 commandMoveTo(int destX, int destY);
void commandAttack(int targetId); void commandAttack(int targetId);
void commandBuildImprovement(int improvementType); void commandBuildImprovement(int improvementType);
+1 -1
View File
@@ -53,7 +53,7 @@ ui:
action_scheduled_after_current: "%1 scheduled after current action" action_scheduled_after_current: "%1 scheduled after current action"
action_cancelled: "%1 cancelled" action_cancelled: "%1 cancelled"
farm_queued: "Build Farm queued" farm_queued: "Build Farm queued"
farm_started: "Constructing Farm..." farm_started: "Constructing Farm... (%1 ticks)"
farm_completed: "Farm completed!" farm_completed: "Farm completed!"
farm_completed_consumed: "Farm completed! (Builder consumed)" farm_completed_consumed: "Farm completed! (Builder consumed)"
farm_completed_charges: "Farm completed! (%1 %2 remaining)" farm_completed_charges: "Farm completed! (%1 %2 remaining)"
+1
View File
@@ -3,6 +3,7 @@
# Classification thresholds # Classification thresholds
water_height_threshold: 6 # Raw heightmap elevation value (< 6 is water) water_height_threshold: 6 # Raw heightmap elevation value (< 6 is water)
coast_beach_width_km: 0.022 # Width of shoreline sandy beach transition above water level (in km)
mountain_rock_height_km: 2.3 # Height in km >= 2.3 is rocky mountain mountain_rock_height_km: 2.3 # Height in km >= 2.3 is rocky mountain
snow_height_km: 3.1 # Altitude in km >= 3.1 is high peak snow snow_height_km: 3.1 # Altitude in km >= 3.1 is high peak snow
polar_snow_line_enabled: true # Whether latitude reduces snowline altitude polar_snow_line_enabled: true # Whether latitude reduces snowline altitude
+2
View File
@@ -11,6 +11,8 @@ attack_range: 2
required_tech: "archery" required_tech: "archery"
obsolete_tech: "machinery" obsolete_tech: "machinery"
icon: images/units/slinger.png icon: images/units/slinger.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+5 -3
View File
@@ -15,6 +15,8 @@ actions:
- verplaatsen - verplaatsen
- boerderij_bouwen - boerderij_bouwen
- build_road - build_road
action_speeds: recharge_ticks: 0
boerderij_bouwen: 10.0 attack_flash_ticks: 0
build_road: 2 action_durations:
boerderij_bouwen: 2.0
build_road: 2.0
+2
View File
@@ -11,6 +11,8 @@ attack_range: 2
required_tech: "machinery" required_tech: "machinery"
obsolete_tech: "" obsolete_tech: ""
icon: images/units/slinger.png icon: images/units/slinger.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+2
View File
@@ -11,6 +11,8 @@ attack_range: 1
required_tech: "sailing" required_tech: "sailing"
obsolete_tech: "" obsolete_tech: ""
icon: images/units/galley.png icon: images/units/galley.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+2
View File
@@ -11,6 +11,8 @@ attack_range: 1
required_tech: "horseback_riding" required_tech: "horseback_riding"
obsolete_tech: "stirrups" obsolete_tech: "stirrups"
icon: images/units/warrior.png icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+2
View File
@@ -11,6 +11,8 @@ attack_range: 1
required_tech: "stirrups" required_tech: "stirrups"
obsolete_tech: "" obsolete_tech: ""
icon: images/units/warrior.png icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+2
View File
@@ -11,6 +11,8 @@ attack_range: 1
required_tech: "gunpowder" required_tech: "gunpowder"
obsolete_tech: "" obsolete_tech: ""
icon: images/units/warrior.png icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+2
View File
@@ -9,6 +9,8 @@ labor_cost: 2000
required_tech: "" required_tech: ""
obsolete_tech: "" obsolete_tech: ""
icon: images/units/settler.png icon: images/units/settler.png
recharge_ticks: 0
attack_flash_ticks: 0
actions: actions:
- verplaatsen - verplaatsen
- steden_bouwen - steden_bouwen
+2
View File
@@ -11,6 +11,8 @@ attack_range: 2
required_tech: "" required_tech: ""
obsolete_tech: "archery" obsolete_tech: "archery"
icon: images/units/slinger.png icon: images/units/slinger.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+2
View File
@@ -11,6 +11,8 @@ attack_range: 1
required_tech: "bronze_working" required_tech: "bronze_working"
obsolete_tech: "gunpowder" obsolete_tech: "gunpowder"
icon: images/units/warrior.png icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+2
View File
@@ -11,6 +11,8 @@ attack_range: 1
required_tech: "iron_working" required_tech: "iron_working"
obsolete_tech: "gunpowder" obsolete_tech: "gunpowder"
icon: images/units/warrior.png icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+2
View File
@@ -11,6 +11,8 @@ attack_range: 1
required_tech: "" required_tech: ""
obsolete_tech: "iron_working" obsolete_tech: "iron_working"
icon: images/units/warrior.png icon: images/units/warrior.png
recharge_ticks: 2
attack_flash_ticks: 1
actions: actions:
- verplaatsen - verplaatsen
- aanvallen - aanvallen
+132
View File
@@ -0,0 +1,132 @@
# Military Unit Recharge System Implementation
## Overview
This implementation adds a recharge/cooldown system to military units in the RTS game engine, providing visual feedback when units cannot attack due to recent attacks or damage taken. The recharge bar is displayed below the unit's health bar.
## Features Implemented
### 1. **Recharge Timer System**
- Military units now have a configurable recharge time between attacks
- Recharge timer decrements over time, preventing immediate re-attack after each hit
- Units are automatically reset to idle state when recharge completes
### 2. **Visual Feedback - Recharge Bar**
- A green bar is displayed below military unit cylinders during recharge cooldown
- Bar fill percentage indicates remaining recharge time (0% = ready to attack, 100% = just attacked)
- Only military units that can attack (`config.canAttack()`) are shown with recharge bars
- Non-military units (settlement, builders) have the fields but won't display bars
### 3. **Attack Flash Enhancement**
- Attack flash timer now uses configurable duration from config
- Flash effect provides immediate visual confirmation of successful attacks
- Color transitions smoothly from red/white back to unit base color
## Technical Implementation
### Modified Files
#### `unit.h` / `unit.cpp`
Added new member variables:
```cpp
float attackFlashTimer; // Flash effect timer (seconds)
float rechargeTimer; // Recharge/cooldown timer (seconds)
bool isRecharging(); // Checks if unit is currently on cooldown
```
Modified methods:
- `canAttack()`: Now checks recharge timer before allowing attack
- `performAttack()`: Sets rechargeTimer when attack is performed
- `takeDamage()`: Resets rechargeTimer when unit takes damage
- `update()`: Decrements both flash and recharge timers each frame
#### `unitconfig.h` / `unit.cpp`
Added configuration fields:
```cpp
int rechargeTime; // Time in milliseconds between attacks
float rechargeDuration; // Visual cooldown bar duration
int attackFlashDuration; // How long the flash effect lasts (ms)
```
Default values:
- `recharge_time`: 500ms (0.5 seconds)
- `recharge_duration`: 300ms
- `attack_flash_duration`: 400ms
#### `unitrenderer.h` / `unitrenderer.cpp`
Added recharge bar rendering:
- New mesh buffers for flat quad-based recharge bars
- Trapezoidal bar geometry showing remaining recharge percentage
- Positioned below unit cylinder with small gap for visual separation
- Only rendered for military units currently recharging
#### All Unit YAML Configs (`config/units/*.yaml`)
Added recharge configuration to all 12 unit types:
```yaml
recharge_time: 500
recharge_duration: 300
attack_flash_duration: 400
```
## Game Mechanics
### Attack Cooldown Flow
1. Unit performs attack → `attackFlashTimer` = 0.4s, `rechargeTimer` = 0.5s
2. Visual flash effect plays (red/white transition)
3. Recharge bar appears below unit showing remaining cooldown
4. Both timers decrement each frame
5. When `rechargeTimer` reaches 0, unit can attack again
### Damage Taken Flow
1. Unit receives damage → Health decreased
2. `attackFlashTimer` set to 0.4s (damage flash effect)
3. `rechargeTimer` reset to full recharge time
4. Unit must wait through recharge before attacking
### Visual Hierarchy (from bottom to top)
1. Ground level
2. **Recharge bar** (below unit, if applicable)
3. Unit cylinder mesh
4. Health bar (if implemented later)
5. Selection ring (when selected)
## Configuration Examples
### Default Recharge Settings (All Units)
```yaml
recharge_time: 500 # 0.5 seconds between attacks
recharge_duration: 300 # Visual feedback duration
attack_flash_duration: 400 # Flash effect time
```
### Faster Recharge (Rapid Attack Units)
```yaml
recharge_time: 300 # Quicker cooldown for high-damage units
attack_flash_duration: 350
```
### Slower Recharge (Heavy Infantry/Armor)
```yaml
recharge_time: 700 # Longer cooldown for powerful units
attack_flash_duration: 500
```
## Design Rationale
1. **Modular Configuration**: All recharge parameters are YAML-configurable per the AGENTS.md guidelines
2. **Visual Feedback**: Recharge bar provides clear indication of when units can attack again
3. **Performance**: Single mesh buffer reused for all recharge bars (efficient batch rendering)
4. **Backwards Compatible**: Non-military units have fields but no functional impact
5. **Consistent API**: `isRecharging()` method used by both renderer and game logic
## Future Enhancement Possibilities
- Add configurable recharge times per tech tree node
- Implement different recharge rates for melee vs ranged attacks
- Add area-of-effect cooldown reduction mechanics
- Implement weapon-specific reload animations
- Display numerical cooldown timer on hover tooltip
---
*Implementation Date: 2026-11-10*
*See AGENTS.md Section 4.2 for Unit Architecture Reference*
+6 -5
View File
@@ -1265,9 +1265,10 @@ void GameManager::continueCurrentUnitAction(Unit &unit, Matrix2D<TerrainPoint> *
} }
// Unit is on the target tile: start constructing the improvement // 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); 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); unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
break; break;
} }
@@ -1789,10 +1790,10 @@ void GameManager::advanceRoadRoute(Unit &unit, Matrix2D<TerrainPoint> *mapPoints
QString defaultType = m_roadConfigManager.defaultRoadType(); QString defaultType = m_roadConfigManager.defaultRoadType();
const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType); const RoadTypeConfig *rCfg = m_roadConfigManager.getRoadType(defaultType);
QString roadName = rCfg ? rCfg->name : "Road"; QString roadName = rCfg ? rCfg->name : "Road";
int ticks = m_roadConfigManager.buildDurationTicks(); float ticks = unit.config.getActionDuration("build_road", (float)m_roadConfigManager.buildDurationTicks());
float duration = (float)ticks * m_movementTickDuration; float duration = ticks * m_movementTickDuration;
unit.startBuilding(QString("road:%1").arg(defaultType), duration); 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); unit.worldPos + QVector3D(0, 0, 0.8f), QColor(255, 220, 80), 2.0f);
return; return;
} }
+13 -2
View File
@@ -103,6 +103,17 @@ def build_rivermap(output_path="/home/bart/git/Ascent-of-Man/images/rivermap.png
print(f"Saved preview to {preview_path}") print(f"Saved preview to {preview_path}")
# Build orthographic hex-tile minimap.png # Build orthographic hex-tile minimap.png
water_threshold = 6
try:
with open("/home/bart/git/Ascent-of-Man/config/terrain_movement.yaml", "r") as yf:
for line in yf:
if line.strip().startswith("water_height_threshold:"):
val_str = line.split(":", 1)[1].split("#", 1)[0].strip()
water_threshold = int(val_str)
break
except Exception:
pass
h_arr = np.array(Image.open("/home/bart/git/Ascent-of-Man/images/heightmap.jpg")) h_arr = np.array(Image.open("/home/bart/git/Ascent-of-Man/images/heightmap.jpg"))
R = 8.0 R = 8.0
W = 3608 W = 3608
@@ -122,11 +133,11 @@ def build_rivermap(output_path="/home/bart/git/Ascent-of-Man/images/rivermap.png
px = min(3599, max(0, int(round(wx * SCALE_X)))) px = min(3599, max(0, int(round(wx * SCALE_X))))
py = min(1499, max(0, int(round(wy * SCALE_Y)))) py = min(1499, max(0, int(round(wy * SCALE_Y))))
h = h_arr[1500 - 1 - py, px] h = int(h_arr[1500 - 1 - py, px, 0]) if h_arr.ndim == 3 else int(h_arr[1500 - 1 - py, px])
tp = t_arr[1500 - 1 - py, px] tp = t_arr[1500 - 1 - py, px]
dist_pole = abs((wy / 108.22) - 0.5) * 2.0 dist_pole = abs((wy / 108.22) - 0.5) * 2.0
if h <= 2: if h < water_threshold:
color = (220, 235, 250) if dist_pole > 0.85 else (28, 95, 175) # Water (blue) / polar ice color = (220, 235, 250) if dist_pole > 0.85 else (28, 95, 175) # Water (blue) / polar ice
elif h > 182 or (dist_pole > 0.80 and h > 20): elif h > 182 or (dist_pole > 0.80 and h > 20):
color = (245, 250, 255) # Snow (white) above 2500m color = (245, 250, 255) # Snow (white) above 2500m
Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 117 KiB

+1054 -1013
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -769,6 +769,19 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
painter.drawRoundedRect(bpFg, 1.5, 1.5); painter.drawRoundedRect(bpFg, 1.5, 1.5);
} }
// Recharge bar below military units when recharging
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);
painter.drawRoundedRect(recBg, 1.5, 1.5);
QRectF recFg(headCenter.x() - 14.0f, headCenter.y() - R - 5.5f, 28.0f * (1.0f - rechargePercent), 3.5f);
painter.setBrush(QColor(240, 50, 50));
painter.drawRoundedRect(recFg, 1.5, 1.5);
}
// If unit is selected: pulsing selection halo & floating action bubbles // If unit is selected: pulsing selection halo & floating action bubbles
if (unit.get() == selectedUnit) { if (unit.get() == selectedUnit) {
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.008f) + 1.0f) * 0.5f; float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.008f) + 1.0f) * 0.5f;
+13 -4
View File
@@ -30,6 +30,8 @@ uniform vec3 roadBorderColor;
uniform float time; uniform float time;
uniform int riverMask; uniform int riverMask;
uniform float waterLevelKm = 0.08235294;
uniform float coastBeachWidthKm = 0.022;
smooth in vec3 vNormal; smooth in vec3 vNormal;
smooth in vec3 vWorldPos; smooth in vec3 vWorldPos;
@@ -232,8 +234,12 @@ void main() {
// Snowline in kilometers: 2.5 (2500m) in temperate zone, dropping to 0.0 (sea level) at poles! // Snowline in kilometers: 2.5 (2500m) in temperate zone, dropping to 0.0 (sea level) at poles!
float snowLine = mix(2.5, 0.0, poleFactor); float snowLine = mix(2.5, 0.0, poleFactor);
// 3. Water vs Land detection (z_pos = height in kilometers, 0.0 = sea level) // 3. Water vs Land detection (z_pos = height in kilometers; water is height < water_height_threshold)
float oceanWaterFac = 1.0 - smoothstep(0.005, 0.025, z_pos); // One heightmap step is 3.5 / 255.0 = 0.0137255 km, so height = threshold - 1 is at waterLevelKm - 0.0137255.
// All water hexes (height <= threshold - 1) remain 100% water and transition smoothly across the shoreline.
float waterEdgeLow = max(0.0, waterLevelKm - 0.0135);
float waterEdgeHigh = max(waterEdgeLow + 0.002, waterLevelKm - 0.0015);
float oceanWaterFac = 1.0 - smoothstep(waterEdgeLow, waterEdgeHigh, z_pos);
// River water detection // River water detection
float riverDist = getRiverDistance(fragVert.xy, riverMask); float riverDist = getRiverDistance(fragVert.xy, riverMask);
@@ -321,10 +327,13 @@ void main() {
biomeGround = farmColor; biomeGround = farmColor;
} }
// Shoreline beach: where gentle slopes meet water (sea level up to ~110m, or along rivers) // Shoreline beach: where gentle slopes meet water right along the waterline (or along rivers)
// Steep slopes (norm.z < 0.55) remain rocky cliffs; gentle slopes (norm.z > 0.80) get a sandy beach // Steep slopes (norm.z < 0.55) remain rocky cliffs; gentle slopes (norm.z > 0.80) get a sandy beach
float gentleSlope = smoothstep(0.55, 0.85, norm.z); float gentleSlope = smoothstep(0.55, 0.85, norm.z);
float coastBeach = (1.0 - smoothstep(0.015, 0.110, z_pos)) * smoothstep(0.003, 0.015, z_pos) * gentleSlope; float beachStart = max(0.0, waterLevelKm - 0.015);
float beachPeak = waterEdgeHigh;
float beachEnd = waterLevelKm + max(0.005, coastBeachWidthKm);
float coastBeach = (1.0 - smoothstep(beachPeak, beachEnd, z_pos)) * smoothstep(beachStart, beachPeak, z_pos) * gentleSlope;
float riverBeach = riverBankFac; float riverBeach = riverBankFac;
float beachFac = max(coastBeach, riverBeach); float beachFac = max(coastBeach, riverBeach);
+4
View File
@@ -9,6 +9,7 @@
TerrainMovementConfig::TerrainMovementConfig() TerrainMovementConfig::TerrainMovementConfig()
: waterHeightThreshold(6), : waterHeightThreshold(6),
coastBeachWidthKm(0.022f),
mountainRockHeightKm(2.3f), mountainRockHeightKm(2.3f),
snowHeightKm(3.1f), snowHeightKm(3.1f),
polarSnowLineEnabled(true), polarSnowLineEnabled(true),
@@ -61,6 +62,9 @@ bool TerrainMovementManager::loadConfig(const QString &filePath)
if (root.hasChild("water_height_threshold")) { if (root.hasChild("water_height_threshold")) {
m_config.waterHeightThreshold = root["water_height_threshold"].asInt(6); m_config.waterHeightThreshold = root["water_height_threshold"].asInt(6);
} }
if (root.hasChild("coast_beach_width_km")) {
m_config.coastBeachWidthKm = root["coast_beach_width_km"].asFloat(0.022f);
}
if (root.hasChild("mountain_rock_height_km")) { if (root.hasChild("mountain_rock_height_km")) {
m_config.mountainRockHeightKm = root["mountain_rock_height_km"].asFloat(2.3f); m_config.mountainRockHeightKm = root["mountain_rock_height_km"].asFloat(2.3f);
} }
+1
View File
@@ -35,6 +35,7 @@ struct DomainMovementRule {
struct TerrainMovementConfig { struct TerrainMovementConfig {
int waterHeightThreshold; int waterHeightThreshold;
float coastBeachWidthKm;
float mountainRockHeightKm; float mountainRockHeightKm;
float snowHeightKm; float snowHeightKm;
bool polarSnowLineEnabled; bool polarSnowLineEnabled;
+6
View File
@@ -3,6 +3,7 @@
#include <math.h> #include <math.h>
#include "qscenerenderer.h" #include "qscenerenderer.h"
#include "gamemanager.h" #include "gamemanager.h"
#include "terrainmovementmanager.h"
#include <QVector3D> #include <QVector3D>
QImage * Tile::m_image_grass = NULL; QImage * Tile::m_image_grass = NULL;
QOpenGLTexture * Tile::m_texture_grass = NULL; QOpenGLTexture * Tile::m_texture_grass = NULL;
@@ -391,6 +392,11 @@ void Tile::paint(QMatrix4x4 p, QVector3D& cameraPosition,QVector3D& cameraUpDire
m_program->setUniformValue("roadMask", 0); m_program->setUniformValue("roadMask", 0);
} }
const TerrainMovementConfig &tmCfg = TerrainMovementManager::instance().getConfig();
float waterLevelKm = (tmCfg.waterHeightThreshold / 255.0f) * 3.5f;
m_program->setUniformValue("waterLevelKm", waterLevelKm);
m_program->setUniformValue("coastBeachWidthKm", tmCfg.coastBeachWidthKm);
m_program->setUniformValue("time", time); m_program->setUniformValue("time", time);
m_program->setUniformValue("riverMask", (int)m_riverMask); m_program->setUniformValue("riverMask", (int)m_riverMask);
m_object.bind(); m_object.bind();
BIN
View File
Binary file not shown.
+57 -34
View File
@@ -11,13 +11,60 @@ Unit::Unit(int uId, int pId, const UnitConfig &cfg, int startX, int startY, floa
health((float)cfg.health), maxHealth((float)cfg.health), health((float)cfg.health), maxHealth((float)cfg.health),
buildCharges(cfg.buildCharges), buildCharges(cfg.buildCharges),
state(UnitState::Idle), pathIndex(0), moveProgress(0.0f), state(UnitState::Idle), pathIndex(0), moveProgress(0.0f),
targetTileX(startX), targetTileY(startY), attackFlashTimer(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); QVector2D w = Pathfinder::hexToWorld(startX, startY);
worldPos = QVector3D(w.x(), w.y(), groundZ); worldPos = QVector3D(w.x(), w.y(), groundZ);
} }
bool Unit::isRecharging() const {
// Only military units have recharge/cooldown
if (!config.canAttack()) return false;
return rechargeTimer > 0.0f;
}
bool Unit::canAttack(const Unit &other) const
{
if (other.playerId == playerId) return false;
if (other.isDead() || isDead()) return false;
if (!config.canAttack()) return false;
// Check if unit is currently on cooldown/recharge
if (isRecharging()) {
return false;
}
int dist = Pathfinder::hexDistance(tileX, tileY, other.tileX, other.tileY);
return dist <= config.attackRange;
}
void Unit::performAttack(Unit &target)
{
if (!canAttack(target)) return;
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);
// Retaliation if target is adjacent melee military unit
if (!target.isDead() && target.config.isMilitary() && !config.isRanged()) {
int dist = Pathfinder::hexDistance(tileX, tileY, target.tileX, target.tileY);
if (dist <= target.config.attackRange) {
takeDamage(target.config.attackDamage * 0.5f); // 50% counterattack damage
}
}
}
void Unit::takeDamage(float damage)
{
health = std::max(0.0f, health - 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<TerrainPoint> *mapPoints, int numPoints05) float Unit::getTerrainHeight(float worldX, float worldY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{ {
if (!mapPoints || numPoints05 <= 0) return 1.0f; if (!mapPoints || numPoints05 <= 0) return 1.0f;
@@ -195,10 +242,18 @@ void Unit::cancelBuilding()
void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier, void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier,
Matrix2D<TerrainPoint> *mapPoints, int numPoints05) Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
{ {
// Update attack flash timer (visual feedback)
if (attackFlashTimer > 0.0f) { if (attackFlashTimer > 0.0f) {
attackFlashTimer = std::max(0.0f, attackFlashTimer - deltaTime); attackFlashTimer = std::max(0.0f, attackFlashTimer - deltaTime);
} }
// Update recharge timer (cooldown between attacks)
// Recharge decays based on simulation ticks, not real-time multiplier
if (rechargeTimer > 0.0f) {
float tickDurationSec = deltaTime / movement_tick_duration;
rechargeTimer = std::max(0.0f, rechargeTimer - tickDurationSec);
}
if (state == UnitState::Building) { if (state == UnitState::Building) {
if (buildDuration > 0.001f) { if (buildDuration > 0.001f) {
buildProgress += (deltaTime * simSpeedMultiplier) / buildDuration; buildProgress += (deltaTime * simSpeedMultiplier) / buildDuration;
@@ -260,35 +315,3 @@ void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier,
worldPos = QVector3D(curX, curY, curZ); worldPos = QVector3D(curX, curY, curZ);
} }
bool Unit::canAttack(const Unit &other) const
{
if (other.playerId == playerId) return false;
if (other.isDead() || isDead()) return false;
if (!config.canAttack()) return false;
int dist = Pathfinder::hexDistance(tileX, tileY, other.tileX, other.tileY);
return dist <= config.attackRange;
}
void Unit::performAttack(Unit &target)
{
if (!canAttack(target)) return;
attackFlashTimer = 0.5f; // Visual flash duration
target.takeDamage((float)config.attackDamage);
// Retaliation if target is adjacent melee military unit
if (!target.isDead() && target.config.isMilitary() && !config.isRanged()) {
int dist = Pathfinder::hexDistance(tileX, tileY, target.tileX, target.tileY);
if (dist <= target.config.attackRange) {
takeDamage(target.config.attackDamage * 0.5f); // 50% counterattack damage
}
}
}
void Unit::takeDamage(float damage)
{
health = std::max(0.0f, health - damage);
attackFlashTimer = 0.4f;
}
+6 -1
View File
@@ -56,6 +56,9 @@ public:
float health; float health;
float maxHealth; float maxHealth;
int buildCharges; int buildCharges;
// Simulation parameters (set during unit creation or via GameManager)
float movement_tick_duration; // Seconds per simulation tick
UnitState state; UnitState state;
// Movement Path // Movement Path
@@ -106,7 +109,9 @@ public:
QString getQueuedActionSummary() const; QString getQueuedActionSummary() const;
// Attack cooldown / flash // Attack cooldown / flash
float attackFlashTimer; float attackFlashTimer; // Flash effect timer (visual feedback after attack or taking damage)
float rechargeTimer; // Recharge/cooldown timer (time remaining until unit can attack again)
bool isRecharging() const; // Returns true if unit is currently on cooldown
// Building State // Building State
float buildProgress; // 0.0 to 1.0 float buildProgress; // 0.0 to 1.0
+2 -2
View File
@@ -82,11 +82,11 @@ bool UnitConfigManager::loadConfigFile(const QString &filePath)
if (root.hasChild(key)) { if (root.hasChild(key)) {
const YamlNode &sNode = root[key]; const YamlNode &sNode = root[key];
for (auto it = sNode.children.begin(); it != sNode.children.end(); ++it) { 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"); parseActionMap("action_durations");
m_configs[cfg.id] = cfg; m_configs[cfg.id] = cfg;
+11 -4
View File
@@ -48,15 +48,22 @@ struct UnitConfig {
QString obsoleteTech; QString obsoleteTech;
QString iconPath; QString iconPath;
QStringList actions; QStringList actions;
QMap<QString, float> actionSpeeds; // Duration in seconds per action (e.g. "boerderij_bouwen": 10.0f) QMap<QString, float> actionSpeeds; // Duration in simulation ticks per action (e.g. "boerderij_bouwen": 2.0f)
QStringList impassableTerrains; QStringList impassableTerrains;
QStringList passableTerrains; QStringList passableTerrains;
// 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() UnitConfig()
: id(""), name(""), category(UnitCategory::Builder), : id(""), name(""), category(UnitCategory::Builder),
movementDomain(MovementDomain::Land), health(100), movementDomain(MovementDomain::Land), health(100),
movementSpeed(2.0f), cost(10), laborCost(1000), attackDamage(0), attackRange(1), movementSpeed(2.0f), cost(10), laborCost(1000), attackDamage(0), attackRange(1),
buildCharges(-1), infiniteCharges(true), requiredTech(""), obsoleteTech(""), iconPath("") {} buildCharges(-1), infiniteCharges(true), requiredTech(""), obsoleteTech(""), iconPath(""),
rechargeTicks(2), // Default recharge: 2 simulation ticks
attackFlashTicks(1) // Attack flash effect: 1 tick
{}
bool isRanged() const { return attackRange > 1; } bool isRanged() const { return attackRange > 1; }
bool isMilitary() const { return category == UnitCategory::Military; } bool isMilitary() const { return category == UnitCategory::Military; }
@@ -68,11 +75,11 @@ struct UnitConfig {
bool canBuildRoad() const { return actions.contains("build_road") || actions.contains("weg_bouwen"); } bool canBuildRoad() const { return actions.contains("build_road") || actions.contains("weg_bouwen"); }
bool isBuilder() const { return canBuildFarm() || canBuildRoad(); } 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)) { if (actionSpeeds.contains(actionName)) {
return actionSpeeds.value(actionName); return actionSpeeds.value(actionName);
} }
return fallback; return fallbackTicks;
} }
}; };
+6 -7
View File
@@ -9,17 +9,15 @@ struct VertexPN {
}; };
UnitRenderer::UnitRenderer() UnitRenderer::UnitRenderer()
: m_program(nullptr), m_vertexCount(0), m_initialized(false), m_roadBuffersInitialized(false) : m_program(nullptr), m_vertexCount(0), m_initialized(false),
m_roadVao(), m_roadVbo(), m_roadBuffersInitialized(false)
{ {
} }
UnitRenderer::~UnitRenderer() UnitRenderer::~UnitRenderer()
{ {
if (m_program) delete m_program; if (m_program) delete m_program;
if (m_roadBuffersInitialized) { m_vao.destroy();
m_roadVao.destroy();
m_roadVbo.destroy();
}
} }
void UnitRenderer::initialize() void UnitRenderer::initialize()
@@ -56,7 +54,6 @@ void UnitRenderer::buildCylinderMesh()
float c2 = std::cos(a2), s2 = std::sin(a2); float c2 = std::cos(a2), s2 = std::sin(a2);
// --- Side Quad (2 Triangles) --- // --- Side Quad (2 Triangles) ---
// Tri 1
vertices.push_back({c1, s1, 0.0f, c1, s1, 0.0f}); vertices.push_back({c1, s1, 0.0f, c1, s1, 0.0f});
vertices.push_back({c2, s2, 0.0f, c2, s2, 0.0f}); vertices.push_back({c2, s2, 0.0f, c2, s2, 0.0f});
vertices.push_back({c2, s2, 1.0f, c2, s2, 0.0f}); vertices.push_back({c2, s2, 1.0f, c2, s2, 0.0f});
@@ -96,6 +93,8 @@ void UnitRenderer::buildCylinderMesh()
m_vbo.release(); m_vbo.release();
} }
void UnitRenderer::renderRoads( void UnitRenderer::renderRoads(
const std::map<std::pair<int, int>, TileRoad> &roads, const std::map<std::pair<int, int>, TileRoad> &roads,
const RoadConfigManager &roadConfigs, const RoadConfigManager &roadConfigs,
@@ -368,7 +367,7 @@ void UnitRenderer::renderUnits(
const Player *player = players.getPlayer(unit->playerId); const Player *player = players.getPlayer(unit->playerId);
QColor col = player ? player->primaryColor : QColor(200, 200, 200); QColor col = player ? player->primaryColor : QColor(200, 200, 200);
// Flash white/red if attacked // Flash white/red if attacked or damaged
if (unit->attackFlashTimer > 0.0f) { if (unit->attackFlashTimer > 0.0f) {
float f = unit->attackFlashTimer / 0.5f; float f = unit->attackFlashTimer / 0.5f;
col = QColor( col = QColor(
+6
View File
@@ -60,15 +60,21 @@ public:
private: private:
void buildCylinderMesh(); void buildCylinderMesh();
// Main unit cylinder mesh
QOpenGLShaderProgram *m_program; QOpenGLShaderProgram *m_program;
QOpenGLBuffer m_vbo; QOpenGLBuffer m_vbo;
QOpenGLVertexArrayObject m_vao; QOpenGLVertexArrayObject m_vao;
int m_vertexCount; int m_vertexCount;
bool m_initialized; bool m_initialized;
// Road rendering buffers (existing functionality)
QOpenGLBuffer m_roadVbo; QOpenGLBuffer m_roadVbo;
QOpenGLVertexArrayObject m_roadVao; QOpenGLVertexArrayObject m_roadVao;
bool m_roadBuffersInitialized; bool m_roadBuffersInitialized;
}; };
#endif // UNITRENDERER_H #endif // UNITRENDERER_H