Partial Vehicle class that handles catching fire and becoming a wreck. Host-side logic ignites the vehicle when body or engine health hits zero, burns occupants and nearby players once per second, explodes after BurnTime, and removes the wreck after WreckSeconds. Client visuals clone an ignite prefab onto the body, grow particle emitters over time, add a flickering point light, and swap all renderers to a charred material.
namespace HorizonRP;
/// <summary>
/// Le feu et l'épave. Une caisse ou un moteur à zéro — par les chocs, les balles ou un explosif —
/// et le véhicule prend feu (<see cref="OnFire"/>) : de vraies flammes sur toute la carrosserie
/// (l'effet « ignite » du moteur, qui grandit pendant qu'il brûle), crépitement, ceux qui restent
/// à bord se brûlent. Au bout de <see cref="BurnTime"/> (une minute par défaut) il explose (l'explosif
/// « vehicule » : le souffle blesse et abîme autour) et devient une épave (<see cref="Wrecked"/>) :
/// carbonisée, vitres soufflées, pneus à plat, moteur mort, fumée noire, inutilisable. Au bout de
/// <see cref="WreckSeconds"/> (dix minutes), elle disparaît d'elle-même (Julien) : un véhicule
/// acheté rentre au garage dans cet état, à réparer.
/// </summary>
public sealed partial class Vehicle
{
/// <summary>Le temps qu'un véhicule en feu met à exploser (secondes ; réglage « vehicule_feu_secondes »).</summary>
public static float BurnTime => ServerSettings.VehicleBurnSeconds;
[Sync( SyncFlags.FromHost )] public bool OnFire { get; set; }
[Sync( SyncFlags.FromHost )] public bool Wrecked { get; set; }
RealTimeSince burning;
RealTimeSince burnTick;
GameObject flames;
SoundHandle fireSound;
bool tintedWreck;
/// <summary>Le temps qu'une épave reste dans la ville avant de disparaître (secondes).</summary>
public static float WreckSeconds { get; set; } = 600f;
bool wreckCounting;
RealTimeSince sinceWrecked;
RealTimeSince sinceCharred = 10;
const string CharredMaterial = "materials/horizon/carbonise.vmat";
/// <summary>Hôte : un explosif à proximité (<paramref name="f"/> : 1 au contact, 0 au bord du souffle).</summary>
public void TakeBlast( Vector3 position, float f, float power )
{
if ( Rig is null || !bodyRenderer.IsValid() )
return;
var center = WorldTransform.PointToWorld( Rig.Bounds.Center );
var away = (center - position).WithZ( 0 ).Normal;
// La tôle enfoncée du côté de l'explosion, plus fort qu'un choc (et qui compte comme un).
var near = center - away * Rig.Bounds.Size.y * 0.5f;
TakeImpact( near, 120 + power * f * 0.3f );
BodyHealth = MathF.Max( 0, BodyHealth - power * f * 0.6f );
EngineHealth = MathF.Max( 0, EngineHealth - power * f * 0.5f );
// Vitres soufflées, pneus crevés : tout près, tout ; plus loin, ceux du côté de l'explosion.
var pieces = GlassPieces();
for ( var g = 0; g < pieces.Length; g++ )
{
if ( pieces[g] is null )
continue;
foreach ( var p in pieces[g].Distinct() )
if ( f > 0.45f || f > 0.15f && Game.Random.Float() < f * 1.5f )
BreakGlass( g, p );
}
for ( var i = 0; i < Rig.Wheels.Length; i++ )
{
var wheel = WorldTransform.PointToWorld( Rig.Wheels[i].Center );
if ( f > 0.5f || f > 0.2f && wheel.Distance( position ) < 160 )
Puncture( i );
}
// Soufflé : poussé, soulevé, un peu tourné.
if ( Components.Get<Rigidbody>() is { } body )
{
Wake();
body.Velocity += (away + Vector3.Up * 0.7f).Normal * power * f * 0.35f;
body.AngularVelocity += new Vector3( Game.Random.Float( -1, 1 ), Game.Random.Float( -1, 1 ), Game.Random.Float( -0.5f, 0.5f ) ) * f * 2.5f;
}
RequestSaveState();
}
/// <summary>Chaque image : l'hôte allume, brûle et fait exploser ; chaque machine montre le feu et l'épave.</summary>
void UpdateFire()
{
// Hôte : l'épave s'en va d'elle-même (le compte part de l'explosion, ou de l'apparition
// d'une épave enregistrée).
if ( Networking.IsHost && Ready && Wrecked )
{
if ( !wreckCounting )
{
wreckCounting = true;
sinceWrecked = 0;
}
else if ( sinceWrecked > WreckSeconds )
{
HorizonGame.Current?.AddLog( "vehicule_feu", OwnerId, $"Épave {Plate} retirée de la ville (au bout de {WreckSeconds / 60:0} min)" );
Remove();
return;
}
}
else
wreckCounting = false;
if ( Networking.IsHost && Ready && !Wrecked )
{
if ( !OnFire && (BodyHealth <= 0 || EngineHealth <= 0) )
Ignite();
if ( OnFire )
{
// Les flammes brûlent fort (Julien) : à bord, on ne tient que quelques secondes ; contre
// la carrosserie aussi, un peu moins.
if ( burnTick > 1f )
{
burnTick = 0;
var aboard = new HashSet<RoleplayPlayer>();
foreach ( var seat in Seats )
if ( seat.GetOccupant()?.GetComponent<RoleplayPlayer>() is { } p )
{
aboard.Add( p );
WeaponSystem.Current?.HostDamage( p, FireDamageAboard, null, "brûlé dans un véhicule" );
}
var center = WorldTransform.PointToWorld( Rig.Bounds.Center );
var reach = MathF.Max( Rig.Bounds.Size.x, Rig.Bounds.Size.y ) * 0.5f + FireReach;
foreach ( var p in RoleplayPlayer.All )
if ( p.IsValid() && p.Health > 0 && !aboard.Contains( p ) && (p.WorldPosition + Vector3.Up * 36).Distance( center ) < reach )
WeaponSystem.Current?.HostDamage( p, FireDamageNear, null, "brûlé par un véhicule en feu" );
}
if ( burning > BurnTime )
Explode();
}
}
FireVisuals();
}
/// <summary>Les brûlures par seconde : à bord, et à moins de <see cref="FireReach"/> pouces de la caisse.</summary>
public static float FireDamageAboard { get; set; } = 22f;
public static float FireDamageNear { get; set; } = 12f;
public static float FireReach { get; set; } = 45f;
/// <summary>Ni en feu ni en épave : on peut y monter et ouvrir le coffre.</summary>
public bool Usable => !OnFire && !Wrecked;
/// <summary>Hôte : le feu prend. Le moteur s'arrête.</summary>
void Ignite()
{
OnFire = true;
burning = 0;
burnTick = 0;
if ( EngineOn )
{
EngineOn = false;
PlayEngineSwitch( false );
}
HorizonGame.Current?.AddLog( "vehicule_feu", 0, $"Véhicule {Plate} en feu" );
}
/// <summary>Hôte : l'explosion finale. Ceux qui sont encore à bord sont éjectés dans le souffle.</summary>
void Explode()
{
OnFire = false;
Wrecked = true;
EngineHealth = 0;
BodyHealth = 0;
foreach ( var seat in Seats.ToList() )
if ( seat.GetOccupant() is { } pc )
seat.Eject( pc );
var center = WorldTransform.PointToWorld( Rig.Bounds.Center );
WeaponSystem.Current?.Detonate( Explosives.Get( "vehicule" ), center, null, Vector3.Zero );
FlatTyres = (1 << Rig.Wheels.Length) - 1;
HorizonGame.Current?.AddLog( "vehicule_feu", 0, $"Véhicule {Plate} détruit (explosion)" );
RequestSaveState();
}
// Le feu : l'effet « ignite » du moteur (prefabs/engine/ignite.prefab), fait pour couvrir un
// modèle : des flammes animées émises sur toute la surface de la carrosserie, la fumée
// au-dessus, les braises, sa lumière et son crépitement. Réglé pour une personne : pour une
// voiture, plus de flammes et plus grandes, et il grandit pendant qu'il brûle (de quelques
// flammes à l'allumage à la voiture embrasée juste avant l'explosion). L'ancien nuage de
// sprites, lui, semait des flammes isolées sur des mètres autour du véhicule (Julien).
const string FirePrefab = "prefabs/engine/ignite.prefab";
readonly List<ParticleModelEmitter> fireEmitters = new();
readonly List<ParticleSphereEmitter> smokeEmitters = new();
RealTimeSince flamesSince;
/// <summary>Chaque machine : les flammes sur la carrosserie tant qu'il brûle ; la caisse noircie de l'épave.</summary>
void FireVisuals()
{
if ( Application.IsDedicatedServer || Rig is null || !bodyRenderer.IsValid() )
return;
if ( OnFire && !flames.IsValid() )
{
flames = GameObject.Clone( FirePrefab, new CloneConfig { Parent = bodyRenderer.GameObject, StartEnabled = false, Transform = new Transform( Vector3.Zero ) } );
flames.Name = "Flammes";
flames.Flags |= GameObjectFlags.NotNetworked | GameObjectFlags.NotSaved;
// Ni ses dégâts (le feu brûle ceux qui sont à bord, voir UpdateFire), ni sa fin d'effet.
foreach ( var c in flames.Components.GetAll<Component>( FindMode.EverythingInSelfAndDescendants ).ToList() )
if ( c is FireDamage or TemporaryEffect )
c.Destroy();
fireEmitters.Clear();
fireEmitters.AddRange( flames.Components.GetAll<ParticleModelEmitter>( FindMode.EverythingInSelfAndDescendants ) );
foreach ( var e in fireEmitters )
e.Target = bodyRenderer.GameObject;
smokeEmitters.Clear();
smokeEmitters.AddRange( flames.Components.GetAll<ParticleSphereEmitter>( FindMode.EverythingInSelfAndDescendants ) );
foreach ( var s in smokeEmitters )
s.Radius = MathF.Max( 30f, Rig.BodyBounds.Size.y * 0.45f );
foreach ( var r in flames.Components.GetAll<ParticleSpriteRenderer>( FindMode.EverythingInSelfAndDescendants ) )
r.Scale *= 1.8f;
// La lueur sur la route, sous la voiture.
var bb = Rig.BodyBounds;
var glow = new GameObject( flames, true, "Lueur" );
glow.LocalPosition = new Vector3( bb.Center.x, bb.Center.y, bb.Maxs.z );
var light = glow.Components.Create<PointLight>();
light.LightColor = new Color( 1f, 0.5f, 0.15f ) * 3f;
light.Radius = 320;
light.Shadows = false;
flames.Enabled = true;
flamesSince = 0;
}
else if ( !OnFire && flames.IsValid() )
{
flames.Destroy();
flames = null;
fireSound?.Stop( 0.5f );
fireSound = null;
}
if ( flames.IsValid() )
{
// Il grandit : de 40 à 170 flammes par seconde sur la minute, la fumée avec.
var grown = Math.Clamp( flamesSince / MathF.Max( 5f, BurnTime ), 0, 1 );
foreach ( var e in fireEmitters )
if ( e.IsValid() )
e.Rate = 40f + 130f * grown;
foreach ( var s in smokeEmitters )
if ( s.IsValid() )
s.Rate = 4f + 14f * grown;
// La flamme vacille.
if ( flames.Components.Get<PointLight>( FindMode.EverythingInDescendants ) is { } light )
light.LightColor = new Color( 1f, 0.5f, 0.15f ) * ((1.6f + 2f * grown) + MathF.Sin( RealTime.Now * 23 ) * 0.5f + MathF.Sin( RealTime.Now * 37 ) * 0.4f);
}
if ( Wrecked != tintedWreck )
{
tintedWreck = Wrecked;
bodyRenderer.Tint = Wrecked ? Color.White : PaintTint( ColorHex );
sinceCharred = 10;
}
// L'épave carbonisée : la teinte ne prenait que la peinture (vitres, chromes, pneus et
// intérieur restaient intacts) ; toute la voiture reçoit la matière brûlée. Reposée de
// temps en temps : les roues et la caisse déformée peuvent être recréées.
if ( sinceCharred > 1f )
{
sinceCharred = 0;
var charred = Wrecked ? Material.Load( CharredMaterial ) : null;
foreach ( var r in GameObject.GetComponentsInChildren<ModelRenderer>( true ) )
{
// Les vitres allumées des feux (calques de Vehicle.Lights.cs, éteints sur une épave) :
// leur matière est à eux, sinon les deux se la renverraient chaque seconde.
if ( r.GameObject.Tags.Has( LampTag ) || r.MaterialOverride?.ResourcePath?.StartsWith( "materials/horizon/feu_" ) == true )
continue;
if ( r.MaterialOverride != charred && (Wrecked || r.MaterialOverride?.ResourcePath == CharredMaterial) )
r.MaterialOverride = charred;
}
}
}
}