Add attribute system with core stats and gameplay components
This commit is contained in:
8
GameCore/Combat/AttackType.cs
Normal file
8
GameCore/Combat/AttackType.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public enum AttackType
|
||||
{
|
||||
Hitscan = 0,
|
||||
Projectile = 1,
|
||||
Melee = 2
|
||||
}
|
||||
15
GameCore/Combat/DamageEventComponent.cs
Normal file
15
GameCore/Combat/DamageEventComponent.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
/// <summary>
|
||||
/// A temporary, event-like component. When this component is added to an entity,
|
||||
/// the DamageSystem will process it, apply damage to the target, and then
|
||||
/// immediately remove this component. This simulates a one-time damage event.
|
||||
/// </summary>
|
||||
public class DamageEventComponent(Entity target, float amount) : IComponent
|
||||
{
|
||||
public Entity Target { get; set; } = target;
|
||||
public float Amount { get; set; } = amount;
|
||||
}
|
||||
38
GameCore/Combat/DamageSystem.cs
Normal file
38
GameCore/Combat/DamageSystem.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using GameCore.Attributes;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Events;
|
||||
using Attribute = GameCore.Attributes.Attribute;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class DamageSystem : ISystem
|
||||
{
|
||||
private readonly World _world;
|
||||
|
||||
public DamageSystem(World world)
|
||||
{
|
||||
_world = world;
|
||||
world.Subscribe<DamageDealtEvent>(OnDamageDealt);
|
||||
}
|
||||
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entitiesWithHealth = world.GetEntitiesWith<AttributeComponent>();
|
||||
foreach (var entity in entitiesWithHealth)
|
||||
{
|
||||
if (world.GetComponent<DeathComponent>(entity) != null) continue;
|
||||
|
||||
var attributes = world.GetComponent<AttributeComponent>(entity);
|
||||
if (attributes == null) continue;
|
||||
|
||||
if (attributes.GetValue(Attribute.Health) <= 0) world.AddComponent(entity, new DeathComponent());
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDamageDealt(DamageDealtEvent e)
|
||||
{
|
||||
var targetAttributes = _world.GetComponent<AttributeComponent>(e.Target);
|
||||
targetAttributes?.ModifyValue(Attribute.Health, -e.Amount);
|
||||
}
|
||||
}
|
||||
13
GameCore/Combat/DeathComponent.cs
Normal file
13
GameCore/Combat/DeathComponent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
/// <summary>
|
||||
/// A simple "marker" component. When the DamageSystem determines an entity's
|
||||
/// health has dropped to 0 or below, it adds this component to the entity.
|
||||
/// Other systems (or the presenter layer) can then react to this, for example,
|
||||
/// by playing a death animation, creating a ragdoll, and eventually destroying the entity.
|
||||
/// </summary>
|
||||
public class DeathComponent : IComponent
|
||||
{
|
||||
}
|
||||
19
GameCore/Combat/DestructionSystem.cs
Normal file
19
GameCore/Combat/DestructionSystem.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Events;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class DestructionSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entitiesWithDeath = world.GetEntitiesWith<DeathComponent>();
|
||||
foreach (var entity in entitiesWithDeath)
|
||||
{
|
||||
world.PublishEvent(new EntityDiedEvent(entity));
|
||||
|
||||
world.DestroyEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
GameCore/Combat/EffectExecutionSystem.cs
Normal file
12
GameCore/Combat/EffectExecutionSystem.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class EffectExecutionSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
48
GameCore/Combat/Effects/BulkProjectileEffect.cs
Normal file
48
GameCore/Combat/Effects/BulkProjectileEffect.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.Events;
|
||||
using GameCore.Input;
|
||||
using GameCore.Movement;
|
||||
|
||||
namespace GameCore.Combat.Effects;
|
||||
|
||||
public class BulkProjectileEffect(string archetypeId, int count, float spreadAngle, float speed) : IEffect
|
||||
{
|
||||
private static readonly Random _random = new();
|
||||
|
||||
public void Execute(EffectContext context)
|
||||
{
|
||||
var ownerInput = context.World.GetComponent<InputStateComponent>(context.Owner);
|
||||
if (ownerInput == null)
|
||||
return;
|
||||
|
||||
var ownerRotation = context.World.GetComponent<RotationComponent>(context.Owner);
|
||||
if (ownerRotation == null) return;
|
||||
|
||||
var spreadRadians = spreadAngle * (float)System.Math.PI / 180f;
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var direction = ownerInput.MuzzleDirection;
|
||||
|
||||
if (spreadRadians > 0f)
|
||||
{
|
||||
var randomYaw = ((float)_random.NextDouble() * 2f - 1f) * spreadRadians;
|
||||
var randomPitch = ((float)_random.NextDouble() * 2f - 1f) * spreadRadians;
|
||||
|
||||
direction = context.World.WorldQuery.RotateVectorByYaw(direction, randomYaw);
|
||||
direction.Y += (float)System.Math.Sin(randomPitch);
|
||||
direction = direction.Normalize();
|
||||
}
|
||||
|
||||
var initialVelocity = direction * speed;
|
||||
|
||||
context.World.PublishEvent(new SpawnEntityEvent(
|
||||
archetypeId,
|
||||
ownerInput.MuzzlePosition,
|
||||
ownerRotation.Rotation,
|
||||
context.Owner,
|
||||
initialVelocity
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
15
GameCore/Combat/Effects/DamageEffect.cs
Normal file
15
GameCore/Combat/Effects/DamageEffect.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.Events;
|
||||
|
||||
namespace GameCore.Combat.Effects;
|
||||
|
||||
public class DamageEffect(float amount) : IEffect
|
||||
{
|
||||
public void Execute(EffectContext context)
|
||||
{
|
||||
if (context.Target == null)
|
||||
return;
|
||||
|
||||
context.World.PublishEvent(new DamageDealtEvent(context.Target.Value, amount));
|
||||
}
|
||||
}
|
||||
10
GameCore/Combat/Effects/EffectContext.cs
Normal file
10
GameCore/Combat/Effects/EffectContext.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using GameCore.ECS;
|
||||
|
||||
namespace GameCore.Combat.Effects;
|
||||
|
||||
public class EffectContext
|
||||
{
|
||||
public Entity Owner;
|
||||
public Entity? Target;
|
||||
public World World;
|
||||
}
|
||||
30
GameCore/Combat/Effects/HitscanEffect.cs
Normal file
30
GameCore/Combat/Effects/HitscanEffect.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.Input;
|
||||
|
||||
namespace GameCore.Combat.Effects;
|
||||
|
||||
public class HitscanEffect(float range) : IEffect
|
||||
{
|
||||
public void Execute(EffectContext context)
|
||||
{
|
||||
var input = context.World.GetComponent<InputStateComponent>(context.Owner);
|
||||
var weapon = context.World.GetComponent<WeaponComponent>(context.Owner);
|
||||
|
||||
if (input == null || weapon == null) return;
|
||||
|
||||
var targetPos = input.MuzzlePosition + input.MuzzleDirection * range;
|
||||
var hit = context.World.WorldQuery.Raycast(input.MuzzlePosition, targetPos, context.Owner);
|
||||
|
||||
if (hit.DidHit)
|
||||
{
|
||||
var hitContext = new EffectContext
|
||||
{
|
||||
World = context.World,
|
||||
Owner = context.Owner,
|
||||
Target = hit.HitEntity
|
||||
};
|
||||
|
||||
foreach (var effect in weapon.OnHitEffects) effect.Execute(hitContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
8
GameCore/Combat/Interfaces/IEffect.cs
Normal file
8
GameCore/Combat/Interfaces/IEffect.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using GameCore.Combat.Effects;
|
||||
|
||||
namespace GameCore.Combat.Interfaces;
|
||||
|
||||
public interface IEffect
|
||||
{
|
||||
void Execute(EffectContext context);
|
||||
}
|
||||
25
GameCore/Combat/ProjectileCleanupSystem.cs
Normal file
25
GameCore/Combat/ProjectileCleanupSystem.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Physics;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class ProjectileCleanupSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var projectiles = world.GetEntitiesWith<ProjectileComponent>();
|
||||
foreach (var projectile in projectiles)
|
||||
{
|
||||
var projectileData = world.GetComponent<ProjectileComponent>(projectile);
|
||||
var position = world.GetComponent<PositionComponent>(projectile);
|
||||
var velocity = world.GetComponent<VelocityComponent>(projectile);
|
||||
|
||||
if (projectileData == null || position == null || velocity == null)
|
||||
continue;
|
||||
|
||||
projectileData.Lifetime -= deltaTime;
|
||||
if (projectileData.Lifetime <= 0f) world.AddComponent(projectile, new DeathComponent());
|
||||
}
|
||||
}
|
||||
}
|
||||
13
GameCore/Combat/ProjectileComponent.cs
Normal file
13
GameCore/Combat/ProjectileComponent.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class ProjectileComponent : IComponent
|
||||
{
|
||||
public Entity Owner { get; set; }
|
||||
public float Lifetime { get; set; }
|
||||
|
||||
public List<IEffect> OnHitEffects { get; set; } = [];
|
||||
}
|
||||
33
GameCore/Combat/ProjectileInitializationSystem.cs
Normal file
33
GameCore/Combat/ProjectileInitializationSystem.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Events;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class ProjectileInitializationSystem : ISystem
|
||||
{
|
||||
private readonly World _world;
|
||||
|
||||
public ProjectileInitializationSystem(World world)
|
||||
{
|
||||
_world = world;
|
||||
_world.Subscribe<EntitySpawnedEvent>(OnEntitySpawned);
|
||||
}
|
||||
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
}
|
||||
|
||||
private void OnEntitySpawned(EntitySpawnedEvent e)
|
||||
{
|
||||
var projComp = _world.GetComponent<ProjectileComponent>(e.NewEntity);
|
||||
if (projComp == null) return;
|
||||
|
||||
var ownerWeapon = _world.GetComponent<WeaponComponent>(e.Owner);
|
||||
if (ownerWeapon != null)
|
||||
{
|
||||
projComp.Owner = e.Owner;
|
||||
projComp.OnHitEffects = ownerWeapon.OnHitEffects;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
GameCore/Combat/ProjectileSystem.cs
Normal file
43
GameCore/Combat/ProjectileSystem.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using GameCore.Combat.Effects;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Physics;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class ProjectileSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var projectiles = world.GetEntitiesWith<ProjectileComponent>();
|
||||
foreach (var projectile in projectiles)
|
||||
{
|
||||
var velocity = world.GetComponent<VelocityComponent>(projectile);
|
||||
var position = world.GetComponent<PositionComponent>(projectile);
|
||||
var projectileData = world.GetComponent<ProjectileComponent>(projectile);
|
||||
|
||||
if (velocity == null || position == null || projectileData == null)
|
||||
continue;
|
||||
|
||||
var newPosition = position.Position + velocity.DesiredVelocity * deltaTime;
|
||||
|
||||
var hit = world.WorldQuery.Raycast(position.Position, newPosition, projectileData.Owner);
|
||||
if (hit.DidHit)
|
||||
{
|
||||
var hitContext = new EffectContext
|
||||
{
|
||||
World = world,
|
||||
Owner = projectileData.Owner,
|
||||
Target = hit.HitEntity
|
||||
};
|
||||
|
||||
foreach (var effect in projectileData.OnHitEffects) effect.Execute(hitContext);
|
||||
|
||||
world.AddComponent(projectile, new DeathComponent());
|
||||
continue;
|
||||
}
|
||||
|
||||
position.Position = newPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
GameCore/Combat/WeaponComponent.cs
Normal file
14
GameCore/Combat/WeaponComponent.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using GameCore.Combat.Interfaces;
|
||||
using GameCore.ECS.Interfaces;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class WeaponComponent : IComponent
|
||||
{
|
||||
public float FireRate { get; set; } = 1f;
|
||||
|
||||
public List<IEffect> OnFireEffects { get; set; } = [];
|
||||
public List<IEffect> OnHitEffects { get; set; } = [];
|
||||
|
||||
public float CooldownTimer { get; set; } = 0f;
|
||||
}
|
||||
39
GameCore/Combat/WeaponSystem.cs
Normal file
39
GameCore/Combat/WeaponSystem.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using GameCore.Combat.Effects;
|
||||
using GameCore.ECS;
|
||||
using GameCore.ECS.Interfaces;
|
||||
using GameCore.Events;
|
||||
using GameCore.Input;
|
||||
using GameCore.Movement;
|
||||
using GameCore.Physics;
|
||||
|
||||
namespace GameCore.Combat;
|
||||
|
||||
public class WeaponSystem : ISystem
|
||||
{
|
||||
public void Update(World world, float deltaTime)
|
||||
{
|
||||
var entities = world.GetEntitiesWith<InputStateComponent>();
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
var input = world.GetComponent<InputStateComponent>(entity);
|
||||
var weapon = world.GetComponent<WeaponComponent>(entity);
|
||||
var position = world.GetComponent<PositionComponent>(entity);
|
||||
var rotation = world.GetComponent<RotationComponent>(entity);
|
||||
|
||||
if (input == null || weapon == null || position == null || rotation == null)
|
||||
continue;
|
||||
|
||||
if (weapon.CooldownTimer > 0f) weapon.CooldownTimer -= deltaTime;
|
||||
|
||||
if (input.IsFiring && weapon.CooldownTimer <= 0f)
|
||||
{
|
||||
var context = new EffectContext { World = world, Owner = entity };
|
||||
|
||||
foreach (var effect in weapon.OnFireEffects) effect.Execute(context);
|
||||
|
||||
world.PublishEvent(new WeaponFiredEvent(entity, input.MuzzlePosition));
|
||||
weapon.CooldownTimer = 1f / weapon.FireRate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user