modified: content/2025-07-13-building-and-modding-multimower-with-dry-engine.rst
This commit is contained in:
parent
870ef65ff9
commit
48e94f60b7
1 changed files with 212 additions and 142 deletions
|
|
@ -288,15 +288,12 @@ Part 1: Tank Movement Controls with Realistic Acceleration
|
|||
|
||||
.. code-block:: cpp
|
||||
|
||||
class Mower : public Controllable
|
||||
{
|
||||
// Add these private members after existing variables:
|
||||
// Add these private members after existing variables:
|
||||
private:
|
||||
// Acceleration system
|
||||
float currentSpeed_;
|
||||
float maxSpeed_;
|
||||
float acceleration_;
|
||||
};
|
||||
|
||||
**Step 2**: Initialize acceleration in ``src/mower.cpp`` constructor:
|
||||
|
||||
|
|
@ -304,9 +301,10 @@ Part 1: Tank Movement Controls with Realistic Acceleration
|
|||
|
||||
Mower::Mower(Context* context): Controllable(context),
|
||||
// existing initializers...
|
||||
lastFiredRight_{ false },
|
||||
currentSpeed_{ 0.0f },
|
||||
maxSpeed_{ 60.0f }, // 60 units/sec top speed
|
||||
acceleration_{ 7.5f } // 0-60 in 8 seconds
|
||||
acceleration_{ 7.5f } // 0-60 in 8 seconds
|
||||
{
|
||||
}
|
||||
|
||||
|
|
@ -323,9 +321,6 @@ Part 1: Tank Movement Controls with Realistic Acceleration
|
|||
|
||||
void Mower::HandleInput(float timeStep)
|
||||
{
|
||||
// Don't handle input if destroyed
|
||||
if (isDestroyed_) return;
|
||||
|
||||
RigidBody* body = node_->GetComponent<RigidBody>();
|
||||
if (!body) return;
|
||||
|
||||
|
|
@ -385,6 +380,8 @@ Part 1: Tank Movement Controls with Realistic Acceleration
|
|||
}
|
||||
}
|
||||
|
||||
**Note**: This is the initial version from Part 1. The health system checks (``isDestroyed_``) will be added in Part 2.
|
||||
|
||||
**Step 5**: Update the call site in ``Mower::Update()``:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
|
@ -405,15 +402,12 @@ Part 2: Health and Damage System with Random Damage
|
|||
|
||||
.. code-block:: cpp
|
||||
|
||||
class Mower : public Controllable
|
||||
{
|
||||
// Add these private members:
|
||||
// Add these private members:
|
||||
private:
|
||||
// Health system
|
||||
float health_;
|
||||
float maxHealth_;
|
||||
bool isDestroyed_;
|
||||
};
|
||||
|
||||
**Step 2**: Initialize health in ``src/mower.cpp`` constructor:
|
||||
|
||||
|
|
@ -421,6 +415,7 @@ Part 2: Health and Damage System with Random Damage
|
|||
|
||||
Mower::Mower(Context* context): Controllable(context),
|
||||
// existing initializers...
|
||||
acceleration_{ 7.5f }, // 0-60 in 8 seconds
|
||||
health_{ 100.0f },
|
||||
maxHealth_{ 100.0f },
|
||||
isDestroyed_{ false }
|
||||
|
|
@ -453,6 +448,31 @@ Part 2: Health and Damage System with Random Damage
|
|||
}
|
||||
}
|
||||
|
||||
void Mower::DestroyMower()
|
||||
{
|
||||
if (isDestroyed_) return;
|
||||
|
||||
isDestroyed_ = true;
|
||||
health_ = 0.0f;
|
||||
currentSpeed_ = 0.0f; // Stop acceleration
|
||||
|
||||
DRY_LOGINFO("Mower destroyed!");
|
||||
|
||||
// Disable physics so it stops moving
|
||||
RigidBody* body = node_->GetComponent<RigidBody>();
|
||||
if (body)
|
||||
{
|
||||
body->SetEnabled(false);
|
||||
}
|
||||
|
||||
// Hide the mower model
|
||||
AnimatedModel* model = node_->GetComponent<AnimatedModel>();
|
||||
if (model)
|
||||
{
|
||||
model->SetEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
float Mower::GetHealthPercentage() const
|
||||
{
|
||||
return health_ / maxHealth_;
|
||||
|
|
@ -462,63 +482,99 @@ Part 2: Health and Damage System with Random Damage
|
|||
|
||||
.. code-block:: cpp
|
||||
|
||||
// Check all mowers in the scene for bullet hits
|
||||
PODVector<Node*> mowerNodes;
|
||||
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
|
||||
|
||||
for (Node* mowerNode : mowerNodes)
|
||||
// Replace the bullet update loop with collision detection
|
||||
for (unsigned b{ 0 }; b < bullets_.Size(); )
|
||||
{
|
||||
Mower* otherMower = mowerNode->GetComponent<Mower>();
|
||||
Projectile& bullet{ bullets_.At(b)};
|
||||
|
||||
bullet.age_ += timeStep;
|
||||
Vector3 bulletPos = bullet.path_.Solve(bullet.age_);
|
||||
|
||||
if (otherMower && otherMower != this && !otherMower->isDestroyed_)
|
||||
// Check for hits on other mowers
|
||||
bool bulletHit = false;
|
||||
|
||||
// Check all mowers in the scene
|
||||
PODVector<Node*> mowerNodes;
|
||||
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
|
||||
|
||||
for (Node* mowerNode : mowerNodes)
|
||||
{
|
||||
float distance = (bulletPos - otherMower->node_->GetWorldPosition()).Length();
|
||||
if (distance < 5.0f) // Hit radius
|
||||
Mower* otherMower = mowerNode->GetComponent<Mower>();
|
||||
|
||||
if (otherMower && otherMower != this && !otherMower->isDestroyed_)
|
||||
{
|
||||
float damage = 1.0f + Random(4); // Random 1-4 damage
|
||||
DRY_LOGINFOF("Bullet hit! Distance: %f, Damage: %.0f", distance, damage);
|
||||
otherMower->TakeDamage(damage);
|
||||
bullets_.EraseSwap(b);
|
||||
bulletHit = true;
|
||||
break;
|
||||
float distance = (bulletPos - otherMower->node_->GetWorldPosition()).Length();
|
||||
if (distance < 5.0f) // Hit radius
|
||||
{
|
||||
float damage = 1.0f + Random(4); // Random 1-4 damage
|
||||
DRY_LOGINFOF("Bullet hit! Distance: %f, Damage: %.0f", distance, damage);
|
||||
otherMower->TakeDamage(damage);
|
||||
bullets_.EraseSwap(b);
|
||||
bulletHit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bulletHit && (bullet.age_ > 0.17f || bulletPos.y_ < .075f))
|
||||
{
|
||||
for (int s{ 0 }; s < 23; ++s)
|
||||
Sparkle(bulletPos, 5.f);
|
||||
bullets_.EraseSwap(b);
|
||||
}
|
||||
else if (!bulletHit)
|
||||
{
|
||||
++b;
|
||||
}
|
||||
}
|
||||
|
||||
**Step 6**: Add missile collision detection in the missile update loop:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// Check for missile hits on other mowers
|
||||
bool missileHit = false;
|
||||
PODVector<Node*> mowerNodes;
|
||||
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
|
||||
|
||||
for (Node* mowerNode : mowerNodes)
|
||||
// In the missile update loop, add collision detection
|
||||
if (missile.age_ < 4.f/3.f)
|
||||
{
|
||||
Mower* otherMower = mowerNode->GetComponent<Mower>();
|
||||
missile.age_ += timeStep;
|
||||
Sparkle(missile.path_.Solve(missile.age_), .1f);
|
||||
|
||||
// No self-damage for any mower - missiles only hit other mowers
|
||||
bool allowHit = otherMower && otherMower != this && !otherMower->isDestroyed_;
|
||||
Vector3 missilePos = missile.path_.Solve(missile.age_);
|
||||
|
||||
if (allowHit)
|
||||
// Check for missile hits on other mowers
|
||||
bool missileHit = false;
|
||||
PODVector<Node*> mowerNodes;
|
||||
GetScene()->GetChildrenWithComponent<Mower>(mowerNodes, true);
|
||||
|
||||
for (Node* mowerNode : mowerNodes)
|
||||
{
|
||||
float distance = (missilePos - otherMower->node_->GetWorldPosition()).Length();
|
||||
if (distance < 8.0f) // Larger explosion radius
|
||||
Mower* otherMower = mowerNode->GetComponent<Mower>();
|
||||
|
||||
if (otherMower && otherMower != this && !otherMower->isDestroyed_)
|
||||
{
|
||||
float damage = 10.0f + Random(16); // Random 10-25 damage
|
||||
DRY_LOGINFOF("Missile hit! Distance: %f, Damage: %.0f", distance, damage);
|
||||
otherMower->TakeDamage(damage);
|
||||
missileHit = true;
|
||||
break;
|
||||
float distance = (missilePos - otherMower->node_->GetWorldPosition()).Length();
|
||||
if (distance < 8.0f) // Larger explosion radius
|
||||
{
|
||||
float damage = 10.0f + Random(16); // Random 10-25 damage
|
||||
DRY_LOGINFOF("Missile hit! Distance: %f, Damage: %.0f", distance, damage);
|
||||
otherMower->TakeDamage(damage);
|
||||
missileHit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missileHit || missile.age_ > 4.f/3.f || missilePos.y_ < .25f)
|
||||
{
|
||||
// Create explosion effects and remove missile
|
||||
// ... existing explosion code ...
|
||||
}
|
||||
}
|
||||
|
||||
**Step 7**: Add health bar visualization in ``RenderDebug()`` method:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// Add to RenderDebug() method in mower.cpp
|
||||
// Render health bar above mower
|
||||
if (!isDestroyed_)
|
||||
{
|
||||
|
|
@ -542,6 +598,42 @@ Part 2: Health and Damage System with Random Damage
|
|||
- **Missiles**: Random 10-25 damage per hit (takes 4-10 missiles to destroy)
|
||||
- **Total mower health**: 100 HP
|
||||
|
||||
**Step 8**: Update ``HandleInput()`` to prevent firing when dead (modify the method from Part 1):
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
void Mower::HandleInput(float timeStep)
|
||||
{
|
||||
// Don't handle input if destroyed
|
||||
if (isDestroyed_) return;
|
||||
|
||||
// ... existing movement code from Part 1 ...
|
||||
|
||||
// Fire bullet (only if not destroyed)
|
||||
if (!isDestroyed_ && INPUT->GetMouseButtonDown(MOUSEB_LEFT) && sinceBullet >= bulletInterval)
|
||||
{
|
||||
bullets_.Push(Projectile{ gun_ });
|
||||
sinceBullet = 0.f;
|
||||
}
|
||||
|
||||
// Fire missile (only if not destroyed)
|
||||
if (!isDestroyed_ && INPUT->GetMouseButtonPress(MOUSEB_RIGHT) && sinceMissile >= missileInterval)
|
||||
{
|
||||
FireMissile();
|
||||
}
|
||||
}
|
||||
|
||||
**Step 9**: Update AI firing in ``Update()`` method:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
else
|
||||
{
|
||||
// AI mowers only fire if not destroyed
|
||||
if (!isDestroyed_ && Random(420) == 0)
|
||||
FireMissile();
|
||||
}
|
||||
|
||||
**Result**: Balanced combat with visible health bars and satisfying random damage!
|
||||
|
||||
Part 3: Death and Victory Effects
|
||||
|
|
@ -549,7 +641,35 @@ Part 3: Death and Victory Effects
|
|||
|
||||
**Goal**: Create a complete end-game experience with massive explosions, death camera effects, electrical sparking, and victory celebrations.
|
||||
|
||||
Replace the ``DestroyMower()`` method in ``src/mower.cpp``:
|
||||
**Step 1**: Add new variables to ``src/mower.h``:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// Add these private members:
|
||||
private:
|
||||
// Death camera
|
||||
float deathCameraDistance_;
|
||||
|
||||
// Continuous sparking effect for destroyed mowers
|
||||
float sparkTimer_;
|
||||
|
||||
// Victory flag to prevent multiple celebrations
|
||||
bool victoryTriggered_;
|
||||
|
||||
**Step 2**: Initialize new variables in ``src/mower.cpp`` constructor:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
Mower::Mower(Context* context): Controllable(context),
|
||||
// existing initializers...
|
||||
isDestroyed_{ false },
|
||||
deathCameraDistance_{ 10.0f }, // Initial camera distance on death
|
||||
sparkTimer_{ 0.0f }, // Timer for continuous sparking
|
||||
victoryTriggered_{ false } // Flag to prevent multiple victory celebrations
|
||||
{
|
||||
}
|
||||
|
||||
**Step 3**: Replace the ``DestroyMower()`` method in ``src/mower.cpp``:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
|
|
@ -609,11 +729,21 @@ Replace the ``DestroyMower()`` method in ``src/mower.cpp``:
|
|||
body->SetEnabled(false);
|
||||
}
|
||||
|
||||
// Simple visual change - just disable the model instead of trying to clone material
|
||||
// Transform into a grayscale wreck instead of hiding
|
||||
AnimatedModel* model = node_->GetComponent<AnimatedModel>();
|
||||
if (model)
|
||||
{
|
||||
model->SetEnabled(false);
|
||||
// Create grayscale material to show battle damage
|
||||
SharedPtr<Material> grayscaleMaterial = model->GetMaterial(0)->Clone();
|
||||
|
||||
// Convert to grayscale - desaturate while keeping original brightness
|
||||
grayscaleMaterial->SetShaderParameter("MatDiffColor", Color(0.4f, 0.4f, 0.4f)); // Grayscale
|
||||
grayscaleMaterial->SetShaderParameter("MatSpecColor", Color(0.1f, 0.1f, 0.1f)); // Dull reflection
|
||||
grayscaleMaterial->SetShaderParameter("MatEmissiveColor", Color(0.0f, 0.0f, 0.0f)); // No glow
|
||||
|
||||
model->SetMaterial(grayscaleMaterial);
|
||||
|
||||
DRY_LOGINFO("Applied grayscale material to destroyed mower");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -625,81 +755,14 @@ Replace the ``DestroyMower()`` method in ``src/mower.cpp``:
|
|||
- **Louder explosion sound** (1.5x volume)
|
||||
- **Varied particle speeds** and positions for realism
|
||||
|
||||
**Step 2**: Add tracking variables to ``src/mower.h``:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// Death camera
|
||||
float deathCameraDistance_;
|
||||
|
||||
// Continuous sparking effect for destroyed mowers
|
||||
float sparkTimer_;
|
||||
|
||||
**Step 2**: Initialize in constructor in ``src/mower.cpp``:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
Mower::Mower(Context* context): Controllable(context),
|
||||
// existing initializers...
|
||||
deathCameraDistance_{ 10.0f }, // Initial camera distance on death
|
||||
sparkTimer_{ 0.0f } // Timer for continuous sparking
|
||||
|
||||
**Step 3**: Prevent firing when dead in ``HandleInput()`` and AI update:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// Fire bullet (only if not destroyed)
|
||||
if (!isDestroyed_ && INPUT->GetMouseButtonDown(MOUSEB_LEFT) && sinceBullet >= bulletInterval)
|
||||
{
|
||||
bullets_.Push(Projectile{ gun_ });
|
||||
sinceBullet = 0.f;
|
||||
}
|
||||
|
||||
// Fire missile (only if not destroyed)
|
||||
if (!isDestroyed_ && INPUT->GetMouseButtonPress(MOUSEB_RIGHT) && sinceMissile >= missileInterval)
|
||||
{
|
||||
FireMissile();
|
||||
}
|
||||
|
||||
And for AI mowers:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
else
|
||||
{
|
||||
// AI mowers only fire if not destroyed
|
||||
if (!isDestroyed_ && Random(420) == 0)
|
||||
FireMissile();
|
||||
}
|
||||
|
||||
**Step 4**: Replace the model hiding code in ``DestroyMower()``:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// Transform into a grayscale wreck instead of hiding
|
||||
AnimatedModel* model = node_->GetComponent<AnimatedModel>();
|
||||
if (model)
|
||||
{
|
||||
// Create grayscale material to show battle damage
|
||||
SharedPtr<Material> grayscaleMaterial = model->GetMaterial(0)->Clone();
|
||||
|
||||
// Convert to grayscale - desaturate while keeping original brightness
|
||||
grayscaleMaterial->SetShaderParameter("MatDiffColor", Color(0.4f, 0.4f, 0.4f)); // Grayscale
|
||||
grayscaleMaterial->SetShaderParameter("MatSpecColor", Color(0.1f, 0.1f, 0.1f)); // Dull reflection
|
||||
grayscaleMaterial->SetShaderParameter("MatEmissiveColor", Color(0.0f, 0.0f, 0.0f)); // No glow
|
||||
|
||||
model->SetMaterial(grayscaleMaterial);
|
||||
|
||||
DRY_LOGINFO("Applied grayscale material to destroyed mower");
|
||||
}
|
||||
|
||||
**Step 5**: Add continuous sparking in ``Update()`` method:
|
||||
**Step 4**: Add continuous sparking in ``Update()`` method:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
// Add to Update() method after existing timer updates
|
||||
// Update spark timer for destroyed mowers
|
||||
sparkTimer_ += timeStep;
|
||||
|
||||
|
||||
// Continuous sparking effect for destroyed mowers
|
||||
if (isDestroyed_ && sparkTimer_ >= 0.1f) // Spark every 0.1 seconds
|
||||
{
|
||||
|
|
@ -724,7 +787,8 @@ And for AI mowers:
|
|||
}
|
||||
}
|
||||
|
||||
**Step 6**: Add dynamic camera zoom-out in ``PostUpdate()`` when player is destroyed:
|
||||
|
||||
**Step 4**: Add dynamic camera zoom-out in ``PostUpdate()`` method:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
|
|
@ -735,8 +799,8 @@ And for AI mowers:
|
|||
|
||||
Jib* jib{ GetPlayer()->GetJib() };
|
||||
|
||||
// Handle death camera zoom
|
||||
if (isDestroyed_ && jib && jib->GetCamera())
|
||||
// Handle death camera zoom (both for death and victory)
|
||||
if ((isDestroyed_ || victoryTriggered_) && jib && jib->GetCamera())
|
||||
{
|
||||
// Dynamic zoom speed - fast initially, then slow to a halt
|
||||
float maxDistance = 50.0f; // Maximum zoom distance
|
||||
|
|
@ -747,16 +811,16 @@ And for AI mowers:
|
|||
deathCameraDistance_ += 60.0f * timeStep * speedFactor;
|
||||
deathCameraDistance_ = Min(deathCameraDistance_, maxDistance); // Cap at max distance
|
||||
|
||||
// Set camera to orbit destroyed mower
|
||||
// Set camera to orbit the mower (destroyed or victorious)
|
||||
Node* cameraNode = jib->GetCamera()->GetNode();
|
||||
Vector3 offset = Vector3::BACK * deathCameraDistance_ + Vector3::UP * (deathCameraDistance_ * 0.4f);
|
||||
cameraNode->SetPosition(node_->GetWorldPosition() + offset);
|
||||
cameraNode->LookAt(node_->GetWorldPosition(), Vector3::UP);
|
||||
|
||||
return; // Skip normal camera update when dead
|
||||
return; // Skip normal camera update when dead or victorious
|
||||
}
|
||||
|
||||
// Existing PostUpdate code continues here...
|
||||
// ... existing PostUpdate code continues here ...
|
||||
}
|
||||
|
||||
**Visual Effects**:
|
||||
|
|
@ -770,13 +834,13 @@ And for AI mowers:
|
|||
|
||||
**Result**: When you die, all firing stops, the camera dramatically pulls back, and your mower becomes a gray wreck with continuous electrical sparking effects - like damaged electronics shorting out!
|
||||
|
||||
**Step 7**: Add victory condition system. First add the method declaration to ``src/mower.h``:
|
||||
**Step 5**: Add victory condition system. First add the method declaration to ``src/mower.h``:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
void CheckWinCondition();
|
||||
|
||||
**Step 8**: Add victory check in ``src/mower.cpp`` Update method:
|
||||
**Step 6**: Add victory check in ``src/mower.cpp`` Update method:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
|
|
@ -793,7 +857,7 @@ And for AI mowers:
|
|||
// Rest of existing update code...
|
||||
}
|
||||
|
||||
**Step 9**: Implement the victory detection system:
|
||||
**Step 7**: Implement the victory detection system:
|
||||
|
||||
.. code-block:: cpp
|
||||
|
||||
|
|
@ -823,32 +887,38 @@ And for AI mowers:
|
|||
}
|
||||
|
||||
// Check if all AI tanks are destroyed
|
||||
if (totalAITanks > 0 && aliveTanks == 0)
|
||||
if (totalAITanks > 0 && aliveTanks == 0 && !victoryTriggered_)
|
||||
{
|
||||
victoryTriggered_ = true; // Set flag to prevent multiple celebrations
|
||||
|
||||
// Player wins! Display victory message
|
||||
DRY_LOGINFO("=== VICTORY! ALL ENEMY TANKS DESTROYED! ===");
|
||||
|
||||
// Create a big celebration explosion at player position
|
||||
Vector3 playerPos = node_->GetWorldPosition();
|
||||
|
||||
// Victory celebration - minimal and clean
|
||||
for (int i = 0; i < 15; ++i)
|
||||
// Victory celebration - exciting but controlled (only fires once!)
|
||||
for (int i = 0; i < 50; ++i)
|
||||
{
|
||||
Vector3 fireworkPos = playerPos + Vector3{RandomOffCenter(3.0f), Random(2.0f) + 1.0f, RandomOffCenter(3.0f)};
|
||||
Sparkle(fireworkPos, 15.0f + Random(10.0f));
|
||||
Vector3 fireworkPos = playerPos + Vector3{RandomOffCenter(6.0f), Random(4.0f) + 2.0f, RandomOffCenter(6.0f)};
|
||||
Sparkle(fireworkPos, 25.0f + Random(15.0f));
|
||||
}
|
||||
|
||||
// Small victory trail burst
|
||||
for (int i = 0; i < 8; ++i)
|
||||
// Victory trail burst
|
||||
for (int i = 0; i < 20; ++i)
|
||||
{
|
||||
Vector3 trailStart = playerPos + Vector3{RandomOffCenter(2.0f), Random(1.5f), RandomOffCenter(2.0f)};
|
||||
Vector3 trailStart = playerPos + Vector3{RandomOffCenter(4.0f), Random(2.0f) + 1.0f, RandomOffCenter(4.0f)};
|
||||
Trail(trailStart);
|
||||
}
|
||||
|
||||
// Single celebration light
|
||||
Node* lightNode = GetScene()->CreateChild("VictoryLight");
|
||||
lightNode->CreateComponent<BlastLight>();
|
||||
lightNode->SetPosition(playerPos + Vector3::UP * 2.0f);
|
||||
// Multiple celebration lights
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
Node* lightNode = GetScene()->CreateChild("VictoryLight");
|
||||
lightNode->CreateComponent<BlastLight>();
|
||||
Vector3 lightPos = playerPos + Vector3{RandomOffCenter(3.0f), Random(2.0f) + 3.0f, RandomOffCenter(3.0f)};
|
||||
lightNode->SetPosition(lightPos);
|
||||
}
|
||||
|
||||
// Victory sound
|
||||
PlaySample(RES(Sound, "Samples/Explode.wav"), 2.0f);
|
||||
|
|
@ -859,9 +929,9 @@ And for AI mowers:
|
|||
}
|
||||
|
||||
**Victory Effects**:
|
||||
- **Victory Sparkles**: 15 sparkles in a small area around the player (minimal and clean)
|
||||
- **Victory Trails**: 8 colored trail effects radiating outward from player position
|
||||
- **Celebration Light**: Single blast light above the player
|
||||
- **Victory Sparkles**: 50 sparkles in a large area around the player (exciting celebration)
|
||||
- **Victory Trails**: 20 colored trail effects radiating outward from player position
|
||||
- **Celebration Lights**: 3 blast lights scattered around the player
|
||||
- **Audio Feedback**: Victory explosion sound at double volume
|
||||
- **Console Message**: Clear victory announcement with enemy count
|
||||
- **Single Trigger**: Victory only fires once when condition is first met (prevents multiple celebrations per frame)
|
||||
|
|
@ -886,7 +956,7 @@ And for AI mowers:
|
|||
# Watch for log message: "=== VICTORY! ALL ENEMY TANKS DESTROYED! ==="
|
||||
# Enjoy the massive fireworks celebration!
|
||||
|
||||
**Result**: When you destroy the last enemy tank, a clean and satisfying victory celebration appears around your mower with minimal sparkles, trails, a light, and victory sound - making you feel like a true tank commander without visual chaos!
|
||||
**Result**: When you destroy the last enemy tank, a massive and exciting victory celebration appears around your mower with 50 sparkles, 20 trails, 3 lights, and victory sound - making you feel like a true tank commander with a spectacular fireworks display!
|
||||
|
||||
Complete Modding Results
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue