Add attribute system with core stats and gameplay components

This commit is contained in:
2025-10-13 12:10:45 +02:00
commit ce3596efaa
55 changed files with 1161 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
namespace GameCore.Combat;
public enum AttackType
{
Hitscan = 0,
Projectile = 1,
Melee = 2
}

View 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;
}

View 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);
}
}

View 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
{
}

View 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);
}
}
}

View 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();
}
}

View 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
));
}
}
}

View 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));
}
}

View File

@@ -0,0 +1,10 @@
using GameCore.ECS;
namespace GameCore.Combat.Effects;
public class EffectContext
{
public Entity Owner;
public Entity? Target;
public World World;
}

View 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);
}
}
}

View File

@@ -0,0 +1,8 @@
using GameCore.Combat.Effects;
namespace GameCore.Combat.Interfaces;
public interface IEffect
{
void Execute(EffectContext context);
}

View 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());
}
}
}

View 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; } = [];
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}

View 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;
}
}
}
}