Weapons/Explosives.cs

Definition table for the grenades, rockets and mines used by the weapon pack, with blast falloff math copied from the GMod source. Also has the ExplosiveBulletInfo bullet type that turns a shot into a launch request to the host.

using SWB.Base;

namespace HorizonRP;

/// <summary>
/// Les explosifs du pack FA:S 2.0 : ce que l'arme lance, et ce que ça fait en arrivant.
///
/// Valeurs reprises des entités Lua du pack (lua/entities/arccw_firearms2_*_fly et *_thrown) :
/// GMod compte en pouces comme s&box ; son util.BlastDamage( rayon, dégâts ) décroît de façon
/// linéaire du centre au bord, et le pack l'appelle deux fois — un cercle proche à 100 points,
/// un grand à 75. Les vies d'ici valent 100 : à bout portant on meurt, à la limite on s'en sort.
///
/// Modes : « lance » (grenade à main, rebondit, mèche), « obus » (40 mm, trajectoire balistique,
/// explose au contact), « roquette » (droite, au contact, s'autodétruit au bout de sa course),
/// « mine » (posée au sol, part quand quelqu'un passe devant). Effets : « souffle », « flash »
/// (aveugle et assourdit, sans blesser), « fumee » (un nuage une demi-minute).
/// </summary>
public static class Explosives
{
	public sealed record Kind(
		string Id, string Name, string Mode, string Model, float Speed, float Fuse, string Effect,
		float Inner, float Outer, float Damage, float VehiclePower, string[] Sounds, string Distant = null, string Loop = null );

	const string S = "weapons/fas2/sounds/events/";

	static readonly Kind[] all =
	{
		new( "m79", "Grenade 40 mm", "obus", "weapons/fas2/projectiles/m79_grenade.vmdl", 2900, 8, "souffle", 300, 770, 100, 900,
			new[] { S + "fas2_m79_m79_explode1.wav.sound" }, S + "fas2_m79_m79_explode1_distant.ogg.sound" ),
		new( "rpg7", "Roquette PG-7V", "roquette", "weapons/fas2/projectiles/rpg7_rocket.vmdl", 4300, 4.5f, "souffle", 300, 770, 100, 1600,
			new[] { S + "fas2_rpg7_rocket_hit1.wav.sound" }, S + "fas2_rpg26_rpg26_rockethit_distance1.wav.sound", S + "fas2_rpg26_rpg26_rocketloop.wav.sound" ),
		new( "rpg26", "Roquette RPG-26", "roquette", "weapons/fas2/projectiles/rpg26_rocket.vmdl", 4600, 4.5f, "souffle", 300, 770, 100, 1500,
			new[] { S + "fas2_rpg26_rpg26_rockethit.wav.sound" }, S + "fas2_rpg26_rpg26_rockethit_distance1.wav.sound", S + "fas2_rpg26_rpg26_rocketloop.wav.sound" ),
		new( "nade_m67", "Grenade M67", "lance", "weapons/fas2/projectiles/m67_thrown.vmdl", 850, 3.5f, "souffle", 380, 770, 100, 600,
			Enumerable.Range( 1, 6 ).Select( i => S + $"fas2_m67_m67_explode_{i}.wav.sound" ).ToArray(), S + "fas2_m67_m67_explode_1_distant.ogg.sound" ),
		new( "nade_m84", "Grenade M84", "lance", "weapons/fas2/projectiles/m84_thrown.vmdl", 850, 2.5f, "flash", 0, 900, 0, 0,
			new[] { S + "fas2_m84_flashbang_explode1.wav.sound" }, S + "fas2_m84_flashbang_explode1_distant.wav.sound" ),
		new( "nade_m18", "Fumigène M18", "lance", "weapons/fas2/projectiles/m18_thrown.vmdl", 800, 3.5f, "fumee", 0, 260, 0, 0,
			new[] { S + "fas2_m18_smoke.wav.sound" } ),
		// Dirigée : 700 billes d'acier dans un éventail de 60° devant elle (Detonate, « facing »).
		new( "nade_claymore", "Mine Claymore", "mine", "weapons/fas2/projectiles/claymore.vmdl", 0, 300, "souffle", 250, 600, 120, 500,
			new[] { S + "fas2_claymore_claymore_explode_1.wav.sound" }, S + "fas2_claymore_claymore_explode_1_distant.wav.sound" ),
		// Un véhicule qui a brûlé jusqu'au bout (Vehicle.Fire.cs).
		new( "vehicule", "Explosion du véhicule", "", "", 0, 0, "souffle", 240, 650, 90, 700,
			new[] { S + "fas2_rpg7_rocket_hit1.wav.sound" }, S + "fas2_rpg26_rpg26_rockethit_distance1.wav.sound" ),
	};

	public static Kind Get( string id ) => all.FirstOrDefault( x => x.Id == id );

	/// <summary>Décroissance linéaire du souffle, comme util.BlastDamage : 1 au centre, 0 au bord.</summary>
	public static float Falloff( float distance, float radius ) => radius <= 0 ? 0 : MathF.Max( 0, 1 - distance / radius );

	/// <summary>Les dégâts du souffle à cette distance : le cercle proche et le grand, comme le pack.</summary>
	public static float BlastDamage( Kind k, float distance ) =>
		k.Damage * Falloff( distance, k.Inner ) + k.Damage * 0.75f * Falloff( distance, k.Outer );
}

/// <summary>
/// La « balle » des explosifs dans le prefab SWB (posée par outils/fas2/fas2_prefab.py) : chez le
/// tireur, le tir devient une demande à l'hôte, qui lance le projectile (WeaponSystem.RequestLaunch).
/// </summary>
[Group( "SWB" )]
[Title( "Explosif (Horizon RP)" )]
public sealed class ExplosiveBulletInfo : BulletInfo
{
	/// <summary>L'explosif lancé : l'identifiant de l'arme dans Explosives.</summary>
	[Property] public string Kind { get; set; }

	public override void Shoot( Weapon weapon, bool isPrimary, Vector3 spreadOffset )
	{
		if ( !weapon.IsValid() || weapon.IsProxy || !weapon.Owner.IsValid() )
			return;
		var owner = weapon.Owner;
		var dir = (owner.EyeAngles.Forward + spreadOffset).Normal;
		WeaponSystem.Current?.RequestLaunch( Kind, owner.EyePos, dir );
	}
}