add recharge
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,9 @@ attack_range: 2
|
||||
required_tech: "archery"
|
||||
obsolete_tech: "machinery"
|
||||
icon: images/units/slinger.png
|
||||
recharge_time: 500
|
||||
recharge_duration: 300
|
||||
attack_flash_duration: 400
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -15,6 +15,9 @@ actions:
|
||||
- verplaatsen
|
||||
- boerderij_bouwen
|
||||
- build_road
|
||||
recharge_time: 500
|
||||
recharge_duration: 300
|
||||
attack_flash_duration: 400
|
||||
action_speeds:
|
||||
boerderij_bouwen: 10.0
|
||||
build_road: 2
|
||||
|
||||
@@ -11,6 +11,9 @@ attack_range: 2
|
||||
required_tech: "machinery"
|
||||
obsolete_tech: ""
|
||||
icon: images/units/slinger.png
|
||||
recharge_time: 500
|
||||
recharge_duration: 300
|
||||
attack_flash_duration: 400
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,9 @@ attack_range: 1
|
||||
required_tech: "sailing"
|
||||
obsolete_tech: ""
|
||||
icon: images/units/galley.png
|
||||
recharge_time: 500
|
||||
recharge_duration: 300
|
||||
attack_flash_duration: 400
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,9 @@ 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
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,9 @@ attack_range: 1
|
||||
required_tech: "stirrups"
|
||||
obsolete_tech: ""
|
||||
icon: images/units/warrior.png
|
||||
recharge_time: 500
|
||||
recharge_duration: 300
|
||||
attack_flash_duration: 400
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,9 @@ attack_range: 1
|
||||
required_tech: "gunpowder"
|
||||
obsolete_tech: ""
|
||||
icon: images/units/warrior.png
|
||||
recharge_time: 500
|
||||
recharge_duration: 300
|
||||
attack_flash_duration: 400
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -9,6 +9,9 @@ labor_cost: 2000
|
||||
required_tech: ""
|
||||
obsolete_tech: ""
|
||||
icon: images/units/settler.png
|
||||
recharge_time: 500
|
||||
recharge_duration: 300
|
||||
attack_flash_duration: 400
|
||||
actions:
|
||||
- verplaatsen
|
||||
- steden_bouwen
|
||||
|
||||
@@ -11,6 +11,9 @@ attack_range: 2
|
||||
required_tech: ""
|
||||
obsolete_tech: "archery"
|
||||
icon: images/units/slinger.png
|
||||
recharge_time: 500
|
||||
recharge_duration: 300
|
||||
attack_flash_duration: 400
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,9 @@ 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
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,9 @@ 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
|
||||
actions:
|
||||
- verplaatsen
|
||||
- aanvallen
|
||||
|
||||
@@ -11,6 +11,9 @@ attack_range: 1
|
||||
required_tech: ""
|
||||
obsolete_tech: "iron_working"
|
||||
icon: images/units/warrior.png
|
||||
recharge_time: 500
|
||||
recharge_duration: 300
|
||||
attack_flash_duration: 400
|
||||
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*
|
||||
+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->config.canAttack() && unit->isRecharging()) {
|
||||
float rechargePercent = std::max(0.0f, std::min(1.0f, unit->rechargeTimer / unit->config.rechargeTime));
|
||||
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,59 @@ 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),
|
||||
targetTileX(startX), targetTileY(startY), attackFlashTimer(0.0f), rechargeTimer(0.0f),
|
||||
buildProgress(0.0f), buildDuration(0.0f), buildingImprovementId("")
|
||||
{
|
||||
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.attackFlashDuration / 1000.0f; // Convert ms to seconds
|
||||
rechargeTimer = config.rechargeTime / 1000.0f; // Convert ms 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 = 0.4f;
|
||||
rechargeTimer = config.rechargeTime / 1000.0f; // Reset recharge on taking damage
|
||||
}
|
||||
|
||||
float Unit::getTerrainHeight(float worldX, float worldY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05)
|
||||
{
|
||||
if (!mapPoints || numPoints05 <= 0) return 1.0f;
|
||||
@@ -195,10 +241,16 @@ 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)
|
||||
if (rechargeTimer > 0.0f) {
|
||||
rechargeTimer = std::max(0.0f, rechargeTimer - deltaTime * simSpeedMultiplier);
|
||||
}
|
||||
|
||||
if (state == UnitState::Building) {
|
||||
if (buildDuration > 0.001f) {
|
||||
buildProgress += (deltaTime * simSpeedMultiplier) / buildDuration;
|
||||
@@ -259,36 +311,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;
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,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 +135,4 @@ public:
|
||||
static float getTerrainHeight(float worldX, float worldY, Matrix2D<TerrainPoint> *mapPoints, int numPoints05);
|
||||
};
|
||||
|
||||
#endif // UNIT_H
|
||||
#endif // UNIT_H
|
||||
+11
-2
@@ -52,11 +52,20 @@ struct UnitConfig {
|
||||
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)
|
||||
|
||||
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(""),
|
||||
rechargeTime(500), // Default recharge time: 500ms (0.5 seconds)
|
||||
rechargeDuration(300), // Visual cooldown bar duration: 300ms
|
||||
attackFlashDuration(400) // Attack flash effect duration: 400ms
|
||||
{}
|
||||
|
||||
bool isRanged() const { return attackRange > 1; }
|
||||
bool isMilitary() const { return category == UnitCategory::Military; }
|
||||
@@ -89,4 +98,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