diff --git a/Assets/prefabs/player/Player.prefab b/Assets/prefabs/player/Player.prefab index c069afb..59bb1e6 100644 --- a/Assets/prefabs/player/Player.prefab +++ b/Assets/prefabs/player/Player.prefab @@ -8,11 +8,14 @@ "NetworkMode": 1, "Components": [ { - "__type": "Entity", - "__guid": "751a8182-31bd-4112-baea-c0227c4bbc0c", + "__type": "Ultraneon.PlayerNeon", + "__guid": "98a827c8-40ab-4441-8a5d-5f78d88e626f", + "CaptureTime": 15, + "HealInterval": 5, "health": 0, "isAlive": true, - "maxHealth": 100 + "MaxHealth": 100, + "RespawnDelay": 5 }, { "__type": "PlayerInventory", diff --git a/Assets/prefabs/weapons/weapon_auto.prefab b/Assets/prefabs/weapons/weapon_auto.prefab index 4f54a3a..5e16e2b 100644 --- a/Assets/prefabs/weapons/weapon_auto.prefab +++ b/Assets/prefabs/weapons/weapon_auto.prefab @@ -7,23 +7,41 @@ "Enabled": true, "Components": [ { - "__type": "WeaponBaseNeon", + "__type": "Ultraneon.WeaponBaseNeon", "__guid": "cf2c2bda-0cc9-4d89-9fa8-8c19d6d2aff1", - "clipSize": 30, - "equipTime": 1.5, - "fireRate": 0.08, - "headShotMultiplier": 3, + "CurrentAmmo": 30, + "ClipSize": 30, + "EquipTime": 1.5, + "FireRate": 0.08, + "HeadshotMultiplier": 3, "ImpactPrefab": { "_type": "gameobject", "prefab": "prefabs/effects/weapons/impacteffect.prefab" }, - "isPickedUp": false, - "isReloading": false, - "isSemiAuto": false, - "reloadTime": 3, - "shootSound": "sounds/placeholders/pistol_shoot.sound", - "weaponDamage": 20, - "weaponType": "auto" + "IsPickedUp": false, + "IsSemiAuto": false, + "ReloadTime": 3, + "ShootSound": "sounds/placeholders/pistol_shoot.sound", + "SinceEquipped": { + "Relative": 348.20987 + }, + "SinceShot": { + "Relative": 348.20987 + }, + "Viewmodel": { + "_type": "component", + "component_id": "412b4798-3f36-4a70-9be7-4fec02bee19a", + "go": "4e00321c-fac1-4e38-a817-9a0f24af6c68", + "component_type": "SkinnedModelRenderer" + }, + "ViewmodelArms": { + "_type": "component", + "component_id": "412b4798-3f36-4a70-9be7-4fec02bee19a", + "go": "4e00321c-fac1-4e38-a817-9a0f24af6c68", + "component_type": "SkinnedModelRenderer" + }, + "WeaponDamage": 20, + "WeaponType": "Auto" }, { "__type": "Sandbox.SkinnedModelRenderer", diff --git a/Assets/prefabs/weapons/weapon_bolt.prefab b/Assets/prefabs/weapons/weapon_bolt.prefab index fc97232..71e5947 100644 --- a/Assets/prefabs/weapons/weapon_bolt.prefab +++ b/Assets/prefabs/weapons/weapon_bolt.prefab @@ -7,36 +7,29 @@ "Enabled": true, "Components": [ { - "__type": "WeaponBaseNeon", + "__type": "Ultraneon.WeaponBaseNeon", "__guid": "c1aa54b4-d7fa-4c6c-a5e7-b3ca0c393fe0", - "clipSize": 5, - "equipTime": 0.5, - "fireRate": 4, - "hasShoot": false, - "headShotMultiplier": 4, + "CurrentAmmo": 0, + "ClipSize": 5, + "EquipTime": 0.5, + "FireRate": 4, + "HeadshotMultiplier": 4, "ImpactPrefab": { "_type": "gameobject", "prefab": "prefabs/effects/weapons/impacteffect.prefab" }, - "isPickedUp": false, - "isReloading": false, - "isSemiAuto": true, - "reloadTime": 6, - "shootSound": "sounds/weapons/bolt_shoot_1.sound", - "sinceEquippd": { - "Relative": 4944.8877 - }, - "sinceShot": { - "Relative": 4944.8877 - }, + "IsPickedUp": false, + "IsSemiAuto": true, + "ReloadTime": 6, + "ShootSound": "sounds/weapons/bolt_shoot_1.sound", "Viewmodel": { "_type": "component", "component_id": "0569bd32-86ee-4d14-b3cc-712ee6555520", "go": "a3add265-5ca9-4525-955b-45830c25617e", "component_type": "SkinnedModelRenderer" }, - "weaponDamage": 100, - "weaponType": "bolt", + "WeaponDamage": 100, + "WeaponType": "Bolt", "Worldmodel": { "_type": "component", "component_id": "c427806b-1128-4c06-a279-827c42fa8211", diff --git a/Assets/prefabs/weapons/weapon_pistol.prefab b/Assets/prefabs/weapons/weapon_pistol.prefab index 1b9e583..1ae4db1 100644 --- a/Assets/prefabs/weapons/weapon_pistol.prefab +++ b/Assets/prefabs/weapons/weapon_pistol.prefab @@ -3,34 +3,45 @@ "__guid": "cd2d7e06-735b-4d77-ae08-f48f8e06e45a", "Flags": 0, "Name": "weapon_pistol", - "Position": "0,0,64", "Tags": "trigger", "Enabled": true, "Components": [ { - "__type": "WeaponBaseNeon", + "__type": "Ultraneon.WeaponBaseNeon", "__guid": "a1ae995c-834b-4ce3-8278-6d37b76ee3a3", - "clipSize": 12, - "equipTime": 0.3, - "fireRate": 0.4, - "headShotMultiplier": 10, + "CurrentAmmo": 12, + "ClipSize": 12, + "EquipTime": 0.3, + "FireRate": 0.4, + "HeadshotMultiplier": 10, "ImpactPrefab": { "_type": "gameobject", "prefab": "prefabs/effects/weapons/impacteffect.prefab" }, - "isPickedUp": false, - "isReloading": false, - "isSemiAuto": true, - "reloadTime": 2, - "shootSound": "sounds/weapons/pistol_laser_shoot_1.sound", + "IsPickedUp": false, + "IsSemiAuto": true, + "ReloadTime": 2, + "ShootSound": "sounds/weapons/pistol_laser_shoot_1.sound", + "SinceEquipped": { + "Relative": 347.32962 + }, + "SinceShot": { + "Relative": 347.32962 + }, "Viewmodel": { "_type": "component", "component_id": "b4a4919a-28f1-425e-9644-26a585f5c74d", "go": "cd2d7e06-735b-4d77-ae08-f48f8e06e45a", "component_type": "SkinnedModelRenderer" }, - "weaponDamage": 35, - "weaponType": "pistol", + "ViewmodelArms": { + "_type": "component", + "component_id": "b4a4919a-28f1-425e-9644-26a585f5c74d", + "go": "cd2d7e06-735b-4d77-ae08-f48f8e06e45a", + "component_type": "SkinnedModelRenderer" + }, + "WeaponDamage": 35, + "WeaponType": "Pistol", "Worldmodel": { "_type": "component", "component_id": "6eb81083-303f-425f-9279-68e5523e2cee", diff --git a/Assets/prefabs/weapons/weapon_semi.prefab b/Assets/prefabs/weapons/weapon_semi.prefab index 30815c0..b79ee70 100644 --- a/Assets/prefabs/weapons/weapon_semi.prefab +++ b/Assets/prefabs/weapons/weapon_semi.prefab @@ -7,23 +7,41 @@ "Enabled": true, "Components": [ { - "__type": "WeaponBaseNeon", + "__type": "Ultraneon.WeaponBaseNeon", "__guid": "b147493a-3f49-4eac-9567-7d649d923e5a", - "clipSize": 25, - "equipTime": 1, - "fireRate": 0.2, - "headShotMultiplier": 4, + "CurrentAmmo": 25, + "ClipSize": 25, + "EquipTime": 1, + "FireRate": 0.2, + "HeadshotMultiplier": 4, "ImpactPrefab": { "_type": "gameobject", "prefab": "prefabs/effects/weapons/impacteffect.prefab" }, - "isPickedUp": false, - "isReloading": false, - "isSemiAuto": true, - "reloadTime": 2, - "shootSound": "sounds/placeholders/pistol_shoot.sound", - "weaponDamage": 50, - "weaponType": "semi" + "IsPickedUp": false, + "IsSemiAuto": true, + "ReloadTime": 2, + "ShootSound": "sounds/placeholders/pistol_shoot.sound", + "SinceEquipped": { + "Relative": 486.8176 + }, + "SinceShot": { + "Relative": 486.8176 + }, + "Viewmodel": { + "_type": "component", + "component_id": "b7225ee5-e002-46da-a6da-58b1a5fb2e9f", + "go": "d9b51831-8aa4-4903-aacf-49555d47eda1", + "component_type": "SkinnedModelRenderer" + }, + "ViewmodelArms": { + "_type": "component", + "component_id": "b7225ee5-e002-46da-a6da-58b1a5fb2e9f", + "go": "d9b51831-8aa4-4903-aacf-49555d47eda1", + "component_type": "SkinnedModelRenderer" + }, + "WeaponDamage": 50, + "WeaponType": "Semi" }, { "__type": "Sandbox.SkinnedModelRenderer", diff --git a/Assets/scenes/dev.scene b/Assets/scenes/dev.scene index e3c7c62..4f4151c 100644 --- a/Assets/scenes/dev.scene +++ b/Assets/scenes/dev.scene @@ -236,28 +236,28 @@ "Enabled": true, "Components": [ { - "__type": "Entity", - "__guid": "cf374dc6-08e2-47d8-a2be-b8cd98002ff4", - "health": 0, - "isAlive": true, - "maxHealth": 100 - }, - { - "__type": "EnemyAi", - "__guid": "b0166651-833f-47b4-a42e-f7af2cadd89e", - "agent": { + "__type": "BotAi", + "__guid": "090b4747-b2f5-43f5-a2f2-daf60f1debd0", + "Agent": { "_type": "component", "component_id": "4eec2632-8196-40fb-8daf-296e6517cdff", "go": "e3a6c309-0a49-4905-b357-6091e0c24a0d", "component_type": "NavMeshAgent" }, - "animationHelper": { + "AnimationHelper": { "_type": "component", "component_id": "273f8c95-b5d5-4927-8ebd-5fb6883f749c", "go": "e3a6c309-0a49-4905-b357-6091e0c24a0d", "component_type": "CitizenAnimationHelper" }, - "stopDistance": 255 + "AttackRange": 200, + "CurrentTeam": "Enemy", + "EntityName": "enemy_regular", + "Health": 100, + "Id": "00000000-0000-0000-0000-000000000000", + "isAlive": true, + "MaxHealth": 100, + "StopDistance": 600 }, { "__type": "Sandbox.SkinnedModelRenderer", @@ -354,7 +354,8 @@ { "__type": "SpotAi", "__guid": "220d9c41-3b5d-4c2f-9808-fe33486483dd", - "lastSeenLocation": "0,0,0" + "DetectionInterval": 0.5, + "DetectionRadius": 500 } ] } @@ -364,7 +365,7 @@ "__guid": "7584abe1-ac04-4f25-a3c9-a2d6f29cc86b", "Flags": 0, "Name": "Box", - "Position": "169,-597,61.00001", + "Position": "647.2552,-853.1647,61.00006", "Enabled": true, "Components": [ { @@ -507,22 +508,22 @@ "0.25,0.25" ], "TextureOffset": [ - "348.0011,214.6318", - "348.0011,214.6318", - "214.6318,244", - "297.3682,244.0001", - "163.9996,244", + "482.9836,451.9583", + "482.9836,451.9583", + "451.9583,244.0002", + "60.04175,244.0002", + "29.0188,244.0002", "348,244", - "348.0012,244.0001", - "0,0.0000152588", - "0,0", - "0,0", + "482.9836,244.0002", + "0.6586914,0.0002136231", + "134.9792,511.3413", "0,0", - "0,0.00006103516", - "0,0", - "0,0", - "0,0", - "0,0.00001525879" + "134.9792,511.3413", + "511.3413,0.0002441406", + "134.9792,511.3413", + "377.0208,0.0001831055", + "134.9792,511.3413", + "134.9792,0.000213623" ], "MaterialIndex": [ 0, @@ -610,22 +611,111 @@ ] }, { - "__guid": "04e992a7-d061-4b82-8505-0186af1e59ac", + "__guid": "ff9c3881-ceb5-4b55-9046-cb597e8d4786", "Flags": 0, - "Name": "Object", - "Position": "432,-138,68", + "Name": "Capture Point A", + "Position": "-26.91423,-494.3108,74.56119", "Enabled": true, "Components": [ + { + "__type": "Sandbox.ModelRenderer", + "__guid": "142af44b-896d-4f21-b347-f5d838ea9571", + "BodyGroups": 18446744073709551615, + "Model": "models/dev/box.vmdl", + "RenderType": "On", + "Tint": "1,1,1,1" + }, { "__type": "CaptureZoneEntity", - "__guid": "2df00d8b-025d-4b1e-bcc9-6c715ca39dba", + "__guid": "80c2b359-9700-4704-9e85-a3f83cc8bfb2", "CaptureProgress": 0, - "CaptureRadius": 5, - "CaptureTime": 10, + "CaptureTime": 3, "ControllingTeam": "Neutral", - "Health": 0, - "MaxHealth": 100, - "PointName": "Capture Zone" + "EnemyColor": "0.97255,0.20784,0.41961,1", + "NeutralColor": "0.35294,0.45098,0.5451,1", + "PlayerColor": "0.32941,0.7451,0.20784,1", + "PointName": "A", + "ZoneModel": { + "_type": "component", + "component_id": "142af44b-896d-4f21-b347-f5d838ea9571", + "go": "ff9c3881-ceb5-4b55-9046-cb597e8d4786", + "component_type": "ModelRenderer" + } + }, + { + "__type": "Sandbox.BoxCollider", + "__guid": "1f979f6f-708c-4905-84f9-68d0c52cb779", + "Center": "0,0,0", + "IsTrigger": true, + "Scale": "97.20003,147.7998,87.00007", + "Static": false + } + ] + }, + { + "__guid": "31b1ea49-bbb5-4516-94f9-5b81e6c76fa7", + "Flags": 0, + "Name": "Capture Point B", + "Position": "-451.2886,700.6271,74.56231", + "Enabled": true, + "Components": [ + { + "__type": "Sandbox.ModelRenderer", + "__guid": "01f3292d-324b-4bd9-a43d-d101ccbc122c", + "BodyGroups": 18446744073709551615, + "Model": "models/dev/box.vmdl", + "RenderType": "On", + "Tint": "1,1,1,1" + }, + { + "__type": "CaptureZoneEntity", + "__guid": "dd3295c1-9b2f-4728-b62c-368923cdb77b", + "CaptureProgress": 0, + "CaptureTime": 3, + "ControllingTeam": "Neutral", + "EnemyColor": "0.97255,0.20784,0.41961,1", + "NeutralColor": "0.35294,0.45098,0.5451,1", + "PlayerColor": "0.32941,0.7451,0.20784,1", + "PointName": "B", + "ZoneModel": { + "_type": "component", + "component_id": "01f3292d-324b-4bd9-a43d-d101ccbc122c", + "go": "31b1ea49-bbb5-4516-94f9-5b81e6c76fa7", + "component_type": "ModelRenderer" + } + }, + { + "__type": "Sandbox.BoxCollider", + "__guid": "c034c522-52f5-41dd-bb59-7bfdea118a95", + "Center": "0,0,0", + "IsTrigger": true, + "Scale": "97.20003,147.7998,87.00007", + "Static": false + } + ] + }, + { + "__guid": "f2d1c66d-773f-436b-8490-ad1d8cf4d369", + "Flags": 0, + "Name": "Game Service", + "Position": "-827.2766,-254.7903,301.302", + "Enabled": true, + "Components": [ + { + "__type": "Ultraneon.GameService", + "__guid": "ba0f7a02-c3b4-410d-8822-23f69915f107", + "CaptureZones": [ + { + "_type": "component", + "component_id": "80c2b359-9700-4704-9e85-a3f83cc8bfb2", + "go": "ff9c3881-ceb5-4b55-9046-cb597e8d4786", + "component_type": "CaptureZoneEntity" + } + ], + "RoundTime": 600, + "ScoreCaptureZone": 100, + "ScoreLoseZone": 50, + "ScoreToWin": 1000 } ] } diff --git a/code/entity/BaseNeonCharacterEntity.cs b/code/entity/BaseNeonCharacterEntity.cs new file mode 100644 index 0000000..6252a28 --- /dev/null +++ b/code/entity/BaseNeonCharacterEntity.cs @@ -0,0 +1,70 @@ +using Sandbox; +using Sandbox.Citizen; +using Sandbox.Diagnostics; +using System; +using Sandbox.Events; +using Ultraneon; +using Ultraneon.Events; + +[Category( "Ultraneon" )] +[Icon( "settings_accessibility" )] +public class BaseNeonCharacterEntity : Entity, Component.INetworkListener +{ + [Property, ReadOnly] + public float MaxHealth { get; set; } = 100f; + + [Property, HostSync, Change( nameof(OnDamage) )] + public float Health { get; set; } + + [Property, ReadOnly] + public bool isAlive { get; private set; } = true; + + [Property] + public Team CurrentTeam { get; set; } = Team.Neutral; + + public void OnDamage( DamageInfo damage ) + { + if ( !isAlive ) return; + + Health = Math.Clamp( Health - damage.Damage, 0f, MaxHealth ); + + if ( Health <= 0 ) KillCharacter( damage.Attacker ); + Log.Info( $"{EntityName} took {damage.Damage} damage from {damage.Attacker?.Name ?? "unknown"}" ); + } + + protected override void OnEnabled() + { + base.OnEnabled(); + Health = MaxHealth; + } + + [Button( "Kill Character" )] + public void KillCharacter( GameObject attacker = null ) + { + Health = 0f; + isAlive = false; + BecomeRagdoll(); + + var baseNeonCharacterEntity = Components.Get(); + if ( baseNeonCharacterEntity != null ) + { + var killer = attacker?.Components.Get(); + // bool isStylishKill = IsStylishKill( killer ); todo + GameObject.Dispatch( new CharacterDeathEvent( this, killer, false ) ); + } + } + + void BecomeRagdoll() + { + var collider = GameObject.Components.Get(); + collider.Enabled = false; + var ragdoll = GameObject.Components.Get( true ); + ragdoll.Enabled = true; + } + + private bool IsStylishKill( BaseNeonCharacterEntity killer ) + { + // TODO: Implement logic for determining if it's a stylish kill (airborne, wallbang) + return false; + } +} diff --git a/code/entity/CaptureZoneEntity.cs b/code/entity/CaptureZoneEntity.cs deleted file mode 100644 index f7f0f8c..0000000 --- a/code/entity/CaptureZoneEntity.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; - -[Category( "Ultraneon" )] -[Icon( "place" )] -public sealed class CaptureZoneEntity : Component, Component.IDamageable, Component.INetworkListener -{ - [Property] - public string PointName { get; set; } = "Capture Zone"; - - [Property] - public float CaptureRadius { get; set; } = 5f; - - [Property] - public float CaptureTime { get; set; } = 10f; - - [Property, HostSync] - public string ControllingTeam { get; set; } = "Neutral"; - - [Property, HostSync] - public float CaptureProgress { get; set; } = 0f; - - [Property, ReadOnly] - public float MaxHealth { get; set; } = 100f; - - [Property, HostSync, Change( nameof(OnHealthChanged) )] - public float Health { get; set; } - - public float RadarX { get; set; } - public float RadarY { get; set; } - - private TimeSince timeSinceLastCapture; - - protected override void OnStart() - { - base.OnStart(); - Health = MaxHealth; - timeSinceLastCapture = 0f; - } - - protected override void OnUpdate() - { - if ( IsProxy ) - { - return; - } - - var nearbyPlayers = Scene.Components.GetAll() - .Where( e => e.GameObject.Tags.Has( "player" ) && e.Transform.Position.Distance( Transform.Position ) <= CaptureRadius ) - .ToList(); - - if ( nearbyPlayers.Any() ) - { - var dominantTeam = nearbyPlayers.GroupBy( p => p.GameObject.Tags.First( t => t == "team1" || t == "team2" ) ) - .OrderByDescending( g => g.Count() ) - .First().Key; - - if ( dominantTeam != ControllingTeam ) - { - CaptureProgress += Time.Delta / CaptureTime; - if ( CaptureProgress >= 1f ) - { - ControllingTeam = dominantTeam; - CaptureProgress = 0f; - OnPointCaptured(); - } - } - else - { - CaptureProgress = Math.Max( 0f, CaptureProgress - Time.Delta / CaptureTime ); - } - - timeSinceLastCapture = 0f; - } - else if ( timeSinceLastCapture > 5f && ControllingTeam != "Neutral" ) - { - CaptureProgress += Time.Delta / CaptureTime; - if ( CaptureProgress >= 1f ) - { - ControllingTeam = "Neutral"; - CaptureProgress = 0f; - OnPointNeutralized(); - } - } - } - - public void OnDamage( in DamageInfo damage ) - { - if ( Health <= 0 ) return; - - Health = Math.Clamp( Health - damage.Damage, 0f, MaxHealth ); - - if ( Health <= 0 ) - { - OnPointDestroyed(); - } - } - - private void OnHealthChanged() - { - } - - private void OnPointCaptured() - { - Log.Info( $"{PointName} has been captured by {ControllingTeam}!" ); - } - - private void OnPointNeutralized() - { - Log.Info( $"{PointName} has been neutralized!" ); - } - - private void OnPointDestroyed() - { - Log.Info( $"{PointName} has been destroyed!" ); - GameObject.Destroy(); - } -} diff --git a/code/entity/EnemyAi.cs b/code/entity/EnemyAi.cs deleted file mode 100644 index c243d96..0000000 --- a/code/entity/EnemyAi.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Sandbox; -using Sandbox.Citizen; - -public sealed class EnemyAi : Component, Component.ITriggerListener -{ - [Property, ToggleGroup( "Nav" )] public GameObject Target { get; set; } - [Property, ToggleGroup( "Nav" )] public NavMeshAgent agent { get; set; } - - [Property, ToggleGroup( "Nav" )] public CitizenAnimationHelper animationHelper { get; set; } - - [Property, ToggleGroup( "Gameplay" )] public float stopDistance { get; set; } - protected override void OnUpdate() - { - if ( !agent.Enabled ) return; - if ( Target is null ) return; - agent.MoveTo( Target.Transform.Position ); - animationHelper.WithVelocity( agent.Velocity ); - //animationHelper.WithLook( Target.Transform.Position, 1,1,1 ); - - if ( Vector3.DistanceBetween( Target.Transform.Position, agent.Transform.Position ) < stopDistance ) - { - agent.Stop(); - } - } - - -} diff --git a/code/entity/Entity.cs b/code/entity/Entity.cs index e859dfb..cb0a245 100644 --- a/code/entity/Entity.cs +++ b/code/entity/Entity.cs @@ -1,54 +1,65 @@ using Sandbox; -using Sandbox.Citizen; -using Sandbox.Diagnostics; using System; -using System.Threading.Channels; -[Category("Ultraneon")] -[Icon( "settings_accessibility" )] -public sealed class Entity : Component, Component.IDamageable, Component.INetworkListener +namespace Ultraneon { - [Property, ReadOnly] private float maxHealth { get; set; } = 100f; - [Property,ReadOnly] public bool isAlive { get;private set; } = true; - [Property,HostSync, Change( nameof( OnDamage ) )] public float health { get; set; } - - public void OnDamage( in DamageInfo damage ) + public class Entity : Component, Component.IDamageable { - if ( !isAlive ) return; + [Property] + public Guid Id { get; private set; } + [Property] + public string EntityName { get; set; } + public bool IsActive { get; private set; } = true; - health = Math.Clamp(health - damage.Damage, 0f, maxHealth); + protected override void OnAwake() + { + base.OnAwake(); + Id = Guid.NewGuid(); + } - if ( health <= 0 ) killEntity(); - Log.Info( damage.Attacker ); + protected override void OnStart() + { + base.OnStart(); + if ( string.IsNullOrEmpty( EntityName ) ) + { + EntityName = GetType().Name; + } + } - } + public virtual void Activate() + { + IsActive = true; + } - protected override void OnEnabled() - { - health = maxHealth; - } - [Button( "kill player" )] - public void killEntity() - { - health = 0f; - isAlive = false; - becomeRagdoll(); - } + public virtual void Deactivate() + { + IsActive = false; + } - void becomeRagdoll() - { - var collider = GameObject.Components.Get(); - collider.Enabled = false; - var ragdoll = GameObject.Components.Get(true); - ragdoll.Enabled = true; + public virtual void OnCollision( Entity other ) + { + } - if ( GameObject.Tags.Has( "bot" ) ) + public void OnDamage( in DamageInfo damageInfo ) { - GameObject.Components.Get().Enabled = false; - GameObject.Tags.Add( "debris" ); + return; + } + + public virtual void Destroy() + { + GameObject.Destroy(); } - } + public float DistanceTo( Entity other ) + { + return Vector3.DistanceBetween( Transform.Position, other.Transform.Position ); + } + + public Vector3 DirectionTo( Entity other ) + { + return (other.Transform.Position - Transform.Position).Normal; + } + } } diff --git a/code/entity/SpotAi.cs b/code/entity/SpotAi.cs deleted file mode 100644 index 919f361..0000000 --- a/code/entity/SpotAi.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Sandbox; -using System; - -public sealed class SpotAi : Component, Component.ITriggerListener -{ - [Property, ReadOnly] public GameObject player { get; set; } - [Property, ReadOnly] public Vector3 lastSeenLocation { get; set; } - public void OnTriggerEnter( Collider other ) - { - if ( other.GameObject.Name == "player" ) - { - player = other.GameObject; - } - - } - - public void OnTriggerExit( Collider other ) - { - player = null; - } - - protected override void OnFixedUpdate() - { - base.OnFixedUpdate(); - - if ( player != null ) - { - var spotTrace = Scene.Trace.Ray( Transform.Position, player.Transform.Position ) - .Run(); - DebugOverlay.Trace( spotTrace ); - if ( spotTrace.Hit && spotTrace.GameObject.Tags.Has( "player" ) ) - { - GameObject.Parent.Components.Get().Target = spotTrace.GameObject; - } - } - } -} diff --git a/code/framework/PlayerSpawner.cs b/code/framework/PlayerSpawner.cs index a416e3d..b461707 100644 --- a/code/framework/PlayerSpawner.cs +++ b/code/framework/PlayerSpawner.cs @@ -9,7 +9,8 @@ public sealed class PlayerSpawner : Component /// /// The prefab to spawn for the player to control. /// - [Property] public GameObject PlayerPrefab { get; set; } + [Property] + public GameObject PlayerPrefab { get; set; } protected override void OnEnabled() { @@ -19,5 +20,4 @@ protected override void OnEnabled() var startLocation = Scene.GetAllComponents().FirstOrDefault().Transform.World; PlayerPrefab.Clone( startLocation ); } - } diff --git a/code/game/CaptureZoneEntity.cs b/code/game/CaptureZoneEntity.cs new file mode 100644 index 0000000..bf7ada3 --- /dev/null +++ b/code/game/CaptureZoneEntity.cs @@ -0,0 +1,164 @@ +using Sandbox; +using System; +using System.Collections.Generic; +using System.Linq; +using Sandbox.Events; +using Ultraneon; +using Ultraneon.Events; + +[Category( "Ultraneon" )] +[Icon( "place" )] +public sealed class CaptureZoneEntity : Component, Component.ITriggerListener +{ + [Property] + public string PointName { get; set; } = "Capture Zone"; + + [Property] + public Color NeutralColor { get; set; } + + [Property] + public Color PlayerColor { get; set; } + + [Property] + public Color EnemyColor { get; set; } + + [Property] + public float CaptureTime { get; set; } = 15f; + + [Property, HostSync] + public Team ControllingTeam { get; set; } = Team.Neutral; + + [Property, HostSync] + public float CaptureProgress { get; set; } = 0f; + + [Property] + public ModelRenderer ZoneModel { get; set; } + + public float MinimapX { get; set; } + public float MinimapY { get; set; } + public Team PreviousTeam { get; set; } + public bool HasChanged { get; set; } + + private TimeSince timeSinceLastCapture; + private HashSet charactersInZone = new(); + + protected override void OnStart() + { + base.OnStart(); + timeSinceLastCapture = 0f; + UpdateZoneVisuals(); + } + + protected override void OnUpdate() + { + if ( IsProxy ) + return; + + UpdateCapture(); + UpdateZoneVisuals(); + } + + private void UpdateCapture() + { + if ( charactersInZone.Any() ) + { + var dominantTeam = charactersInZone + .GroupBy( p => p.CurrentTeam ) + .OrderByDescending( g => g.Count() ) + .First().Key; + + if ( dominantTeam != ControllingTeam ) + { + CaptureProgress += Time.Delta / CaptureTime * charactersInZone.Count( p => p.CurrentTeam == dominantTeam ); + if ( CaptureProgress >= 1f ) + { + var previousTeam = ControllingTeam; + ControllingTeam = dominantTeam; + CaptureProgress = 0f; + OnPointCaptured( previousTeam ); + } + } + else + { + CaptureProgress = Math.Max( 0f, CaptureProgress - Time.Delta / CaptureTime ); + } + + timeSinceLastCapture = 0f; + } + else if ( timeSinceLastCapture > 5f && ControllingTeam != Team.Neutral ) + { + CaptureProgress += Time.Delta / CaptureTime; + if ( CaptureProgress >= 1f ) + { + var previousTeam = ControllingTeam; + ControllingTeam = Team.Neutral; + CaptureProgress = 0f; + OnPointNeutralized( previousTeam ); + } + } + } + + private void UpdateZoneVisuals() + { + if ( ZoneModel != null ) + { + Color teamColor = ControllingTeam switch + { + Team.Player => PlayerColor, + Team.Enemy => EnemyColor, + _ => NeutralColor + }; + + ZoneModel.Tint = teamColor; + + // TODO: Add particle effects or other visual indicators for capture progress + } + } + + public void OnTriggerEnter( Collider other ) + { + var character = other.GameObject.Components.Get(); + if ( character != null ) + { + charactersInZone.Add( character ); + if ( charactersInZone.Count == 1 ) + { + OnStartCapture(); + } + } + } + + public void OnTriggerExit( Collider other ) + { + var player = other.GameObject.Components.Get(); + if ( player != null ) + { + charactersInZone.Remove( player ); + } + } + + private void OnStartCapture() + { + Log.Info( $"{PointName} is being captured!" ); + // TODO: Send a lightwave in radius to alert other players + } + + private void OnPointCaptured( Team previousTeam ) + { + Log.Info( $"{PointName} has been captured by {ControllingTeam}!" ); + PreviousTeam = previousTeam; + GameObject.Dispatch( new CaptureZoneEvent( PointName, previousTeam, ControllingTeam ) ); + } + + private void OnPointNeutralized( Team previousTeam ) + { + Log.Info( $"{PointName} has been neutralized!" ); + PreviousTeam = previousTeam; + GameObject.Dispatch( new CaptureZoneEvent( PointName, previousTeam, Team.Neutral ) ); + } + + public bool IsPlayerInZone( PlayerNeon player ) + { + return charactersInZone.Contains( player ); + } +} diff --git a/code/game/Events.cs b/code/game/Events.cs new file mode 100644 index 0000000..4f9c030 --- /dev/null +++ b/code/game/Events.cs @@ -0,0 +1,18 @@ +using Sandbox.Events; + +namespace Ultraneon.Events; + +// Weapons (used to update ui) +public record WeaponStateChangedEvent( WeaponBaseNeon Weapon ) : IGameEvent; + +public record ActiveWeaponChangedEvent( WeaponBaseNeon OldWeapon, WeaponBaseNeon NewWeapon ) : IGameEvent; + +// Zone Capture Stuff +public record CaptureZoneEvent( string ZoneName, Team PreviousTeam, Team NewTeam ) : IGameEvent; + +// Game Events +public record PlayerSpawnEvent( Team Team ) : IGameEvent; + +public record DamageEvent( BaseNeonCharacterEntity Target, Entity Attacker, float Damage, Vector3 Position ) : IGameEvent; + +public record CharacterDeathEvent( BaseNeonCharacterEntity Victim, Entity Killer, bool IsStylish ) : IGameEvent; diff --git a/code/game/GameService.cs b/code/game/GameService.cs new file mode 100644 index 0000000..8f80626 --- /dev/null +++ b/code/game/GameService.cs @@ -0,0 +1,164 @@ +using Sandbox; +using System; +using System.Collections.Generic; +using System.Linq; +using Sandbox.Events; +using Ultraneon.Events; + +namespace Ultraneon; + +public class GameService : Component, + IGameEventHandler, + IGameEventHandler, + IGameEventHandler, + IGameEventHandler +{ + [Property] + public float RoundTime { get; set; } = 600f; // 10 min + + [Property] + public int ScoreToWin { get; set; } = 1000; + + [Property] + public List CaptureZones { get; set; } = new(); + + [Property] + public int ScoreCaptureZone { get; set; } = 100; + + [Property] + public int ScoreLoseZone { get; set; } = 50; + + private TimeSince roundStartTime; + private Dictionary teamScores = new(); + private bool gameFinished = false; + + protected override void OnStart() + { + if ( IsProxy ) return; + + InitializeGame(); + } + + protected override void OnUpdate() + { + if ( IsProxy ) return; + + UpdateGameState(); + } + + private void InitializeGame() + { + roundStartTime = 0f; + teamScores[Team.Player] = 0; + teamScores[Team.Enemy] = 0; + + // Initialize capture zones + CaptureZones = Scene.GetAllComponents().ToList(); + foreach ( var zone in CaptureZones ) + { + zone.ControllingTeam = Team.Neutral; + zone.CaptureProgress = 0f; + } + + // Spawn players + SpawnPlayers(); + } + + private void UpdateGameState() + { + if ( roundStartTime >= RoundTime ) + { + EndGame(); + return; + } + + UpdateScores(); + + if ( teamScores[Team.Player] >= ScoreToWin || teamScores[Team.Enemy] >= ScoreToWin ) + { + if ( !gameFinished ) + { + EndGame(); + } + } + } + + private void UpdateScores() + { + foreach ( var zone in CaptureZones ) + { + if ( zone.ControllingTeam != Team.Neutral ) + { + teamScores[zone.ControllingTeam]++; + } + } + } + + private void SpawnPlayers() + { + // TODO: Implement player spawning logic + GameObject.Dispatch( new PlayerSpawnEvent( Team.Player ) ); + GameObject.Dispatch( new PlayerSpawnEvent( Team.Enemy ) ); + } + + private void EndGame() + { + var winningTeam = teamScores[Team.Player] > teamScores[Team.Enemy] ? Team.Player : Team.Enemy; + Log.Info( $"Game Over! {winningTeam} wins with a score of {teamScores[winningTeam]}" ); + // TODO: Implement game end logic (show results, restart, etc.) + gameFinished = true; + } + + public void OnGameEvent( DamageEvent eventArgs ) + { + if ( eventArgs.Target is BaseNeonCharacterEntity targetEntity ) + { + targetEntity.Health -= eventArgs.Damage; + Log.Info( + $"{targetEntity.EntityName} took {eventArgs.Damage} damage from {eventArgs.Attacker?.EntityName ?? "unknown"}. Remaining health: {targetEntity.Health}" ); + + if ( targetEntity.Health <= 0 ) + { + var killerEntity = eventArgs.Attacker?.Components.Get(); + GameObject.Dispatch( new CharacterDeathEvent( targetEntity, killerEntity, true ) ); + } + } + } + + public void OnGameEvent( CaptureZoneEvent eventArgs ) + { + if ( eventArgs.PreviousTeam != Team.Neutral ) + { + teamScores[eventArgs.PreviousTeam] += ScoreLoseZone; // Lose points for losing a zone + } + + if ( eventArgs.NewTeam != Team.Neutral ) + { + teamScores[eventArgs.PreviousTeam] += ScoreCaptureZone; // Lose points for losing a zone + } + + Log.Info( + $"Zone {eventArgs.ZoneName} captured by {eventArgs.NewTeam}. New scores - Player: {teamScores[Team.Player]}, Enemy: {teamScores[Team.Enemy]}" ); + } + + public void OnGameEvent( PlayerSpawnEvent eventArgs ) + { + // TODO: Implement player spawning logic + Log.Info( $"Player spawned for team {eventArgs.Team}" ); + } + + public void OnGameEvent( CharacterDeathEvent eventArgs ) + { + if ( eventArgs.Killer is not BaseNeonCharacterEntity killer ) + { + return; + } + + if ( killer.CurrentTeam != eventArgs.Victim.CurrentTeam ) + { + var scoreAward = eventArgs.IsStylish ? 40 : 10; + teamScores[killer.CurrentTeam] += scoreAward; + Log.Info( $"{killer.CurrentTeam} scored {scoreAward} points for a kill" ); + } + } +} diff --git a/code/game/Team.cs b/code/game/Team.cs new file mode 100644 index 0000000..7944223 --- /dev/null +++ b/code/game/Team.cs @@ -0,0 +1,26 @@ +namespace Ultraneon; + +public enum Team +{ + Neutral = 0, + Player = 1, + Enemy = 2 +} + +public static class Teams +{ + public static Team GetTeamFromTag( string tag = null ) + { + if ( tag == null ) + { + return Team.Neutral; + } + + return tag switch + { + "player" => Team.Player, + "bot" => Team.Enemy, + _ => Team.Neutral + }; + } +} diff --git a/code/npc/BotAi.cs b/code/npc/BotAi.cs new file mode 100644 index 0000000..2ba2c1c --- /dev/null +++ b/code/npc/BotAi.cs @@ -0,0 +1,105 @@ +using Sandbox; +using Sandbox.Citizen; +using Ultraneon; + +public sealed class BotAi : BaseNeonCharacterEntity +{ + [Property] + public NavMeshAgent Agent { get; set; } + + [Property] + public CitizenAnimationHelper AnimationHelper { get; set; } + + [Property] + public float StopDistance { get; set; } = 100f; + + [Property] + public float AttackRange { get; set; } = 200f; + + private PlayerNeon CurrentTarget { get; set; } + + protected override void OnStart() + { + base.OnStart(); + + if ( Agent == null ) + { + Agent = Components.GetOrCreate(); + } + + if ( AnimationHelper == null ) + { + AnimationHelper = Components.GetOrCreate(); + } + } + + protected override void OnUpdate() + { + base.OnUpdate(); + + if ( !isAlive ) return; + + if ( CurrentTarget != null && CurrentTarget.IsDead ) + { + CurrentTarget = null; + } + + if ( CurrentTarget != null ) + { + UpdateMovement(); + UpdateCombat(); + } + else + { + Agent.Stop(); + } + + UpdateAnimation(); + } + + private void UpdateMovement() + { + float distanceToTarget = Vector3.DistanceBetween( Transform.Position, CurrentTarget.Transform.Position ); + + if ( distanceToTarget > StopDistance ) + { + Agent.MoveTo( CurrentTarget.Transform.Position ); + } + else + { + Agent.Stop(); + } + } + + private void UpdateCombat() + { + float distanceToTarget = Vector3.DistanceBetween( Transform.Position, CurrentTarget.Transform.Position ); + + // if ( distanceToTarget <= AttackRange ) + // { + // Log.Info( $"BotAi {EntityName} is attacking {CurrentTarget.EntityName}" ); + // } + } + + private void UpdateAnimation() + { + if ( AnimationHelper != null ) + { + AnimationHelper.WithVelocity( Agent.Velocity ); + + if ( CurrentTarget != null ) + { + AnimationHelper.WithLook( CurrentTarget.Transform.Position - Transform.Position ); + } + } + } + + public void SetTarget( PlayerNeon newTarget ) + { + if ( newTarget != null && newTarget != CurrentTarget && newTarget.isAlive ) + { + CurrentTarget = newTarget; + Log.Info( $"BotAi {EntityName} is now targeting {CurrentTarget.EntityName}" ); + } + } +} diff --git a/code/npc/SpotAi.cs b/code/npc/SpotAi.cs new file mode 100644 index 0000000..db0f7da --- /dev/null +++ b/code/npc/SpotAi.cs @@ -0,0 +1,71 @@ +using Sandbox; +using Ultraneon; + +public sealed class SpotAi : Component +{ + [Property] + public float DetectionRadius { get; set; } = 500f; + + [Property] + public float DetectionInterval { get; set; } = 0.5f; + + private BotAi ParentBot { get; set; } + private TimeSince TimeSinceLastDetection { get; set; } + + protected override void OnStart() + { + base.OnStart(); + ParentBot = GameObject.Parent?.Components.Get(); + + if ( ParentBot == null ) + { + Log.Warning( $"SpotAi on {GameObject.Name} could not find a parent BotAi component." ); + } + } + + protected override void OnUpdate() + { + base.OnUpdate(); + + if ( ParentBot == null || !ParentBot.isAlive ) return; + + if ( TimeSinceLastDetection >= DetectionInterval ) + { + DetectPlayers(); + TimeSinceLastDetection = 0; + } + } + + private void DetectPlayers() + { + var nearbyEntities = Scene.GetAllComponents(); + + foreach ( var player in nearbyEntities ) + { + if ( player.isAlive && player.CurrentTeam != ParentBot.CurrentTeam ) + { + float distance = Vector3.DistanceBetween( Transform.Position, player.Transform.Position ); + + if ( distance <= DetectionRadius ) + { + if ( IsPlayerVisible( player ) ) + { + ParentBot.SetTarget( player ); + return; + } + } + } + } + } + + private bool IsPlayerVisible( PlayerNeon player ) + { + var trace = Scene.Trace.Ray( Transform.Position, player.Transform.Position ) + .WithoutTags( "bot" ) + .Run(); + + DebugOverlay.Trace( trace ); + + return trace.Hit && trace.GameObject.Components.Get() == player; + } +} diff --git a/code/player/PlayerInventory.cs b/code/player/PlayerInventory.cs index f00797d..29b4e11 100644 --- a/code/player/PlayerInventory.cs +++ b/code/player/PlayerInventory.cs @@ -1,78 +1,138 @@ using Sandbox; -using Sandbox.UI; using System; -using System.ComponentModel.DataAnnotations; +using System.Linq; +using Sandbox.Events; +using Ultraneon.Events; -[Category( "Ultraneon" )] -[Icon( "backpack" )] - -public sealed class PlayerInventory : Component +namespace Ultraneon { - [Property,Sync] public WeaponBaseNeon activeWeapon { get; set; } + [Category( "Ultraneon" )] + [Icon( "backpack" )] + public sealed class PlayerInventory : Component + { + [Property, Sync] + public WeaponBaseNeon ActiveWeapon { get; private set; } - [Property,Sync] public WeaponBaseNeon[] weapons { get; set; } = new WeaponBaseNeon[4]; + [Property, Sync] + public WeaponBaseNeon[] Weapons { get; private set; } = new WeaponBaseNeon[MaxWeapons]; - [Property,Sync] public int SelectedSlot { get; set; } = 0; + [Property, Sync] + public int SelectedSlot { get; private set; } = 5; + private const int MaxWeapons = 4; - protected override void OnFixedUpdate() - { - base.OnFixedUpdate(); + protected override void OnFixedUpdate() + { + if ( IsProxy ) return; + + HandleWeaponSelection(); + HandleWeaponFiring(); + HandleWeaponReload(); + } + + private void HandleWeaponSelection() + { + for ( int i = 0; i < MaxWeapons; i++ ) + { + if ( Input.Pressed( $"Slot{i + 1}" ) ) + { + SetActive( Weapons[i] ); + return; + } + } + + if ( Input.Pressed( "Slot5" ) ) SetActive( null ); + HandleWeaponScroll(); + } + private void HandleWeaponScroll() + { + if ( Input.MouseWheel.Length == 0 || Weapons.Count( x => x != null ) <= 1 ) return; + int delta = Math.Sign( Input.MouseWheel.y ); + int newSlot = SelectedSlot; - if ( Input.Pressed( "Slot1" )) SetActive( weapons[0] ); - if ( Input.Pressed( "Slot2" ) ) SetActive( weapons[1] ); - if ( Input.Pressed( "Slot3" ) ) SetActive(weapons[2]); - if ( Input.Pressed( "Slot4" ) ) SetActive(weapons[3]); - if ( Input.Pressed( "Slot5" ) ) SetActive( null ); + do + { + newSlot = (newSlot - delta + MaxWeapons) % MaxWeapons; + } while ( Weapons[newSlot] == null && newSlot != SelectedSlot ); - if ( Input.Down( "attack1" ) ) activeWeapon?.Shoot(); + if ( newSlot != SelectedSlot ) + { + SetActive( Weapons[newSlot] ); + } + } - if ( Input.MouseWheel.Length != 0) + private void HandleWeaponFiring() { - var lastSlot = SelectedSlot; - if ( weapons.Count( x => x != null ) <= 1 ) return; - - - var delta = (int)Input.MouseWheel.y; - - SelectedSlot -= delta; - SelectedSlot = Math.Clamp(SelectedSlot,0,weapons.Length - 1 ); - if ( SelectedSlot == lastSlot ) return; - - - for ( int i = SelectedSlot; i >=0 && i < weapons.Length;i -= delta ) + if ( Input.Pressed( "attack1" ) ) { - if ( weapons[i] != null ) - { - SelectedSlot = i; - SetActive(weapons[i]); - break; - } - else - { - SelectedSlot = lastSlot; + ActiveWeapon?.Shoot(); + } + else if ( Input.Down( "attack1" ) && ActiveWeapon != null && !ActiveWeapon.IsSemiAuto ) + { + ActiveWeapon.Shoot(); + } + } - } - + private void HandleWeaponReload() + { + if ( Input.Pressed( "reload" ) ) + { + ActiveWeapon?.StartReload(); } - } - } - + public void SetActive( WeaponBaseNeon weapon ) + { + if ( weapon == ActiveWeapon ) return; - public void SetActive( WeaponBaseNeon weapon ) - { - if ( weapon is null ) return; - if ( weapon == activeWeapon ) return; + Log.Info( $"Switching weapon from {ActiveWeapon?.GameObject.Name ?? "None"} to {weapon?.GameObject.Name ?? "None"}" ); - SelectedSlot = (int)weapon.weaponType; + var oldWeapon = ActiveWeapon; + oldWeapon?.Holster(); + ActiveWeapon = weapon; - activeWeapon = weapon; - activeWeapon?.Equip(); - } + GameObject.Dispatch( new ActiveWeaponChangedEvent( oldWeapon, ActiveWeapon ) ); + + if ( ActiveWeapon == null ) + { + SelectedSlot = -1; + Log.Info( "No active weapon. SelectedSlot set to -1" ); + return; + } + + SelectedSlot = Array.IndexOf( Weapons, ActiveWeapon ); + ActiveWeapon.Equip(); + + Log.Info( + $"New active weapon: {ActiveWeapon.GameObject.Name}, SelectedSlot: {SelectedSlot}, Ammo: {ActiveWeapon.CurrentAmmo}/{ActiveWeapon.ClipSize}" ); + } + public bool AddWeapon( WeaponBaseNeon weapon ) + { + int slot = (int)weapon.WeaponType; + if ( Weapons[slot] == null ) + { + Weapons[slot] = weapon; + return true; + } + + return false; + } + + public void RemoveWeapon( WeaponBaseNeon weapon ) + { + int index = Array.IndexOf( Weapons, weapon ); + if ( index != -1 ) + { + Weapons[index] = null; + if ( ActiveWeapon == weapon ) + { + SetActive( Weapons.FirstOrDefault( w => w != null ) ); + } + } + } + } } diff --git a/code/player/PlayerNeon.cs b/code/player/PlayerNeon.cs new file mode 100644 index 0000000..60ac212 --- /dev/null +++ b/code/player/PlayerNeon.cs @@ -0,0 +1,186 @@ +using Sandbox; +using System; +using System.Threading.Tasks; +using Sandbox.Events; +using Ultraneon.Events; + +namespace Ultraneon +{ + public class PlayerNeon : BaseNeonCharacterEntity + { + [Property] + public float CaptureTime { get; set; } = 15f; + + [Sync] + public float CurrentCaptureProgress { get; private set; } = 0f; + + [Property] + public float HealInterval { get; set; } = 5f; + + [Property] + public float HealAmount { get; set; } = 10f; + + [Property] + public float RespawnDelay { get; set; } = 5f; + + private TimeSince TimeSinceLastHeal { get; set; } + private TimeSince TimeSinceDeath { get; set; } + + [RequireComponent] + public PlayerInventory Inventory { get; private set; } + + public bool IsDead => Health <= 0; + + protected override void OnStart() + { + base.OnStart(); + if ( IsProxy ) return; + + CurrentTeam = Team.Player; + Health = MaxHealth; + Inventory = Components.Get(); + } + + protected override void OnUpdate() + { + if ( IsProxy || IsDead ) return; + + HandleInput(); + TryHeal(); + UpdateCapture(); + } + + public new void OnDamage( DamageInfo info ) + { + if ( IsProxy || IsDead ) return; + + if ( info.Attacker.Components.Get() is { } entity ) + { + GameObject.Dispatch( new DamageEvent( this, entity, info.Damage, info.Position ) ); + } + } + + private void HandleInput() + { + // TODO: Implement input handling + } + + private void TryHeal() + { + if ( TimeSinceLastHeal >= HealInterval && IsInCapturedZone() ) + { + Health = Math.Min( Health + HealAmount, MaxHealth ); + TimeSinceLastHeal = 0; + } + } + + private void UpdateCapture() + { + if ( IsInCaptureZone() ) + { + CurrentCaptureProgress += Time.Delta; + if ( CurrentCaptureProgress >= CaptureTime ) + { + CompleteCaptureZone(); + } + } + else + { + CurrentCaptureProgress = Math.Max( 0, CurrentCaptureProgress - Time.Delta ); + } + } + + private bool IsInCapturedZone() + { + // TODO: Implement logic to check if the player is in a captured zone + return false; + } + + private bool IsInCaptureZone() + { + // TODO: Implement logic to check if the player is in a capturable zone + return false; + } + + private void CompleteCaptureZone() + { + // TODO: Implement zone capture logic + } + + public void TakeDamage( DamageInfo info ) + { + if ( IsProxy || IsDead ) return; + + float damageAmount = CalculateDamage( info ); + Health -= damageAmount; + + if ( Health <= 0 ) + { + Die( info.Attacker.Components.Get() ); + } + } + + private float CalculateDamage( DamageInfo info ) + { + float damage = info.Damage; + + // TODO: Wallbang hit reduction + // if (info.Flags.HasFlag(DamageFlags.Wallbang)) + // { + // damage *= 0.5f; + // } + + return damage; + } + + private void Die( PlayerNeon killer ) + { + bool isStylishKill = IsStylishKill( killer ); + GameObject.Dispatch( new CharacterDeathEvent( this, killer, isStylishKill ) ); + + if ( HasCapturedZone() ) + { + TimeSinceDeath = 0; + EnableRespawnState(); + } + else + { + HandlePermanentDeath(); + } + } + + private bool HasCapturedZone() + { + // TODO: Implement logic to check if the player has any captured zones + return false; + } + + private void EnableRespawnState() + { + // TODO: Disable player controls and show respawn UI + } + + private void HandlePermanentDeath() + { + // TODO: Implement permanent death logic and show game over UI + } + + private bool IsStylishKill( PlayerNeon killer ) + { + // TODO: Implement logic for determining if it's a stylish kill (airborne, wallbang) + return false; + } + + public void Respawn( Vector3 position ) + { + Health = MaxHealth; + Transform.Position = position; + EnablePlayerControls(); + } + + private void EnablePlayerControls() + { + // TODO: Re-enable player controls after respawn + } + } +} diff --git a/code/ui/AmmoPanel.razor b/code/ui/AmmoPanel.razor index ea432da..1218811 100644 --- a/code/ui/AmmoPanel.razor +++ b/code/ui/AmmoPanel.razor @@ -4,8 +4,8 @@ @attribute [StyleSheet] -
-
@CurrentAmmo
+
+
@CurrentAmmo
/
@MaxAmmo
@@ -14,41 +14,42 @@ @code { [Property] - public PlayerInventory Inventory { get; set; } + public WeaponBaseNeon Weapon { get; set; } - private WeaponBaseNeon lastWeapon; public int CurrentAmmo { get; set; } public int MaxAmmo { get; set; } + public bool IsReloading { get; set; } + public bool IsLowWarning { get; set; } + public bool IsEmpty { get; set; } - protected override void OnAfterTreeRender( bool firstTime ) + public override void Tick() { - base.OnAfterTreeRender( firstTime ); - - if ( firstTime ) + if ( Weapon == null ) { - lastWeapon = Inventory.activeWeapon; + CurrentAmmo = -1; + MaxAmmo = -1; + return; } + + CurrentAmmo = Weapon.CurrentAmmo; + MaxAmmo = Weapon.ClipSize; + IsReloading = Weapon.IsReloading; + IsEmpty = Weapon.CurrentAmmo <= 0; + IsLowWarning = CurrentAmmo <= MaxAmmo / 3; + + StateHasChanged(); // FIXME: HACK: We shouldn't need to call this manually. We are consuming one state change too late for some reason } - public override void Tick() + protected override int BuildHash() { - if ( Inventory.activeWeapon == null ) - { - lastWeapon = null; - } - else - { - lastWeapon = Inventory.activeWeapon; - } - - if (lastWeapon == null) - { - CurrentAmmo = 0; - MaxAmmo = 0; - return; - } - - CurrentAmmo = 0; // TODO: lastWeapon.currentAmmo - MaxAmmo = lastWeapon.clipSize; // TODO: lastWeapon.maxAmmo + if ( !Weapon.IsValid() ) return 0; + + var hash = System.HashCode.Combine( + Weapon.CurrentAmmo, + Weapon.ClipSize, + Weapon.WeaponType + ); + + return hash; } } diff --git a/code/ui/AmmoPanel.razor.scss b/code/ui/AmmoPanel.razor.scss new file mode 100644 index 0000000..e6eb56e --- /dev/null +++ b/code/ui/AmmoPanel.razor.scss @@ -0,0 +1,39 @@ +AmmoPanel { + .ammo-display { + position: absolute; + right: 32px; + bottom: 32px; + gap: 10px; + background-color: #333; + opacity: 0.8; + border-radius: 3px; + padding: 10px 20px; + color: #3498db; + font-weight: 400; + font-size: 50px; + font-family: Cascadia Code; + justify-content: center; + align-items: center; + backdrop-filter: blur(2px); + transition: background-color 0.2s; + + .low-warning { + color: #efac1a; + } + + .empty { + color: #db3442; + } + + &.reloading { + .current-ammo { + opacity: 0.1; + } + + background-color: #0004; + opacity: 0.7; + backdrop-filter: blur(16px); + transition: background-color 0.2s; + } + } +} \ No newline at end of file diff --git a/code/ui/InfoFeedPanel.razor b/code/ui/InfoFeedPanel.razor new file mode 100644 index 0000000..e69de29 diff --git a/code/ui/InfoFeedPanel.razor.scss b/code/ui/InfoFeedPanel.razor.scss new file mode 100644 index 0000000..e69de29 diff --git a/code/ui/InventoryPanel.razor b/code/ui/InventoryPanel.razor index 788f415..e8cfcda 100644 --- a/code/ui/InventoryPanel.razor +++ b/code/ui/InventoryPanel.razor @@ -2,23 +2,36 @@ @using Sandbox.UI @inherits Panel @namespace Ultraneon.UI +@attribute [StyleSheet] - - @for ( int i = 0; i < MAX_INVENTORY_SLOTS; i++ ) - { - int slotIndex = i; -
-
@( slotIndex + 1 )
- @if ( Inventory?.weapons != null && Inventory.weapons.Length > slotIndex && Inventory.weapons[slotIndex] != null ) - { -
@Inventory.weapons[slotIndex].GameObject.Name
- } - else + +
+
+ @for ( int i = 0; i < MAX_INVENTORY_SLOTS; i++ ) { -
Empty
+ int slotIndex = i; +
+
@( slotIndex + 1 )
+
+ @if ( Inventory?.Weapons != null && Inventory.Weapons.Length > slotIndex && Inventory.Weapons[slotIndex] != null ) + { +
@Inventory.Weapons[slotIndex].GameObject.Name
+ } + else + { +
+ } +
+
} +
+
5
+
+
pan_tool
+
+
- } +
@code @@ -54,11 +67,11 @@ private bool HasInventoryChanged() { - if ( Inventory == null || Inventory.weapons == null ) return false; + if ( Inventory == null || Inventory.Weapons == null ) return false; for ( int i = 0; i < MAX_INVENTORY_SLOTS; i++ ) { - if ( Inventory.weapons[i] != lastWeapons[i] ) + if ( Inventory.Weapons[i] != lastWeapons[i] ) { return true; } @@ -71,12 +84,12 @@ { for ( int i = 0; i < MAX_INVENTORY_SLOTS; i++ ) { - lastWeapons[i] = Inventory.weapons[i]; + lastWeapons[i] = Inventory.Weapons[i]; } } protected override int BuildHash() { - return System.HashCode.Combine( Inventory?.SelectedSlot, string.Join( ",", Inventory?.weapons?.Select( w => w?.GameObject.Name ?? "Empty" ) ?? Array.Empty() ) ); + return System.HashCode.Combine( Inventory?.SelectedSlot, string.Join( ",", Inventory?.Weapons?.Select( w => w?.GameObject.Name ?? "Empty" ) ?? Array.Empty() ) ); } } diff --git a/code/ui/InventoryPanel.razor.scss b/code/ui/InventoryPanel.razor.scss new file mode 100644 index 0000000..b144e70 --- /dev/null +++ b/code/ui/InventoryPanel.razor.scss @@ -0,0 +1,64 @@ +InventoryPanel { + .hotbar-wrapper { + position: absolute; + display: flex; + width: 100%; + bottom: 32px; + justify-content: center; + align-items: center; + border-radius: 4px; + } + + .hotbar { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: 8px; + backdrop-filter: blur(1px); + } + + .item { + position: relative; + width: 96px; + height: 96px; + background-color: #0004; + opacity: 0.7; + border-radius: 3px; + color: #555; + font-weight: 400; + font-size: 35px; + font-family: Cascadia Code; + gap: 4px; + overflow: hidden; + + .item-number { + position: relative; + top: 0; + left: 0; + height: 32px; + border-bottom-right-radius: 4px; + } + + .item-content { + position: relative; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + } + + &.selected { + opacity: 1; + color: #fff; + } + + &.action { + width: 64px; + height: 64px; + opacity: 0.7; + background-color: rgba(0, 0, 0, 0); + } + } +} \ No newline at end of file diff --git a/code/ui/PlayerUI.razor b/code/ui/PlayerUI.razor index 6014816..554678f 100644 --- a/code/ui/PlayerUI.razor +++ b/code/ui/PlayerUI.razor @@ -1,14 +1,17 @@ -@using System @using Sandbox @using Sandbox.UI +@using Ultraneon.Events @inherits PanelComponent @attribute [StyleSheet] @namespace Ultraneon.UI - - - + + + @if ( Inventory?.ActiveWeapon != null ) + { + + } @@ -16,21 +19,32 @@ @code { [Property] - Entity player { get; set; } + PlayerNeon Player { get; set; } [Property] - PlayerInventory inventory { get; set; } + PlayerInventory Inventory { get; set; } + + private AmmoPanel ammoPanel; protected override void OnEnabled() { base.OnEnabled(); - player = Scene.GetAllComponents().FirstOrDefault( x => x.GameObject.Tags.Has( "player" ) ); - inventory = player?.GameObject.Components.Get(); - Log.Info( $"PlayerUI OnEnabled: player = {player}, inventory = {inventory}" ); + Player = Scene.GetAllComponents().FirstOrDefault( x => x.GameObject.Tags.Has( "player" ) ); + Inventory = Player?.GameObject.Components.Get(); } protected override int BuildHash() { - return player.IsValid() ? System.HashCode.Combine( player.health, inventory?.SelectedSlot ) : 0; + if ( !Player.IsValid() ) return 0; + + var hash = System.HashCode.Combine( + ((BaseNeonCharacterEntity)Player).Health, + Inventory?.SelectedSlot, + Inventory?.ActiveWeapon?.WeaponType, + Inventory?.ActiveWeapon?.CurrentAmmo, + Inventory?.ActiveWeapon?.ClipSize + ); + + return hash; } } diff --git a/code/ui/PlayerUI.razor.scss b/code/ui/PlayerUI.razor.scss index 080d8e9..808839a 100644 --- a/code/ui/PlayerUI.razor.scss +++ b/code/ui/PlayerUI.razor.scss @@ -1,90 +1,13 @@ - PlayerUI { position: absolute; bottom: 0px; left: 0px; right: 0px; top: 0px; + display: flex; pointer-events: none; .icon { font-family: Material Icons; } - - .vitals { - position: absolute; - left: 100px; - bottom: 100px; - gap: 10px; - background-color: #0004; - opacity: 0.7; - border-radius: 3px; - padding: 10px 20px; - color: palegreen; - font-weight: 400; - font-size: 50px; - font-family: Cascadia Code; - justify-content: center; - align-items: center; - backdrop-filter: blur(2px); - } - - .hotbar { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - position: absolute; - right: 20px; - bottom: 20px; - gap: 8px; - font-family: Material Icons; - } - - .items { - background-color: #0004; - opacity: 1; - border-radius: 3px; - padding: 30px 120px; - color: dimgrey; - font-weight: 400; - font-size: 35px; - font-family: Cascadia Code; - justify-content: center; - align-items: center; - gap: 16px; - - .item-image { - width: 128px; - height: 128px; - background-size: cover; - background-position: center; - } - - .weapon-pistol { - //background: url(data:image/gif;base64,R0lGODlhAQABAAAAACw=); - } - } - - .ammo-display { - position: absolute; - right: 100px; - bottom: 200px; - gap: 10px; - background-color: #0004; - opacity: 0.7; - border-radius: 3px; - padding: 10px 20px; - color: #3498db; - font-weight: 400; - font-size: 50px; - font-family: Cascadia Code; - justify-content: center; - align-items: center; - backdrop-filter: blur(2px); - - .icon { - font-family: Material Icons; - } - } } diff --git a/code/ui/RadarPanel.razor b/code/ui/RadarPanel.razor index 8082670..32fa2df 100644 --- a/code/ui/RadarPanel.razor +++ b/code/ui/RadarPanel.razor @@ -6,18 +6,21 @@
- @foreach ( var point in CapturePoints ) - { -
-
-
- } +
+ @foreach ( var point in CapturePoints ) + { +
+
+ } +
+
@code { private Entity playerEntity; + private CameraComponent mainCamera; public List CapturePoints { get; set; } = new(); protected override void OnAfterTreeRender( bool firstTime ) @@ -27,28 +30,35 @@ if ( firstTime ) { playerEntity = Scene.Components.GetAll().FirstOrDefault( x => x.GameObject.Tags.Has( "player" ) ); + mainCamera = Scene.Components.GetAll().FirstOrDefault( x => x.GameObject.Tags.Has( "maincamera" ) ); CapturePoints = Scene.Components.GetAll().ToList(); } } public override void Tick() { - if ( playerEntity == null ) return; + if ( playerEntity == null || mainCamera == null ) return; + + var playerPosition = mainCamera.Transform.Position; + var playerRotation = mainCamera.Transform.Rotation; + var maxRadarDistance = 1000f; foreach ( var point in CapturePoints ) { - var relativePos = point.Transform.Position - playerEntity.Transform.Position; - var distance = relativePos.Length; - var maxRadarDistance = 1000f; + var relativePos = point.Transform.Position - playerPosition; + var rotatedPos = playerRotation.Inverse * relativePos; + var distance = rotatedPos.Length; + distance = Math.Min( distance, maxRadarDistance ); - var angle = MathF.Atan2( relativePos.y, relativePos.x ); - point.RadarX = 50f + (distance / maxRadarDistance) * 50f * MathF.Cos( angle ); - point.RadarY = 50f + (distance / maxRadarDistance) * 50f * MathF.Sin( angle ); + point.MinimapX = 50f + (rotatedPos.x / maxRadarDistance) * 50f; + point.MinimapY = 50f - (rotatedPos.y / maxRadarDistance) * 50f; } } protected override int BuildHash() { - return HashCode.Combine( CapturePoints.Select( p => HashCode.Combine( p.RadarX, p.RadarY, p.ControllingTeam, p.Health ) ) ); + return HashCode.Combine( + CapturePoints.Select( p => HashCode.Combine( p.MinimapX, p.MinimapY, p.ControllingTeam, p.CaptureProgress ) ) + ); } } diff --git a/code/ui/RadarPanel.razor.scss b/code/ui/RadarPanel.razor.scss index 90f1d12..d5e0985 100644 --- a/code/ui/RadarPanel.razor.scss +++ b/code/ui/RadarPanel.razor.scss @@ -12,6 +12,29 @@ background-color: rgba(0, 0, 0, 0.5); position: relative; overflow: hidden; + z-index: 1; + } + + .radar-content { + width: 200px; + height: 200px; + overflow: hidden; + transform: rotate(-90deg); + border-radius: 50%; + position: relative; + z-index: 2; + } + + .player-dot { + position: absolute; + width: 12px; + height: 12px; + background-color: #ffffff; + border-radius: 50%; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 3; } .radar-blip { @@ -20,25 +43,18 @@ height: 16px; border-radius: 50%; transform: translate(-50%, -50%); + z-index: 10; &.Neutral { - background-color: gray; - } - - &.team1 { - background-color: blue; + background-color: #5A738B; } - &.team2 { - background-color: red; + &.Player { + background-color: #54be35; } - .health-indicator { - position: absolute; - bottom: 0; - left: 0; - height: 2px; - background-color: green; + &.Enemy { + background-color: #f8356b; } } } \ No newline at end of file diff --git a/code/ui/VitalsPanel.razor b/code/ui/VitalsPanel.razor index 481624d..91a1f0d 100644 --- a/code/ui/VitalsPanel.razor +++ b/code/ui/VitalsPanel.razor @@ -7,14 +7,14 @@
add_box
-
@( Player?.health )
+
@( ((BaseNeonCharacterEntity)Player)?.Health )
@code { [Property] - public Entity Player { get; set; } + public PlayerNeon Player { get; set; } protected override void OnAfterTreeRender( bool firstTime ) { @@ -22,12 +22,12 @@ if ( firstTime ) { - Player = Scene.GetAllComponents().FirstOrDefault( x => x.GameObject.Tags.Has( "player" ) ); + Player = Scene.GetAllComponents().FirstOrDefault( x => x.GameObject.Tags.Has( "player" ) ); } } protected override int BuildHash() { - return System.HashCode.Combine( Player?.health ); + return System.HashCode.Combine( ((BaseNeonCharacterEntity)Player)?.Health ); } } diff --git a/code/ui/VitalsPanel.razor.scss b/code/ui/VitalsPanel.razor.scss new file mode 100644 index 0000000..533d819 --- /dev/null +++ b/code/ui/VitalsPanel.razor.scss @@ -0,0 +1,20 @@ +VitalsPanel { + .vitals { + position: absolute; + display: flex; + left: 32px; + bottom: 32px; + gap: 10px; + background-color: #0004; + opacity: 0.7; + border-radius: 3px; + padding: 10px 20px; + color: palegreen; + font-weight: 400; + font-size: 50px; + font-family: Cascadia Code; + justify-content: center; + align-items: center; + backdrop-filter: blur(2px); + } +} \ No newline at end of file diff --git a/code/weapons/WeaponBaseNeon.cs b/code/weapons/WeaponBaseNeon.cs index 60f5408..43376bd 100644 --- a/code/weapons/WeaponBaseNeon.cs +++ b/code/weapons/WeaponBaseNeon.cs @@ -1,206 +1,307 @@ -using Sandbox; -using Sandbox.Citizen; -using Sandbox.Diagnostics; -using System; +using Sandbox.Events; +using Ultraneon.Events; -public enum WeaponType +namespace Ultraneon { - pistol, - semi, - auto, - bolt -} + public enum WeaponType + { + Pistol, + Semi, + Auto, + Bolt + } -public sealed class WeaponBaseNeon : Component, Component.ITriggerListener -{ + public sealed class WeaponBaseNeon : Component, Component.ITriggerListener + { + [Property, ReadOnly] + public bool IsPickedUp { get; private set; } + public TimeSince SinceEquipped { get; private set; } - [Property,ReadOnly] - public bool isPickedUp { get; set; } = false; + public TimeSince SinceShot { get; private set; } + public TimeSince TimeInReload { get; private set; } - [Property,ReadOnly]public TimeSince sinceEquippd { get; set; } = 0f; - [Property,ReadOnly]public TimeSince sinceShot { get; set; } = 0f; - [Property, ReadOnly] bool hasShoot { get; set; } = false; + [Property, Group( "Viewmodel" )] + public SkinnedModelRenderer Viewmodel { get; set; } - [Property, Group( "Viewmodel" )] public SkinnedModelRenderer Viewmodel { get; set; } + [Property, Group( "Viewmodel" )] + public SkinnedModelRenderer Worldmodel { get; set; } - [Property, Group( "Viewmodel" )] public SkinnedModelRenderer Worldmodel { get; set; } + [Property, Group( "Viewmodel" )] + public SkinnedModelRenderer ViewmodelArms { get; set; } - #region weapon stats - [Property, Group( "Weapon stats" )] - public WeaponType weaponType { get; set; } + [Property, Group( "Current Ammo" )] + public int CurrentAmmo; - [Property,Group("Weapon stats")] - public int clipSize { get; set; } + [Property, Group( "Weapon Stats" )] + public WeaponType WeaponType { get; set; } - [Property, Group( "Weapon stats" )] - public float fireRate { get; set; } + [Property, Group( "Weapon Stats" )] + public int ClipSize { get; set; } - [Property, Group( "Weapon stats" )] - public float equipTime { get; set; } + [Property, Group( "Weapon Stats" )] + public float FireRate { get; set; } - [Property, Group( "Weapon stats" )] - public float reloadTime { get; set; } + [Property, Group( "Weapon Stats" )] + public float EquipTime { get; set; } - [Property, Group( "Weapon stats" )] - public bool isReloading { get; set; } - [Property, Group( "Weapon stats" )] - public bool isSemiAuto { get; set; } - #endregion + [Property, Group( "Weapon Stats" )] + public float ReloadTime { get; set; } - #region weapon damage - [Property, Group( "Weapon damage" )] - public float weaponDamage { get; set; } + [Property, Group( "Weapon Stats" )] + public bool IsSemiAuto { get; set; } - [Property, Range( 1f, 10f, 0.1f ), Group( "Weapon damage" )] - public float headShotMultiplier { get; set; } + [Property, Group( "Weapon Damage" )] + public float WeaponDamage { get; set; } - #endregion + [Property, Range( 1f, 10f, 0.1f ), Group( "Weapon Damage" )] + public float HeadshotMultiplier { get; set; } = 1.5f; + [Property, Group( "Weapon Effects" )] + public SoundEvent ShootSound { get; set; } - [Property, Group( "Weapon effects" )] - public SoundEvent shootSound { get; set; } + [Property, Group( "Weapon Effects" )] + public GameObject ImpactPrefab { get; set; } - [Property, Group( "Weapon effects" )] - public GameObject ImpactPrefab { get; set; } + public GameObject Owner { get; private set; } - public GameObject owner { get;set; } + public int WeaponSlot { get; private set; } + private bool _hasShot; + public bool IsReloading { get; private set; } + protected override void OnStart() + { + base.OnStart(); + CurrentAmmo = ClipSize; + } - protected override void OnUpdate() - { - base.OnUpdate(); - if ( isPickedUp ) + protected override void OnUpdate() { - var camera = Scene.GetAllComponents().Where( x => x.IsMainCamera ).FirstOrDefault(); - if ( camera is null ) return; - Transform.Position = camera.Transform.Position; - + base.OnUpdate(); + + if ( IsPickedUp ) + { + UpdatePosition(); + } + + if ( !Input.Down( "attack1" ) ) + { + _hasShot = false; + } + + if ( IsReloading ) + { + UpdateReload(); + } } - if ( !Input.Down( "attack1" ) && hasShoot ) + private void UpdateReload() { - hasShoot = false; + if ( TimeInReload >= ReloadTime ) + { + CompleteReload(); + } + } + + private void UpdatePosition() + { + var camera = Scene.GetAllComponents().FirstOrDefault( x => x.IsMainCamera ); + if ( camera != null ) + { + Transform.Position = camera.Transform.Position; + } } - } - public void Shoot() - { - if ( IsProxy ) return; - if ( sinceEquippd < equipTime ) return; - if ( sinceShot < fireRate ) return; - if (hasShoot && isSemiAuto) return; - Sound.Play( shootSound ); - hasShoot= true; + public void Shoot() + { + if ( IsProxy || !CanShoot() ) return; - Viewmodel?.Set( "b_attack", true ); + PerformShoot(); + } - var camera = Scene.GetAllComponents().Where( x => x.IsMainCamera ).FirstOrDefault(); - if ( camera is null ) return; - var rayStart = camera.Transform.Position; - var shotTrace = Scene.Trace.Ray( rayStart, rayStart + camera.Transform.World.Forward * 65536f ) - .IgnoreGameObjectHierarchy( GameObject.Parent ) - .UseHitboxes() - .Run(); - if ( shotTrace.Hit ) + private bool CanShoot() { + return SinceEquipped >= EquipTime && + SinceShot >= FireRate && + (!IsSemiAuto || !_hasShot) && + !IsReloading && + CurrentAmmo > 0; + } + + private void PerformShoot() + { + Sound.Play( ShootSound ); + _hasShot = true; + CurrentAmmo--; + Log.Info( $"Weapon {GameObject.Name} fired. Current Ammo: {CurrentAmmo}/{ClipSize}" ); + + GameObject.Dispatch( new WeaponStateChangedEvent( this ) ); - GameObject impact = ImpactPrefab.Clone( shotTrace.EndPosition, Rotation.LookAt( -shotTrace.Normal ) ); + Viewmodel?.Set( "b_attack", true ); - if ( shotTrace.GameObject.Components.Get() == null ) return; - var totalDamage = weaponDamage; - //if ( shotTrace.Hitbox.Bone.Name == "head" ) totalDamage *= headShotMultiplier; - var dmg = shotTrace.GameObject.Components.Get(); - if ( dmg != null ) + var camera = Scene.GetAllComponents().FirstOrDefault( x => x.IsMainCamera ); + if ( camera == null ) return; + + var rayStart = camera.Transform.Position; + var shotTrace = Scene.Trace.Ray( rayStart, rayStart + camera.Transform.World.Forward * 65536f ) + .IgnoreGameObjectHierarchy( GameObject.Parent ) + .UseHitboxes() + .Run(); + + if ( shotTrace.Hit ) { + HandleHit( shotTrace ); + } - dmg.OnDamage( new DamageInfo() - { - Damage = totalDamage, - Attacker = GameObject.Parent, - Position = Transform.Position, - - } ); - Log.Info( totalDamage); + SinceShot = 0f; + + if ( CurrentAmmo == 0 ) + { + StartReload(); } + } + + private void HandleHit( SceneTraceResult shotTrace ) + { + SpawnImpactEffect( shotTrace ); + var damageable = shotTrace.GameObject?.Components.Get(); + if ( damageable != null ) + { + float totalDamage = CalculateDamage( shotTrace ); + ApplyDamage( damageable, totalDamage, shotTrace ); + } } + private void SpawnImpactEffect( SceneTraceResult shotTrace ) + { + if ( ImpactPrefab != null ) + { + ImpactPrefab.Clone( shotTrace.EndPosition, Rotation.LookAt( -shotTrace.Normal ) ); + } + } - sinceShot = 0f; - } + private float CalculateDamage( SceneTraceResult shotTrace ) + { + float damage = WeaponDamage; + if ( IsHeadshot( shotTrace ) ) + { + damage *= HeadshotMultiplier; + } - public void Holster() - { + return damage; + } - } + private bool IsHeadshot( SceneTraceResult shotTrace ) + { + // TODO Implement proper headshot detection logic here + return shotTrace.Hitbox?.Bone?.Name.ToLower().Contains( "head" ) ?? false; + } - public void Equip() - { - sinceEquippd = 0f; - //Log.Info( "equpped" ); - } + private void ApplyDamage( IDamageable damageable, float damage, SceneTraceResult shotTrace ) + { + var damageInfo = new DamageInfo { Damage = damage, Attacker = Owner, Position = shotTrace.EndPosition }; + damageable.OnDamage( damageInfo ); + } - public void OnTriggerEnter( Collider other ) - { - if ( isPickedUp ) return; - if ( !other.GameObject.Tags.Has( "player" ) ) return; - var inventory = other.GameObject.Components.Get(); - if ( inventory.IsValid() ) + public void StartReload() { - switch ( weaponType ) - { - case WeaponType.pistol: - if ( inventory.weapons[0] != null ) return; - inventory.weapons[0] = this; - addToOwner( inventory ); - break; - case WeaponType.semi: - if ( inventory.weapons[1] != null ) return; - inventory.weapons[1] = this; - addToOwner( inventory ); - break; - case WeaponType.auto: - if ( inventory.weapons[2] != null ) return; - inventory.weapons[2] = this; - addToOwner( inventory ); - break; - case WeaponType.bolt: - if ( inventory.weapons[3] != null ) return; - inventory.weapons[3] = this; - addToOwner( inventory ); - break; - } + if ( IsReloading || CurrentAmmo == ClipSize ) return; + IsReloading = true; + TimeInReload = 0f; + // TODO: Play reload animation + // TODO: Play reload sound + Log.Info( $"Weapon {GameObject.Name} started reloading." ); + GameObject.Dispatch( new WeaponStateChangedEvent( this ) ); } - } - void addToOwner(PlayerInventory inventory) - { - this.GameObject.SetParent( inventory.GameObject, false ); - GameObject.Transform.Position = inventory.GameObject.Transform.Position; - GameObject.Transform.Rotation = inventory.GameObject.Transform.Rotation; + private void CompleteReload() + { + CurrentAmmo = ClipSize; + IsReloading = false; + Log.Info( $"Weapon {GameObject.Name} reloaded. Current Ammo: {CurrentAmmo}/{ClipSize}" ); + GameObject.Dispatch( new WeaponStateChangedEvent( this ) ); + + // TODO: Play reload complete animation + // TODO: Play reload complete sound + } - Worldmodel.Enabled = false; - Viewmodel.Enabled = true; + public void SetVisible( bool visible ) + { + if ( Viewmodel != null ) + { + Viewmodel.Enabled = visible; + } + } - var v_arms = inventory.GameObject.Children.FirstOrDefault().Components.Get(true); - v_arms.Enabled = true; - if ( v_arms != null ) + public void Equip() { - - v_arms.BoneMergeTarget = Viewmodel; + SinceEquipped = 0f; + SetVisible( true ); + // TODO: Play equip animation + // TODO: Play equip sound } - owner = inventory.GameObject; - isPickedUp = true; - if ( inventory.weapons.Count( x => x != null ) == 1 ) + + public void Holster() { + SetVisible( false ); + // TODO: Play holster animation + // TODO: Play holster sound + } + + public void OnTriggerEnter( Collider other ) + { + if ( IsPickedUp || !other.GameObject.Tags.Has( "player" ) ) return; + + var inventory = other.GameObject.Components.Get(); + if ( inventory != null ) + { + if ( inventory.AddWeapon( this ) ) + { + AddToOwner( inventory ); + } + } + } + + private void AddToOwner( PlayerInventory inventory ) + { + GameObject.SetParent( inventory.GameObject, false ); + Transform.Position = inventory.GameObject.Transform.Position; + Transform.Rotation = inventory.GameObject.Transform.Rotation; + + if ( Worldmodel is not null ) + { + Worldmodel.Enabled = false; + } + + if ( Viewmodel is not null ) + { + Viewmodel.Enabled = true; + } + + SetupViewmodelArms( inventory ); + + Owner = inventory.GameObject; + IsPickedUp = true; + inventory.SetActive( this ); } - } + private void SetupViewmodelArms( PlayerInventory inventory ) + { + var viewmodelArms = inventory.GameObject.Children + .FirstOrDefault()?.Components.Get( true ); + + if ( viewmodelArms != null ) + { + viewmodelArms.Enabled = true; + viewmodelArms.BoneMergeTarget = Viewmodel; + } + } + } }