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 LayersToIgnoreWhenRaycasting { get; set; } public List 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 s_weaponTypes = new List (); 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 weaponSoundIndexes = new Dictionary() { {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 s_loadedAudioClips = new Dictionary(); // 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 s_weaponsUsingProjectile = new HashSet() { WeaponId.RocketLauncher, WeaponId.RocketLauncherHS, }; private static Geometry.GeometryParts s_projectileModel; public static Geometry.GeometryParts ProjectileModel { get { if (s_projectileModel != null) return s_projectileModel; F.RunExceptionSafe(LoadProjectileModel); return s_projectileModel; } } private static AudioClip s_projectileSound; public static AudioClip ProjectileSound { get { if (s_projectileSound != null) return s_projectileSound; F.RunExceptionSafe(LoadProjectileAudio); return 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.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 (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.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; } static void LoadProjectileModel() { if (s_projectileModel != null) return; var def = Item.GetDefinition(WeaponId.RocketProjectile); s_projectileModel = Geometry.Load (def.ModelName, def.TextureDictionaryName); } static 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 (); } } 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(); this.AssignGunFlashTransform(); } /// /// 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. /// 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(LoadProjectileModel); F.RunExceptionSafe(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; } /// /// True if weapon doesn't inflict damage instantly, but instead fires a projectile. /// 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), 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 (); 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, hitDirection = fireDir, hitPoint = hit.point, hitNormal = hit.normal, attacker = m_ped, damageType = DamageType.Bullet, }); } } } } 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); } } }