SanAndreasUnity/Assets/Scripts/Behaviours/Weapon.cs
2020-12-21 20:55:30 +01:00

855 lines
23 KiB
C#

using SanAndreasUnity.Importing.Conversion;
using SanAndreasUnity.Importing.Items;
using SanAndreasUnity.Importing.Items.Definitions;
using SanAndreasUnity.Importing.Weapons;
using SanAndreasUnity.Utilities;
using SanAndreasUnity.Behaviours.Weapons;
using UnityEngine;
using System.Linq;
using System.Collections.Generic;
using SanAndreasUnity.Importing.Animation;
using System.Reflection;
using SanAndreasUnity.Net;
namespace SanAndreasUnity.Behaviours
{
public static class WeaponSlot
{
public static readonly int Hand = 0,
Melee = 1,
Pistol = 2,
Shotgun = 3,
Submachine = 4, // uzi, mp5, tec9
Machine = 5, // ak47, m4
Rifle = 6,
Heavy = 7, // rocket launcher, flame thrower, minigun
SatchelCharge = 8,
Misc = 9, // spraycan, extinguisher, camera
Misc2 = 10, // dildo, vibe, flowers, cane
Special = 11, // parachute, goggles
Detonator = 12,
Count = 13;
}
public static class WeaponId
{
public static readonly int Pistol = 346;
public static readonly int PistolSilenced = 347;
public static readonly int DesertEagle = 348;
public static readonly int Shotgun = 349;
public static readonly int SawnOff = 350;
public static readonly int SPAS12 = 351;
public static readonly int MicroUzi = 352;
public static readonly int Tec9 = 372;
public static readonly int MP5 = 353;
public static readonly int AK47 = 355;
public static readonly int M4 = 356;
public static readonly int CountryRifle = 357;
public static readonly int SniperRifle = 358;
public static readonly int RocketLauncher = 359;
public static readonly int RocketLauncherHS = 360;
public static readonly int FlameThrower = 361;
public static readonly int MiniGun = 362;
public static readonly int RocketProjectile = 345;
}
// public class WeaponData
// {
// public string type = "";
// public string fireType = "";
// public float targetRange = 0;
// public float weaponRange = 0;
// public int modelId = -1;
// public int slot = -1;
// public AnimGroup animGroup = AnimGroup.None;
// public int clipCapacity = 0;
// public int damage = 0;
// }
public struct WeaponAttackParams
{
public List<int> LayersToIgnoreWhenRaycasting { get; set; }
public List<GameObject> GameObjectsToIgnoreWhenRaycasting { get; set; }
public static WeaponAttackParams Default { get => new WeaponAttackParams(); }
}
public class Weapon : MonoBehaviour
{
private WeaponDef definition = null;
public WeaponDef Definition { get { return this.definition; } }
private WeaponData data = null;
public WeaponData Data { get { return this.data; } }
private WeaponData.GunAimingOffset gunAimingOffset;
public WeaponData.GunAimingOffset GunAimingOffset { get { return this.gunAimingOffset; } }
public int SlotIndex { get { return this.Data.weaponslot; } }
public bool IsGun { get { return this.data.gunData != null; } }
public int AmmoClipSize { get { return this.data.gunData != null ? this.data.gunData.ammoClip : 0 ; } }
public int AmmoInClip { get => m_netWeapon.AmmoInClip; set => m_netWeapon.AmmoInClip = value; }
public int AmmoOutsideOfClip { get => m_netWeapon.AmmoOutsideOfClip; set => m_netWeapon.AmmoOutsideOfClip = value; }
public int TotalAmmo { get { return this.AmmoInClip + this.AmmoOutsideOfClip; } }
public Texture2D HudTexture { get; private set; }
protected Ped m_ped => this.PedOwner;
public Ped PedOwner { get { return m_netWeapon.PedOwner; } internal set { m_netWeapon.PedOwner = value; } }
private static List<System.Type> s_weaponTypes = new List<System.Type> ();
protected WeaponsManager WeaponsSettings { get { return WeaponsManager.Instance; } }
private static GameObject s_weaponsContainer = null;
public static Texture2D CrosshairTexture { get; set; }
public static Texture2D RocketCrosshairTexture { get; set; }
public static Texture2D FistTexture { get; set; }
public AnimationState AimAnimState { get; set; }
private float m_aimAnimTimeForAimWithArmWeapon = 0f;
//public bool IsInsideFireAnim { get { return this.AimAnimState != null && this.AimAnimState.enabled && this.AimAnimState.time > this.AimAnimMaxTime; } }
public Transform GunFlash { get; private set; }
public float LastTimeWhenFired { get; protected set; } = float.NegativeInfinity;
public float TimeSinceFired => Time.time - this.LastTimeWhenFired;
// weapon sounds are located in SFX -> GENRL -> BANK 137
// these indexes represent indexes of sounds in that bank
public static Dictionary<int, int> weaponSoundIndexes = new Dictionary<int, int>() {
{WeaponId.Pistol, 6}, // not correct
{WeaponId.PistolSilenced, 24},
{WeaponId.DesertEagle, 6},
{WeaponId.Shotgun, 21},
{WeaponId.SawnOff, 21},
{WeaponId.SPAS12, 22},
{WeaponId.Tec9, 1},
{WeaponId.MicroUzi, 0},
{WeaponId.MP5, 18},
{WeaponId.AK47, 4},
{WeaponId.M4, 3},
{WeaponId.CountryRifle, 26},
{WeaponId.SniperRifle, 26},
{WeaponId.MiniGun, 11},
// {WeaponId.RocketLauncher, 68},
// {WeaponId.RocketLauncherHS, 68},
};
static Dictionary<int, AudioClip> s_loadedAudioClips = new Dictionary<int, AudioClip>();
// used to play weapon sound
AudioSource m_audioSource;
NetworkedWeapon m_netWeapon;
public NetworkedWeapon NetWeapon => m_netWeapon;
// "size of the array determines how many raycasts will occur" - I don't think this is true
static readonly RaycastHit[] s_raycastHitBuffer = new RaycastHit[100];
WeaponAttackParams m_lastRaycastWeaponAttackParams = WeaponAttackParams.Default;
static readonly HashSet<int> s_weaponsUsingProjectile = new HashSet<int>()
{
WeaponId.RocketLauncher,
WeaponId.RocketLauncherHS,
};
private Geometry.GeometryParts m_projectileModel;
private static AudioClip s_projectileSound;
static Weapon ()
{
// obtain all weapon types
var myType = typeof (Weapon);
foreach (Assembly a in System.AppDomain.CurrentDomain.GetAssemblies())
{
s_weaponTypes.AddRange (a.GetTypes ().Where (t => t.IsSubclassOf (myType)));
}
}
public static Weapon Create (int modelId, Ped initialPedOwner)
{
NetStatus.ThrowIfNotOnServer();
WeaponDef def;
WeaponData weaponData;
GameObject go = CreatePart1(modelId, null, out def, out weaponData);
if (null == go)
return null;
// asign syncvars before spawning it
var networkedWeapon = go.GetComponentOrThrow<NetworkedWeapon>();
networkedWeapon.ModelId = modelId;
networkedWeapon.PedOwner = initialPedOwner;
// spawn game object here
NetManager.Spawn(go);
Weapon weapon = CreatePart2(go, def, weaponData);
if (initialPedOwner != null)
initialPedOwner.WeaponHolder.AddWeapon(weapon);
return weapon;
}
static GameObject CreatePart1(int modelId, GameObject go, out WeaponDef def, out WeaponData weaponData)
{
def = null;
weaponData = null;
def = Item.GetDefinition<WeaponDef> (modelId);
if (null == def)
return null;
var defCopyRef = def;
weaponData = WeaponData.LoadedWeaponsData.FirstOrDefault (wd => wd.modelId1 == defCopyRef.Id);
if (null == weaponData)
return null;
var geoms = Geometry.Load (def.ModelName, def.TextureDictionaryName);
if (null == geoms)
return null;
if (null == s_weaponsContainer) {
s_weaponsContainer = new GameObject ("Weapons");
// weaponsContainer.SetActive (false);
}
if (null == go)
go = Object.Instantiate(WeaponsManager.Instance.weaponPrefab);
go.name = def.ModelName;
go.transform.SetParent (s_weaponsContainer.transform);
geoms.AttachFrames (go.transform, MaterialFlags.Default);
return go;
}
static Weapon CreatePart2(GameObject go, WeaponDef def, WeaponData weaponData)
{
int modelId = def.Id;
Weapon weapon = AddWeaponComponent (go, weaponData);
weapon.definition = def;
weapon.data = weaponData;
// cache gun aiming offset
if (weapon.data.gunData != null)
weapon.gunAimingOffset = weapon.data.gunData.aimingOffset;
// load hud texture
try {
weapon.HudTexture = TextureDictionary.Load( def.TextureDictionaryName ).GetDiffuse( def.TextureDictionaryName + "icon" ).Texture;
} catch {
Debug.LogErrorFormat ("Failed to load hud icon for weapon: model {0}, txd {1}", def.ModelName, def.TextureDictionaryName);
}
// weapon sound
F.RunExceptionSafe (() => {
if (weaponSoundIndexes.ContainsKey (modelId))
{
var audioSource = go.GetOrAddComponent<AudioSource> ();
audioSource.playOnAwake = false;
AudioClip audioClip = null;
if (s_loadedAudioClips.ContainsKey(modelId))
{
audioClip = s_loadedAudioClips[modelId];
}
else
{
audioClip = Audio.AudioManager.CreateAudioClipFromSfx ("GENRL", 136, weaponSoundIndexes [modelId]);
s_loadedAudioClips[modelId] = audioClip;
}
audioSource.clip = audioClip;
weapon.m_audioSource = audioSource;
}
});
weapon.InitWeapon();
return weapon;
}
void LoadProjectileModel()
{
var def = Item.GetDefinition<WeaponDef>(WeaponId.RocketProjectile);
m_projectileModel = Geometry.Load (def.ModelName, def.TextureDictionaryName);
}
void LoadProjectileAudio()
{
if (null == s_projectileSound)
{
s_projectileSound = Audio.AudioManager.CreateAudioClipFromSfx("GENRL", 136, 68);
}
}
internal static void OnWeaponCreatedByServer(NetworkedWeapon networkedWeapon)
{
WeaponDef def;
WeaponData weaponData;
GameObject go = CreatePart1(networkedWeapon.ModelId, networkedWeapon.gameObject, out def, out weaponData);
if (null == go)
return;
Weapon weapon = CreatePart2(go, def, weaponData);
weapon.AssignGunFlashTransform();
if (weapon.PedOwner != null)
weapon.PedOwner.WeaponHolder.AddWeapon(weapon);
}
private static Weapon AddWeaponComponent (GameObject go, WeaponData data)
{
// find type which inherits Weapon class, and whose name matches the one in data
string typeName = data.weaponType.Replace ("_", "");
var type = s_weaponTypes.Where (t => 0 == string.Compare (t.Name, typeName, true)).FirstOrDefault ();
if (type != null) {
return (Weapon)go.AddComponent (type);
} else {
return go.AddComponent<Weapon> ();
}
}
public bool HasFlag( GunFlag gunFlag ) {
if (this.data != null && this.data.gunData != null)
return this.data.gunData.HasFlag (gunFlag);
return false;
}
public static string ExtractAnimGroupName(string assocGroupId)
{
if( assocGroupId.EndsWith( "bad" ) )
return assocGroupId.Substring( 0, assocGroupId.Length - 3 );
if( assocGroupId.EndsWith( "pro" ) )
return assocGroupId.Substring( 0, assocGroupId.Length - 3 );
return assocGroupId;
}
protected virtual void Awake ()
{
m_netWeapon = this.GetComponentOrThrow<NetworkedWeapon>();
this.AssignGunFlashTransform();
}
/// <summary>
/// Called after creating a weapon and assigning it's parameters, such are WeaponData and HUD texture.
/// Use this method to assign aim animations and timings, or initialize anything else related to weapon.
/// </summary>
protected virtual void InitWeapon()
{
// set default weapon anims and other params
string animGroup = ExtractAnimGroupName( this.Data.gunData.AssocGroupId );
this.CanCrouchAim = this.HasFlag( GunFlag.CROUCHFIRE );
if( this.HasFlag( GunFlag.CROUCHFIRE ) )
this.CrouchAimAnim = new AnimId( animGroup, animGroup + "_crouchfire" );
else
this.CrouchAimAnim = new AnimId( "RIFLE", "RIFLE_crouchfire" );
this.CrouchAimAnimMaxTime = WeaponsManager.ConvertAnimTime (this.Data.gunData.animLoop2Start);
this.CrouchAimAnimFireMaxTime = WeaponsManager.ConvertAnimTime (this.Data.gunData.animLoop2End);
this.CrouchSpineRotationOffset = WeaponsSettings.crouchSpineRotationOffset;
this.FiresProjectile = s_weaponsUsingProjectile.Contains(this.Data.modelId1);
if (this.FiresProjectile)
{
this.ProjectilePrefab = WeaponsSettings.projectilePrefab;
this.ReloadTime = WeaponsSettings.projectileReloadTime;
F.RunExceptionSafe(this.LoadProjectileModel);
F.RunExceptionSafe(this.LoadProjectileAudio);
}
}
protected virtual void Start ()
{
}
protected virtual void Update ()
{
if (WeaponsSettings.drawLineFromGun)
{
if (m_ped != null && m_ped.CurrentWeapon == this)
{
Vector3 start, end;
this.GetLineFromGun (out start, out end, m_lastRaycastWeaponAttackParams);
GLDebug.DrawLine (start, end, Color.red, 0, true);
}
}
}
public virtual bool CanSprintWithIt {
get {
if (this.HasFlag (GunFlag.AIMWITHARM))
return true;
if (this.SlotIndex == WeaponSlot.Heavy || this.SlotIndex == WeaponSlot.Machine || this.SlotIndex == WeaponSlot.Rifle
|| this.SlotIndex == WeaponSlot.Shotgun)
return false;
return true;
}
}
public bool IsHeavy { get { return this.HasFlag (GunFlag.HEAVY); } }
public bool CanCrouchAim { get; set; }
public virtual bool CanTurnInDirectionOtherThanAiming {
get {
if (this.HasFlag (GunFlag.AIMWITHARM))
return true;
return false;
}
}
public virtual AnimId IdleAnim {
get {
if (this.HasFlag (GunFlag.AIMWITHARM)) {
return new AnimId (AnimGroup.WalkCycle, AnimIndex.Idle);
} else {
return new AnimId (AnimGroup.MyWalkCycle, AnimIndex.IdleArmed);
}
}
}
public virtual AnimId WalkAnim {
get {
if (this.HasFlag (GunFlag.AIMWITHARM)) {
return new AnimId (AnimGroup.WalkCycle, AnimIndex.Walk);
} else {
return new AnimId (AnimGroup.Gun, AnimIndex.WALK_armed);
}
}
}
public virtual AnimId RunAnim {
get {
if (this.HasFlag (GunFlag.AIMWITHARM)) {
return new AnimId (AnimGroup.WalkCycle, AnimIndex.Run);
} else {
return new AnimId (AnimGroup.Gun, AnimIndex.run_armed);
}
}
}
public virtual AnimId GetAnimBasedOnMovement (bool canSprint)
{
Ped ped = m_ped;
if (ped.IsRunOn) {
return this.RunAnim;
} else if (ped.IsWalkOn) {
return this.WalkAnim;
} else if (ped.IsSprintOn) {
if (canSprint) {
return new AnimId (AnimGroup.MyWalkCycle, AnimIndex.sprint_civi);
} else {
return this.IdleAnim;
}
} else {
// player is standing
return this.IdleAnim;
}
}
public virtual AnimId AimAnim {
get {
return new AnimId (AnimGroup.Rifle, AnimIndex.RIFLE_fire);
}
}
public virtual AnimId AimAnimLowerPart
{
get
{
return new AnimId(AnimGroup.MyWalkCycle, AnimIndex.GUN_STAND);
}
}
public AnimId CrouchAimAnim { get; set; }
public virtual float AimAnimMaxTime {
get {
return Weapons.WeaponsManager.ConvertAnimTime (this.data.gunData.animLoopStart);
}
}
public virtual float AimAnimFireMaxTime {
get {
return Weapons.WeaponsManager.ConvertAnimTime (this.data.gunData.animLoopEnd);
}
}
public float CrouchAimAnimMaxTime { get; set; }
public float CrouchAimAnimFireMaxTime { get; set; }
public virtual float GunFlashDuration {
get {
return Weapons.WeaponsManager.Instance.GunFlashDuration;
}
}
public Vector3 CrouchSpineRotationOffset { get; set; }
/// <summary>
/// True if weapon doesn't inflict damage instantly, but instead fires a projectile.
/// </summary>
public bool FiresProjectile { get; set; } = false;
public GameObject ProjectilePrefab { get; set; }
public float ReloadTime { get; set; } = 0;
// TODO: this function should be removed, and new one should be created: OnAnimsUpdated
public virtual void UpdateAnimWhileHolding ()
{
Ped ped = m_ped;
ped.PlayerModel.PlayAnim (this.GetAnimBasedOnMovement (this.CanSprintWithIt));
}
void AssignGunFlashTransform()
{
this.GunFlash = this.transform.FindChildRecursive("gunflash");
}
public virtual void EnableOrDisableGunFlash ()
{
if (null == this.GunFlash)
return;
// enable/disable gun flash
Ped ped = m_ped;
bool shouldBeVisible = false;
if (ped != null && ped.IsFiring)
{
if (AimAnimState != null && AimAnimState.enabled) {
// aim anim is being played
if (AimAnimState.time.BetweenExclusive (this.AimAnimMaxTime, this.AimAnimMaxTime + this.GunFlashDuration)) {
// muzzle flash should be visible
shouldBeVisible = true;
}
}
}
this.GunFlash.gameObject.SetActive (shouldBeVisible);
}
public virtual void UpdateGunFlashRotation ()
{
if (null == this.GunFlash)
return;
if (!this.GunFlash.gameObject.activeInHierarchy)
return;
float randomFactor = Random.Range (0.75f, 1.25f);
float delta = WeaponsManager.Instance.GunFlashRotationSpeed * Time.deltaTime * randomFactor;
this.GunFlash.rotation *= Quaternion.AngleAxis (delta, Vector3.right);
}
#region Firing
public float MaxRange { get { return this.Data.weaponRange; } }
public float Damage { get { return this.Data.gunData.damage; } }
public virtual void PlayFireSound ()
{
if (m_audioSource && m_audioSource.clip)
{
if(!m_audioSource.isPlaying)
{
// int bankIndex = weaponSoundIndexes [this.Definition.Id];
// Audio.AudioManager.SfxGENRL137Timings [bankIndex];
// int startTimeMs = 0;
// int endTimeMs = 0;
//
// float startTime = startTimeMs / 1000f;
// float endTime = endTimeMs / 1000f;
// Debug.LogFormat("playing weapon sound, start time {0}, end time {1}, bank index {2}", startTime, endTime, bankIndex);
// m_audioSource.Stop();
// m_audioSource.time = startTime;
// m_audioSource.Play();
// m_audioSource.SetScheduledEndTime( AudioSettings.dspTime + (endTime - startTime) );
// Debug.LogFormat("playing weapon sound");
m_audioSource.Stop();
m_audioSource.time = 0f;
m_audioSource.Play();
}
}
}
public void FireProjectile (WeaponAttackParams parameters)
{
this.FireProjectile(this.GetFirePos(), this.GetFireDir(parameters), parameters);
}
public virtual void FireProjectile (Vector3 firePos, Vector3 fireDir, WeaponAttackParams parameters)
{
this.LastTimeWhenFired = Time.time;
if (this.FiresProjectile)
{
Projectile.Create(this.ProjectilePrefab, firePos, Quaternion.LookRotation(fireDir), s_projectileSound, m_projectileModel, m_ped);
return;
}
// raycast against all (non-breakable ?) objects
RaycastHit hit;
if (this.ProjectileRaycast (firePos, fireDir, out hit, parameters))
{
// if target object has damageable script, inflict damage to it
var damageable = hit.collider.gameObject.GetComponentInParent<Damageable> ();
if (damageable)
{
// ray hit something that can be damaged
// damage it
// first check if target object is owner ped
if (m_ped != null && m_ped.gameObject == damageable.gameObject)
{
// ray hit owner ped
// don't do anything
}
else
{
damageable.Damage(new DamageInfo()
{
amount = this.Damage,
raycastHitTransform = hit.collider.transform,
hitPoint = hit.point,
hitNormal = hit.normal,
attacker = m_ped,
});
}
}
}
}
public bool ProjectileRaycast (Vector3 source, Vector3 dir, out RaycastHit hit, WeaponAttackParams parameters)
{
m_lastRaycastWeaponAttackParams = parameters;
if (null == parameters.GameObjectsToIgnoreWhenRaycasting)
return Physics.Raycast(source, dir, out hit, this.MaxRange, WeaponsManager.Instance.projectileRaycastMask);
int numHits = Physics.RaycastNonAlloc (source, dir, s_raycastHitBuffer, this.MaxRange, WeaponsManager.Instance.projectileRaycastMask);
if (numHits < 1)
{
hit = new RaycastHit();
return false;
}
// "Note that the order of the results is undefined"
// - that's why we need to search for closest hit
var validHits = s_raycastHitBuffer
.Take(numHits)
.Where(h => h.collider != null && !ShouldIgnoreObjectWhenRaycasting(h.collider, parameters));
if (!validHits.Any())
{
hit = new RaycastHit();
return false;
}
hit = validHits.Aggregate((h1, h2) => h1.distance <= h2.distance ? h1 : h2);
return true;
}
private static bool ShouldIgnoreObjectWhenRaycasting(Collider collider, WeaponAttackParams parameters)
{
if (!parameters.LayersToIgnoreWhenRaycasting.Contains(collider.gameObject.layer))
return false;
if (parameters.GameObjectsToIgnoreWhenRaycasting.Exists(go => go == collider.gameObject))
return true;
if (parameters.GameObjectsToIgnoreWhenRaycasting.Exists(go => go.transform.IsParentOf(collider.transform)))
return true;
return false;
}
public void GetLineFromGun (out Vector3 start, out Vector3 end, WeaponAttackParams parameters)
{
float distance = this.MaxRange;
Vector3 firePos = this.GetFirePos ();
Vector3 fireDir = this.GetFireDir (parameters);
RaycastHit hit;
if (this.ProjectileRaycast (firePos, fireDir, out hit, parameters))
{
distance = hit.distance;
}
start = firePos;
end = firePos + fireDir * distance;
}
Vector3 GetFirePos()
{
return m_ped != null ? m_ped.FirePosition : this.GetFirePosWithoutPed();
}
public Vector3 GetFirePosWithoutPed ()
{
Vector3 firePos;
if (this.GunFlash != null)
firePos = this.GunFlash.transform.position;
else if (this.Data.gunData != null)
firePos = this.transform.TransformPoint (this.Data.gunData.fireOffset);
else
firePos = this.transform.position;
return firePos;
}
Vector3 GetFireDir (WeaponAttackParams parameters)
{
if (m_ped != null)
{
return m_ped.FireDirection;
}
else
{
return this.GetFireDirWithoutPed();
}
}
public Vector3 GetFireDirWithoutPed()
{
if (this.GunFlash)
return this.GunFlash.transform.right;
else
return this.transform.right;
}
#endregion
public virtual void OnDrawGizmosSelected ()
{
// draw rays from gun
Vector3 firePos = this.GetFirePos ();
// ray based on transform
Gizmos.color = Color.yellow;
GizmosDrawCastedRay (firePos, this.transform.right);
// ray based on gun flash transform
if (this.GunFlash != null)
{
Gizmos.color = F.OrangeColor;
GizmosDrawCastedRay (firePos, this.GunFlash.transform.right);
}
// ray based on aiming direction
if (m_ped != null)
{
Gizmos.color = Color.red;
GizmosDrawCastedRay (firePos, m_ped.WeaponHolder.AimDirection);
}
// ray based on firing direction
Gizmos.color = Color.black;
GizmosDrawCastedRay (firePos, this.GetFireDir (WeaponAttackParams.Default));
}
public void GizmosDrawCastedRay (Vector3 source, Vector3 dir)
{
float distance = 100f;
RaycastHit hit;
if (this.ProjectileRaycast (source, dir, out hit, WeaponAttackParams.Default))
{
distance = hit.distance;
}
Gizmos.DrawLine (source, source + dir * distance);
}
}
}