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 { /// /// Server-side: processes AttackCommandRpc — sets combat target on units. /// [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>() .WithEntityAccess()) { var attacker = cmd.ValueRO.AttackerEntity; var target = cmd.ValueRO.TargetEntity; if (state.EntityManager.Exists(attacker) && state.EntityManager.Exists(target) && state.EntityManager.HasComponent(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(); } } /// /// Server-side: units with a CombatTarget move toward target, then deal damage when in range. /// [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, RefRW, RefRO, RefRW, RefRO>() .WithAll() .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(target)) { combatTarget.ValueRW.Target = Entity.Null; unitState.ValueRW.Value = UnitState.Idle; continue; } var targetTransform = state.EntityManager.GetComponentData(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(target)) { var targetClass = state.EntityManager.GetComponentData(target).Value; float counterMult = GetCounterMultiplier(unitClass.ValueRO.Value, targetClass); damage *= counterMult; } // Apply armor if (state.EntityManager.HasComponent(target)) { float armor = state.EntityManager.GetComponentData(target).Value; damage = math.max(damage - armor, GameConstants.MinDamage); } var targetHealth = state.EntityManager.GetComponentData(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; } } /// /// Server-side: destroys entities that have 0 or less HP after a brief delay. /// [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>() .WithNone() .WithEntityAccess()) { if (health.ValueRO.Current <= 0) { ecb.AddComponent(entity, new DeathTimer { TimeRemaining = 1.0f }); if (state.EntityManager.HasComponent(entity)) { ecb.SetComponent(entity, new UnitStateComponent { Value = UnitState.Dying }); } } } // Tick death timers foreach (var (deathTimer, entity) in SystemAPI.Query>() .WithNone() .WithEntityAccess()) { deathTimer.ValueRW.TimeRemaining -= dt; if (deathTimer.ValueRO.TimeRemaining <= 0) { ecb.AddComponent(entity); } } // Destroy tagged entities foreach (var (tag, entity) in SystemAPI.Query>() .WithEntityAccess()) { ecb.DestroyEntity(entity); } ecb.Playback(state.EntityManager); ecb.Dispose(); } } }