Files
EE2Clone/Assets/Scripts/Systems/CombatSystems.cs
ldbetteridge 58da5d1d71 Initial import
2026-03-31 15:59:23 +01:00

208 lines
8.5 KiB
C#

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.NetCode;
using EE2Clone.Components;
using EE2Clone.Core;
namespace EE2Clone.Systems
{
/// <summary>
/// Server-side: processes AttackCommandRpc — sets combat target on units.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct AttackCommandSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (cmd, source, entity) in
SystemAPI.Query<RefRO<AttackCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
.WithEntityAccess())
{
var attacker = cmd.ValueRO.AttackerEntity;
var target = cmd.ValueRO.TargetEntity;
if (state.EntityManager.Exists(attacker) && state.EntityManager.Exists(target) &&
state.EntityManager.HasComponent<CombatTarget>(attacker))
{
state.EntityManager.SetComponentData(attacker, new CombatTarget { Target = target });
state.EntityManager.SetComponentData(attacker, new UnitStateComponent { Value = UnitState.Attacking });
}
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
/// <summary>
/// Server-side: units with a CombatTarget move toward target, then deal damage when in range.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateAfter(typeof(AttackCommandSystem))]
public partial struct CombatSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
foreach (var (transform, attack, combatTarget, speed, unitState, unitClass, entity) in
SystemAPI.Query<RefRW<LocalTransform>, RefRW<AttackData>, RefRW<CombatTarget>,
RefRO<MovementSpeed>, RefRW<UnitStateComponent>, RefRO<UnitClassComponent>>()
.WithAll<UnitTag>()
.WithEntityAccess())
{
// Tick cooldown
if (attack.ValueRO.CooldownRemaining > 0)
attack.ValueRW.CooldownRemaining -= dt;
var target = combatTarget.ValueRO.Target;
if (target == Entity.Null || !state.EntityManager.Exists(target))
{
// Clear invalid target
if (target != Entity.Null)
{
combatTarget.ValueRW.Target = Entity.Null;
if (unitState.ValueRO.Value == UnitState.Attacking)
unitState.ValueRW.Value = UnitState.Idle;
}
continue;
}
if (!state.EntityManager.HasComponent<Health>(target))
{
combatTarget.ValueRW.Target = Entity.Null;
unitState.ValueRW.Value = UnitState.Idle;
continue;
}
var targetTransform = state.EntityManager.GetComponentData<LocalTransform>(target);
float3 toTarget = targetTransform.Position - transform.ValueRO.Position;
toTarget.y = 0;
float distance = math.length(toTarget);
if (distance > attack.ValueRO.Range)
{
// Move toward target
float3 moveDir = math.normalize(toTarget);
float moveAmount = math.min(speed.ValueRO.Value * dt, distance - attack.ValueRO.Range * 0.9f);
transform.ValueRW.Position += moveDir * moveAmount;
transform.ValueRW.Rotation = quaternion.LookRotationSafe(moveDir, math.up());
}
else if (attack.ValueRO.CooldownRemaining <= 0)
{
// Attack
float damage = attack.ValueRO.Damage;
// Apply counter bonus
if (state.EntityManager.HasComponent<UnitClassComponent>(target))
{
var targetClass = state.EntityManager.GetComponentData<UnitClassComponent>(target).Value;
float counterMult = GetCounterMultiplier(unitClass.ValueRO.Value, targetClass);
damage *= counterMult;
}
// Apply armor
if (state.EntityManager.HasComponent<ArmorData>(target))
{
float armor = state.EntityManager.GetComponentData<ArmorData>(target).Value;
damage = math.max(damage - armor, GameConstants.MinDamage);
}
var targetHealth = state.EntityManager.GetComponentData<Health>(target);
targetHealth.Current -= damage;
state.EntityManager.SetComponentData(target, targetHealth);
attack.ValueRW.CooldownRemaining = attack.ValueRO.AttackCooldown;
// Face target
if (distance > 0.01f)
transform.ValueRW.Rotation = quaternion.LookRotationSafe(math.normalize(toTarget), math.up());
}
}
}
private static float GetCounterMultiplier(UnitClass attacker, UnitClass target)
{
// Rock-paper-scissors: Infantry > Ranged > Cavalry > Infantry
if (attacker == UnitClass.Infantry && target == UnitClass.Ranged) return GameConstants.CounterBonusStrong;
if (attacker == UnitClass.Ranged && target == UnitClass.Cavalry) return GameConstants.CounterBonusStrong;
if (attacker == UnitClass.Cavalry && target == UnitClass.Infantry) return GameConstants.CounterBonusStrong;
if (attacker == UnitClass.Infantry && target == UnitClass.Cavalry) return GameConstants.CounterBonusWeak;
if (attacker == UnitClass.Ranged && target == UnitClass.Infantry) return GameConstants.CounterBonusWeak;
if (attacker == UnitClass.Cavalry && target == UnitClass.Ranged) return GameConstants.CounterBonusWeak;
// Siege bonus vs buildings (handled via BuildingTag check in caller if needed)
return GameConstants.CounterBonusNeutral;
}
}
/// <summary>
/// Server-side: destroys entities that have 0 or less HP after a brief delay.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateAfter(typeof(CombatSystem))]
public partial struct DeathSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
float dt = SystemAPI.Time.DeltaTime;
// Tag entities with <=0 HP for destruction
foreach (var (health, entity) in
SystemAPI.Query<RefRO<Health>>()
.WithNone<DeathTimer, DestroyEntityTag>()
.WithEntityAccess())
{
if (health.ValueRO.Current <= 0)
{
ecb.AddComponent(entity, new DeathTimer { TimeRemaining = 1.0f });
if (state.EntityManager.HasComponent<UnitStateComponent>(entity))
{
ecb.SetComponent(entity, new UnitStateComponent { Value = UnitState.Dying });
}
}
}
// Tick death timers
foreach (var (deathTimer, entity) in
SystemAPI.Query<RefRW<DeathTimer>>()
.WithNone<DestroyEntityTag>()
.WithEntityAccess())
{
deathTimer.ValueRW.TimeRemaining -= dt;
if (deathTimer.ValueRO.TimeRemaining <= 0)
{
ecb.AddComponent<DestroyEntityTag>(entity);
}
}
// Destroy tagged entities
foreach (var (tag, entity) in
SystemAPI.Query<RefRO<DestroyEntityTag>>()
.WithEntityAccess())
{
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}