combat/weapon.cs
using System;
using Sandbox;
public enum WeaponPhysicsState
{
Equipped,
Ground,
AirborneFloating,
LethalProjectile,
Impaled
}
public sealed class Weapon : Component
{
[Property, Group( "1. Données" )]
public WeaponComboResource ComboData { get; set; }
[Property, Group( "1. Données" )]
[Description( "Profil de combat optionnel réservé aux bots IA (animations 3PP et équilibrage dédiés)." )]
public WeaponComboResource BotComboData { get; set; }
[Property, Group( "2. Physique" )] public Collider MainCollider { get; set; }
[Property, Group( "2. Physique" )] public Rigidbody MainRigidbody { get; set; }
[Property, Group( "3. Shady Knight (Kick & Pop)" )]
public float PopUpwardForce { get; set; } = 220f;
[Property, Group( "3. Shady Knight (Kick & Pop)" )]
public float PopForwardForce { get; set; } = 30f;
[Property, Group( "3. Shady Knight (Kick & Pop)" )]
public float AirborneGravityScale { get; set; } = 0.35f;
[Property, Group( "3. Shady Knight (Kick & Pop)" )]
public Vector3 PopSpinVelocity { get; set; } = new( 2f, 16f, 4f );
[Property, Group( "3. Shady Knight (Kick & Pop)" )]
public float AerialKickSpeed { get; set; } = 1400f;
[Property, Group( "4. Règles d'Empalement" )]
public bool CanImpaleOnThrow { get; set; } = true;
[Property, Group( "4. Règles d'Empalement" )]
public bool CanImpaleOnAerialKick { get; set; } = false;
public WeaponType Type => ComboData?.Type ?? WeaponType.Glaive;
public string WeaponName => ComboData?.WeaponName ?? "Arme inconnue";
public bool IsEquipped => CurrentState == WeaponPhysicsState.Equipped;
public Fighter OwnerFighter { get; private set; }
public Fighter ImpaledVictim { get; private set; }
public WeaponPhysicsState CurrentState { get; private set; } = WeaponPhysicsState.Ground;
private Vector3 _lastWorldPos;
private TimeSince _timeSinceStateChanged = 0f;
private Fighter _lastThrower;
private bool _isAerialKickProjectile = false;
private SkinnedModelRenderer _impaledRenderer;
private string _impaledAttachmentName;
private Transform _impaledLocalOffset;
protected override void OnAwake()
{
MainCollider ??= Components.Get<Collider>();
MainRigidbody ??= Components.Get<Rigidbody>();
_lastWorldPos = WorldPosition;
}
public void EquipTo( Fighter newOwner, GameObject socket )
{
OwnerFighter = newOwner;
_lastThrower = newOwner;
CurrentState = WeaponPhysicsState.Equipped;
_isAerialKickProjectile = false;
ClearImpaleData();
if ( MainRigidbody != null )
{
MainRigidbody.MotionEnabled = false;
MainRigidbody.Velocity = Vector3.Zero;
MainRigidbody.AngularVelocity = Vector3.Zero;
}
if ( MainCollider != null )
MainCollider.Enabled = false;
GameObject.Parent = socket;
GameObject.LocalPosition = Vector3.Zero;
GameObject.LocalRotation = Rotation.Identity;
GameObject.Tags.Remove( "weapon_drop" );
}
public void Drop( Vector3 throwVelocity, bool isLethalThrow = false, Vector3? dropPosition = null, Rotation? dropRotation = null )
{
Vector3 targetPos = dropPosition ?? WorldPosition;
Rotation targetRot = dropRotation ?? WorldRotation;
if ( targetPos.LengthSquared < 1f && OwnerFighter != null && OwnerFighter.IsValid )
{
targetPos = OwnerFighter.WorldPosition + Vector3.Up * 45f;
targetRot = OwnerFighter.WorldRotation;
}
_lastThrower = OwnerFighter;
OwnerFighter = null;
GameObject.Parent = null;
WorldPosition = targetPos;
WorldRotation = targetRot;
_isAerialKickProjectile = false;
ClearImpaleData();
if ( MainCollider != null )
MainCollider.Enabled = true;
if ( MainRigidbody != null )
{
MainRigidbody.MotionEnabled = true;
MainRigidbody.Velocity = throwVelocity;
MainRigidbody.AngularVelocity = isLethalThrow
? Vector3.Zero
: new Vector3( Game.Random.Float( -2f, 2f ), Game.Random.Float( -2f, 2f ), Game.Random.Float( -2f, 2f ) );
}
GameObject.Tags.Add( "weapon_drop" );
_timeSinceStateChanged = 0f;
_lastWorldPos = WorldPosition;
if ( isLethalThrow )
{
CurrentState = WeaponPhysicsState.LethalProjectile;
if ( throwVelocity.Length > 10f )
WorldRotation = Rotation.LookAt( throwVelocity.Normal );
}
else
{
CurrentState = WeaponPhysicsState.Ground;
}
}
public void Dislodge()
{
if ( CurrentState != WeaponPhysicsState.Impaled ) return;
var dropPos = WorldPosition;
var dropRot = WorldRotation;
ClearImpaleData();
var dropVel = new Vector3( Game.Random.Float( -15f, 15f ), Game.Random.Float( -15f, 15f ), -45f );
Drop( dropVel, isLethalThrow: false, dropPosition: dropPos, dropRotation: dropRot );
Log.Info( $"💥 [{GameObject.Name}] Décrochée du corps !" );
}
public void ReceiveKick( Vector3 kickOrigin, Vector3 kickForward, Fighter kicker )
{
if ( IsEquipped || CurrentState == WeaponPhysicsState.Impaled ) return;
_lastThrower = kicker;
if ( CurrentState == WeaponPhysicsState.AirborneFloating ||
(CurrentState == WeaponPhysicsState.Ground && WorldPosition.z > kickOrigin.z - 30f && (MainRigidbody?.Velocity.Length ?? 0f) > 75f) )
{
CurrentState = WeaponPhysicsState.LethalProjectile;
_isAerialKickProjectile = true;
_timeSinceStateChanged = 0f;
var launchDir = (kickForward.WithZ( 0 ).Normal + Vector3.Up * 0.04f).Normal;
if ( MainRigidbody != null )
{
MainRigidbody.MotionEnabled = true;
MainRigidbody.Velocity = launchDir * AerialKickSpeed;
MainRigidbody.AngularVelocity = Vector3.Zero;
}
WorldRotation = Rotation.LookAt( launchDir );
Log.Info( $"⚡ [{GameObject.Name}] KICK AÉRIEN ! Vitesse: {AerialKickSpeed} u/s" );
return;
}
CurrentState = WeaponPhysicsState.AirborneFloating;
_isAerialKickProjectile = false;
_timeSinceStateChanged = 0f;
if ( MainRigidbody != null )
{
MainRigidbody.MotionEnabled = true;
MainRigidbody.Velocity = Vector3.Up * PopUpwardForce + kickForward.WithZ( 0 ).Normal * PopForwardForce;
MainRigidbody.AngularVelocity = PopSpinVelocity;
}
Log.Info( $"🎈 [{GameObject.Name}] POP SHADY KNIGHT !" );
}
protected override void OnFixedUpdate()
{
switch ( CurrentState )
{
case WeaponPhysicsState.AirborneFloating:
UpdateAirborneFloat();
break;
case WeaponPhysicsState.LethalProjectile:
UpdateLethalFlight();
break;
case WeaponPhysicsState.Impaled:
if ( ImpaledVictim == null || !ImpaledVictim.IsValid || ImpaledVictim.Status == FighterStatus.Active )
{
Dislodge();
}
break;
}
_lastWorldPos = WorldPosition;
}
protected override void OnPreRender()
{
if ( CurrentState == WeaponPhysicsState.Impaled )
UpdateImpaledPosition();
}
private void UpdateAirborneFloat()
{
if ( MainRigidbody != null && MainRigidbody.MotionEnabled )
{
MainRigidbody.Velocity += Scene.PhysicsWorld.Gravity * (AirborneGravityScale - 1.0f) * Time.Delta;
}
if ( _timeSinceStateChanged > 2.5f || ((MainRigidbody?.Velocity.Length ?? 0f) < 20f && _timeSinceStateChanged > 0.8f) )
{
CurrentState = WeaponPhysicsState.Ground;
}
}
private void UpdateLethalFlight()
{
var currentVel = MainRigidbody?.Velocity ?? Vector3.Zero;
if ( currentVel.LengthSquared > 2500f )
{
WorldRotation = Rotation.Slerp( WorldRotation, Rotation.LookAt( currentVel.Normal ), Time.Delta * 20f );
}
var moveDelta = WorldPosition - _lastWorldPos;
var traceDist = moveDelta.Length;
if ( traceDist > 0.01f )
{
var trace = Scene.Trace.Ray( _lastWorldPos, WorldPosition + moveDelta.Normal * 15f )
.Radius( 6f )
.UseHitboxes()
.IgnoreGameObjectHierarchy( GameObject );
if ( _lastThrower != null && _lastThrower.IsValid )
trace = trace.IgnoreGameObjectHierarchy( _lastThrower.GameObject );
var result = trace.Run();
if ( result.Hit )
{
OnProjectileHit( result, currentVel.Normal );
}
}
if ( _timeSinceStateChanged > 4.0f || currentVel.Length < 120f )
{
CurrentState = WeaponPhysicsState.Ground;
}
}
private void OnProjectileHit( SceneTraceResult trace, Vector3 hitDir )
{
var victim = trace.GameObject?.Components.Get<Fighter>( FindMode.EverythingInSelfAndAncestors )
?? trace.GameObject?.Components.GetInParent<Fighter>();
if ( victim != null && victim != _lastThrower && !victim.IsDead )
{
var victimRenderer = trace.GameObject?.Components.Get<SkinnedModelRenderer>( FindMode.EverythingInSelfAndDescendants )
?? victim.Components.GetInChildren<SkinnedModelRenderer>();
var boneName = (victimRenderer?.Model != null && trace.Bone >= 0) ? victimRenderer.Model.GetBoneName( trace.Bone ) : "Unknown";
var profile = ComboData?.ThrownProfile ?? new AttackProfile();
var hitZone = CombatGeometry.ResolveHitZone( boneName );
var hitDirection = CombatGeometry.CalculateHitDirection( victim.WorldPosition, victim.WorldRotation, _lastWorldPos );
bool canImpaleByMode = _isAerialKickProjectile ? CanImpaleOnAerialKick : CanImpaleOnThrow;
bool isImpaleType = profile.Type == DamageType.Pierce || profile.Type == DamageType.Slash;
bool willImpale = canImpaleByMode && isImpaleType;
var context = new DamageContext
{
Attacker = _lastThrower,
HealthDamage = profile.Damage,
BalanceDamage = profile.BalanceDamage,
KnockbackForce = hitDir * profile.FighterKnockbackForce,
HitPosition = trace.HitPosition,
Zone = hitZone,
Direction = hitDirection,
Type = profile.Type,
IsHeavy = profile.IsHeavy,
CausesKnockdown = true,
ForceKnockdown = willImpale
};
// Émission vers CombatEvents avec le statut d'empalement précis
bool throwerIsPlayer = _lastThrower == null || _lastThrower.Components.Get<PlayerController>() != null;
if ( throwerIsPlayer )
{
CombatEvents.EmitPlayerWeaponThrowHit( this, victim, context, willImpale );
}
victim.ApplyDamage( context );
if ( willImpale )
{
var impaleSound = ComboData?.ImpaleSound ?? ComboData?.HitFleshSound;
if ( impaleSound != null )
Sound.Play( impaleSound, trace.HitPosition );
AttachToVictimAnatomy( victimRenderer, victim, hitZone, trace.HitPosition, hitDirection );
return;
}
else
{
var fleshSound = ComboData?.HitFleshSound;
if ( fleshSound != null )
Sound.Play( fleshSound, trace.HitPosition );
}
}
else
{
var worldSound = ComboData?.HitWorldSound;
if ( worldSound != null )
Sound.Play( worldSound, trace.HitPosition );
}
CurrentState = WeaponPhysicsState.Ground;
_isAerialKickProjectile = false;
if ( MainRigidbody != null )
{
var reflectVel = Vector3.Reflect( MainRigidbody.Velocity, trace.Normal ) * 0.35f;
MainRigidbody.Velocity = reflectVel + Vector3.Up * 80f;
}
}
private void AttachToVictimAnatomy( SkinnedModelRenderer renderer, Fighter victim, HitZone zone, Vector3 hitPos, HitDirection hitDir )
{
CurrentState = WeaponPhysicsState.Impaled;
_timeSinceStateChanged = 0f;
if ( MainRigidbody != null )
{
MainRigidbody.MotionEnabled = false;
MainRigidbody.Velocity = Vector3.Zero;
MainRigidbody.AngularVelocity = Vector3.Zero;
}
if ( MainCollider != null )
MainCollider.Enabled = false;
_impaledRenderer = renderer;
ImpaledVictim = victim;
bool isFromBack = (hitDir == HitDirection.Back);
string targetAttachName = zone switch
{
HitZone.Head => "impale_head",
HitZone.Torso => "impale_chest",
HitZone.Legs => "impale_leg",
_ => "impale_chest"
};
if ( renderer != null && renderer.IsValid && renderer.GetAttachment( targetAttachName ).HasValue )
{
_impaledAttachmentName = targetAttachName;
var attachTransform = renderer.GetAttachment( targetAttachName ).Value;
WorldPosition = attachTransform.Position;
WorldRotation = isFromBack ? attachTransform.Rotation * Rotation.FromYaw( 180f ) : attachTransform.Rotation;
Log.Info( $"📌 [{GameObject.Name}] EMPALÉ sur '{targetAttachName}' (Dos: {isFromBack})" );
return;
}
_impaledAttachmentName = null;
var victimForward = victim.WorldRotation.Forward.WithZ( 0 ).Normal;
var cleanDirection = isFromBack ? victimForward : -victimForward;
var cleanRotation = Rotation.LookAt( cleanDirection, Vector3.Up );
if ( renderer != null && renderer.IsValid )
{
_impaledLocalOffset = renderer.WorldTransform.ToLocal( new Transform( hitPos, cleanRotation ) );
GameObject.Parent = renderer.GameObject;
}
else
{
WorldPosition = hitPos;
WorldRotation = cleanRotation;
}
Log.Info( $"📌 [{GameObject.Name}] EMPALÉ verrouillé (Dos: {isFromBack})" );
}
private void UpdateImpaledPosition()
{
if ( _impaledRenderer == null || !_impaledRenderer.IsValid )
{
Dislodge();
return;
}
if ( !string.IsNullOrWhiteSpace( _impaledAttachmentName ) )
{
var attach = _impaledRenderer.GetAttachment( _impaledAttachmentName );
if ( attach.HasValue )
{
WorldPosition = attach.Value.Position;
return;
}
}
var worldTrans = _impaledRenderer.WorldTransform.ToWorld( _impaledLocalOffset );
WorldPosition = worldTrans.Position;
WorldRotation = worldTrans.Rotation;
}
private void ClearImpaleData()
{
_impaledRenderer = null;
_impaledAttachmentName = null;
ImpaledVictim = null;
}
}