208 lines
8.5 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|