Compare commits
4
Commits
ea0eedc0fb
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9bb91eef4 | ||
|
|
8c24b112d4 | ||
|
|
2ef8e857e0 | ||
|
|
772d4494e8 |
@@ -5,3 +5,4 @@ Makefile*
|
||||
tiletest
|
||||
terrain_test2
|
||||
.qmake.stash
|
||||
tiletest
|
||||
|
||||
@@ -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.
|
||||
> - 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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
- **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 };
|
||||
@@ -153,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<std::pair<int, int>> 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);
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
# Classification thresholds
|
||||
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
|
||||
snow_height_km: 3.1 # Altitude in km >= 3.1 is high peak snow
|
||||
polar_snow_line_enabled: true # Whether latitude reduces snowline altitude
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 2
|
||||
required_tech: "archery"
|
||||
obsolete_tech: "machinery"
|
||||
icon: images/units/slinger.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -15,6 +15,8 @@ actions:
|
||||
- verplaatsen
|
||||
- boerderij_bouwen
|
||||
- build_road
|
||||
action_speeds:
|
||||
boerderij_bouwen: 10.0
|
||||
build_road: 2
|
||||
recharge_ticks: 0
|
||||
attack_flash_ticks: 0
|
||||
action_durations:
|
||||
boerderij_bouwen: 2.0
|
||||
build_road: 2.0
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 2
|
||||
required_tech: "machinery"
|
||||
obsolete_tech: ""
|
||||
icon: images/units/slinger.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 1
|
||||
required_tech: "sailing"
|
||||
obsolete_tech: ""
|
||||
icon: images/units/galley.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 1
|
||||
required_tech: "horseback_riding"
|
||||
obsolete_tech: "stirrups"
|
||||
icon: images/units/warrior.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 1
|
||||
required_tech: "stirrups"
|
||||
obsolete_tech: ""
|
||||
icon: images/units/warrior.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 1
|
||||
required_tech: "gunpowder"
|
||||
obsolete_tech: ""
|
||||
icon: images/units/warrior.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -9,6 +9,8 @@ labor_cost: 2000
|
||||
required_tech: ""
|
||||
obsolete_tech: ""
|
||||
icon: images/units/settler.png
|
||||
recharge_ticks: 0
|
||||
attack_flash_ticks: 0
|
||||
actions:
|
||||
- verplaatsen
|
||||
- steden_bouwen
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 2
|
||||
required_tech: ""
|
||||
obsolete_tech: "archery"
|
||||
icon: images/units/slinger.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 1
|
||||
required_tech: "bronze_working"
|
||||
obsolete_tech: "gunpowder"
|
||||
icon: images/units/warrior.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 1
|
||||
required_tech: "iron_working"
|
||||
obsolete_tech: "gunpowder"
|
||||
icon: images/units/warrior.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,8 @@ attack_range: 1
|
||||
required_tech: ""
|
||||
obsolete_tech: "iron_working"
|
||||
icon: images/units/warrior.png
|
||||
recharge_ticks: 2
|
||||
attack_flash_ticks: 1
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -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
@@ -1265,9 +1265,10 @@ void GameManager::continueCurrentUnitAction(Unit &unit, Matrix2D<TerrainPoint> *
|
||||
}
|
||||
|
||||
// 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<TerrainPoint> *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;
|
||||
}
|
||||
|
||||
+13
-2
@@ -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}")
|
||||
|
||||
# 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"))
|
||||
R = 8.0
|
||||
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))))
|
||||
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]
|
||||
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
|
||||
elif h > 182 or (dist_pole > 0.80 and h > 20):
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -769,6 +769,19 @@ void QSceneRenderer::drawUnitsOverlay(QPainter &painter, const QMatrix4x4 &vMatr
|
||||
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.get() == selectedUnit) {
|
||||
float pulse = (std::sin(m_elapsedTimer.elapsed() * 0.008f) + 1.0f) * 0.5f;
|
||||
|
||||
+13
-4
@@ -30,6 +30,8 @@ uniform vec3 roadBorderColor;
|
||||
|
||||
uniform float time;
|
||||
uniform int riverMask;
|
||||
uniform float waterLevelKm = 0.08235294;
|
||||
uniform float coastBeachWidthKm = 0.022;
|
||||
|
||||
smooth in vec3 vNormal;
|
||||
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!
|
||||
float snowLine = mix(2.5, 0.0, poleFactor);
|
||||
|
||||
// 3. Water vs Land detection (z_pos = height in kilometers, 0.0 = sea level)
|
||||
float oceanWaterFac = 1.0 - smoothstep(0.005, 0.025, z_pos);
|
||||
// 3. Water vs Land detection (z_pos = height in kilometers; water is height < water_height_threshold)
|
||||
// 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
|
||||
float riverDist = getRiverDistance(fragVert.xy, riverMask);
|
||||
@@ -321,10 +327,13 @@ void main() {
|
||||
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
|
||||
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 beachFac = max(coastBeach, riverBeach);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
TerrainMovementConfig::TerrainMovementConfig()
|
||||
: waterHeightThreshold(6),
|
||||
coastBeachWidthKm(0.022f),
|
||||
mountainRockHeightKm(2.3f),
|
||||
snowHeightKm(3.1f),
|
||||
polarSnowLineEnabled(true),
|
||||
@@ -61,6 +62,9 @@ bool TerrainMovementManager::loadConfig(const QString &filePath)
|
||||
if (root.hasChild("water_height_threshold")) {
|
||||
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")) {
|
||||
m_config.mountainRockHeightKm = root["mountain_rock_height_km"].asFloat(2.3f);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ struct DomainMovementRule {
|
||||
|
||||
struct TerrainMovementConfig {
|
||||
int waterHeightThreshold;
|
||||
float coastBeachWidthKm;
|
||||
float mountainRockHeightKm;
|
||||
float snowHeightKm;
|
||||
bool polarSnowLineEnabled;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <math.h>
|
||||
#include "qscenerenderer.h"
|
||||
#include "gamemanager.h"
|
||||
#include "terrainmovementmanager.h"
|
||||
#include <QVector3D>
|
||||
QImage * Tile::m_image_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);
|
||||
}
|
||||
|
||||
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("riverMask", (int)m_riverMask);
|
||||
m_object.bind();
|
||||
|
||||
@@ -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),
|
||||
buildCharges(cfg.buildCharges),
|
||||
state(UnitState::Idle), pathIndex(0), moveProgress(0.0f),
|
||||
targetTileX(startX), targetTileY(startY), attackFlashTimer(0.0f),
|
||||
buildProgress(0.0f), buildDuration(0.0f), buildingImprovementId("")
|
||||
targetTileX(startX), targetTileY(startY), attackFlashTimer(0.0f), rechargeTimer(0.0f),
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!mapPoints || numPoints05 <= 0) return 1.0f;
|
||||
@@ -195,10 +242,18 @@ void Unit::cancelBuilding()
|
||||
void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier,
|
||||
Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
// Update attack flash timer (visual feedback)
|
||||
if (attackFlashTimer > 0.0f) {
|
||||
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 (buildDuration > 0.001f) {
|
||||
buildProgress += (deltaTime * simSpeedMultiplier) / buildDuration;
|
||||
@@ -259,36 +314,4 @@ void Unit::update(float deltaTime, float tickDuration, float simSpeedMultiplier,
|
||||
float curZ = getTerrainHeight(curX, curY, mapPoints, numPoints05);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -106,7 +109,9 @@ public:
|
||||
QString getQueuedActionSummary() const;
|
||||
|
||||
// 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
|
||||
float buildProgress; // 0.0 to 1.0
|
||||
@@ -133,4 +138,4 @@ public:
|
||||
static float getTerrainHeight(float worldX, float worldY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
};
|
||||
|
||||
#endif // UNIT_H
|
||||
#endif // UNIT_H
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+12
-5
@@ -48,15 +48,22 @@ struct UnitConfig {
|
||||
QString obsoleteTech;
|
||||
QString iconPath;
|
||||
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 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()
|
||||
: 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("") {}
|
||||
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 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 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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -89,4 +96,4 @@ private:
|
||||
QMap<QString, UnitConfig> m_configs;
|
||||
};
|
||||
|
||||
#endif // UNITCONFIG_H
|
||||
#endif // UNITCONFIG_H
|
||||
+7
-8
@@ -9,17 +9,15 @@ struct VertexPN {
|
||||
};
|
||||
|
||||
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()
|
||||
{
|
||||
if (m_program) delete m_program;
|
||||
if (m_roadBuffersInitialized) {
|
||||
m_roadVao.destroy();
|
||||
m_roadVbo.destroy();
|
||||
}
|
||||
m_vao.destroy();
|
||||
}
|
||||
|
||||
void UnitRenderer::initialize()
|
||||
@@ -56,7 +54,6 @@ void UnitRenderer::buildCylinderMesh()
|
||||
float c2 = std::cos(a2), s2 = std::sin(a2);
|
||||
|
||||
// --- Side Quad (2 Triangles) ---
|
||||
// Tri 1
|
||||
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, 1.0f, c2, s2, 0.0f});
|
||||
@@ -96,6 +93,8 @@ void UnitRenderer::buildCylinderMesh()
|
||||
m_vbo.release();
|
||||
}
|
||||
|
||||
|
||||
|
||||
void UnitRenderer::renderRoads(
|
||||
const std::map<std::pair<int, int>, TileRoad> &roads,
|
||||
const RoadConfigManager &roadConfigs,
|
||||
@@ -368,7 +367,7 @@ void UnitRenderer::renderUnits(
|
||||
const Player *player = players.getPlayer(unit->playerId);
|
||||
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) {
|
||||
float f = unit->attackFlashTimer / 0.5f;
|
||||
col = QColor(
|
||||
@@ -428,4 +427,4 @@ QPointF UnitRenderer::projectToScreen(
|
||||
float sy = (1.0f - (ndc.y() + 1.0f) * 0.5f) * viewportH;
|
||||
|
||||
return QPointF(sx, sy);
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -60,15 +60,21 @@ public:
|
||||
private:
|
||||
void buildCylinderMesh();
|
||||
|
||||
// Main unit cylinder mesh
|
||||
QOpenGLShaderProgram *m_program;
|
||||
QOpenGLBuffer m_vbo;
|
||||
QOpenGLVertexArrayObject m_vao;
|
||||
int m_vertexCount;
|
||||
bool m_initialized;
|
||||
|
||||
// Road rendering buffers (existing functionality)
|
||||
QOpenGLBuffer m_roadVbo;
|
||||
QOpenGLVertexArrayObject m_roadVao;
|
||||
bool m_roadBuffersInitialized;
|
||||
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
#endif // UNITRENDERER_H
|
||||
#endif // UNITRENDERER_H
|
||||
Reference in New Issue
Block a user