Initial import

This commit is contained in:
ldbetteridge
2026-03-31 15:59:23 +01:00
commit 58da5d1d71
136 changed files with 10922 additions and 0 deletions

View File

@@ -0,0 +1,197 @@
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 PlaceBuildingRpc — validates and spawns building entities.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct PlaceBuildingCommandSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (cmd, source, entity) in
SystemAPI.Query<RefRO<PlaceBuildingRpc>, RefRO<ReceiveRpcCommandRequest>>()
.WithEntityAccess())
{
// TODO: Validate resources, territory, collision
// TODO: Lookup building prefab entity from BuildingType
// TODO: Instantiate the prefab and place it
// For now just log
UnityEngine.Debug.Log($"[Server] PlaceBuilding request: {cmd.ValueRO.Type} at {cmd.ValueRO.Position}");
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
/// <summary>
/// Server-side: processes BuildRepairCommandRpc — assigns citizens to build/repair buildings.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct BuildRepairCommandSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (cmd, source, entity) in
SystemAPI.Query<RefRO<BuildRepairCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
.WithEntityAccess())
{
var citizen = cmd.ValueRO.CitizenEntity;
var building = cmd.ValueRO.BuildingEntity;
if (state.EntityManager.Exists(citizen) && state.EntityManager.Exists(building) &&
state.EntityManager.HasComponent<CitizenStateComponent>(citizen))
{
state.EntityManager.SetComponentData(citizen, new BuildTarget { Target = building });
state.EntityManager.SetComponentData(citizen, new CitizenStateComponent { Value = CitizenState.MovingToBuild });
var buildingPos = state.EntityManager.GetComponentData<LocalTransform>(building).Position;
state.EntityManager.SetComponentData(citizen, new MoveTarget
{
Position = buildingPos,
IsActive = true
});
}
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
/// <summary>
/// Server-side: ticks construction progress for buildings with assigned citizens.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateAfter(typeof(BuildRepairCommandSystem))]
public partial struct ConstructionSystem : ISystem
{
private float _timer;
public void OnCreate(ref SystemState state)
{
_timer = 0f;
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
_timer += dt;
if (_timer < GameConstants.ConstructionTickInterval) return;
_timer -= GameConstants.ConstructionTickInterval;
var ecb = new EntityCommandBuffer(Allocator.Temp);
// Count builders per building
// Simple approach: iterate citizens in Building state
foreach (var (citizenState, buildTarget, transform, moveTarget) in
SystemAPI.Query<RefRW<CitizenStateComponent>, RefRO<BuildTarget>,
RefRO<LocalTransform>, RefRO<MoveTarget>>()
.WithAll<CitizenTag>())
{
if (citizenState.ValueRO.Value == CitizenState.MovingToBuild && !moveTarget.ValueRO.IsActive)
{
citizenState.ValueRW.Value = CitizenState.Building;
}
if (citizenState.ValueRO.Value != CitizenState.Building) continue;
var buildingEntity = buildTarget.ValueRO.Target;
if (buildingEntity == Entity.Null || !state.EntityManager.Exists(buildingEntity)) continue;
if (!state.EntityManager.HasComponent<ConstructionProgress>(buildingEntity)) continue;
var progress = state.EntityManager.GetComponentData<ConstructionProgress>(buildingEntity);
float increment = GameConstants.ConstructionTickInterval / progress.BuildTime;
progress.Progress = math.min(progress.Progress + increment, 1f);
state.EntityManager.SetComponentData(buildingEntity, progress);
// Complete construction
if (progress.Progress >= 1f)
{
ecb.RemoveComponent<UnderConstructionTag>(buildingEntity);
ecb.RemoveComponent<ConstructionProgress>(buildingEntity);
// Set health to max
var health = state.EntityManager.GetComponentData<Health>(buildingEntity);
health.Current = health.Max;
ecb.SetComponent(buildingEntity, health);
// Citizen becomes idle
citizenState.ValueRW.Value = CitizenState.Idle;
}
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
/// <summary>
/// Server-side: processes production queues on buildings and spawns units.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct ProductionSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (productionQueue, rallyPoint, owner, transform, entity) in
SystemAPI.Query<DynamicBuffer<ProductionQueueElement>, RefRO<RallyPoint>,
RefRO<OwnerPlayer>, RefRO<LocalTransform>>()
.WithAll<BuildingTag>()
.WithNone<UnderConstructionTag>()
.WithEntityAccess())
{
if (productionQueue.Length == 0) continue;
var front = productionQueue[0];
front.TimeRemaining -= dt;
if (front.TimeRemaining <= 0)
{
// TODO: Instantiate unit prefab based on front.UnitDataId
// For now just log
UnityEngine.Debug.Log($"[Server] Unit produced: DataId={front.UnitDataId} at {rallyPoint.ValueRO.Position}");
productionQueue.RemoveAt(0);
}
else
{
productionQueue[0] = front;
}
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e4a7a8d853e94f941b762acafa1de53d

View File

@@ -0,0 +1,207 @@
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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1163cc3a2970481429c3bd67549beb78

View File

@@ -0,0 +1,172 @@
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 GatherCommandRpc — assigns a gather target to citizens.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct GatherCommandSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (cmd, source, entity) in
SystemAPI.Query<RefRO<GatherCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
.WithEntityAccess())
{
var citizen = cmd.ValueRO.CitizenEntity;
var node = cmd.ValueRO.ResourceNodeEntity;
if (state.EntityManager.Exists(citizen) && state.EntityManager.Exists(node) &&
state.EntityManager.HasComponent<CitizenStateComponent>(citizen) &&
state.EntityManager.HasComponent<ResourceNode>(node))
{
state.EntityManager.SetComponentData(citizen, new GatherTarget { Target = node });
state.EntityManager.SetComponentData(citizen, new CitizenStateComponent { Value = CitizenState.MovingToGather });
// Set move target to the resource node's position
var nodePos = state.EntityManager.GetComponentData<LocalTransform>(node).Position;
state.EntityManager.SetComponentData(citizen, new MoveTarget
{
Position = nodePos,
IsActive = true
});
}
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
/// <summary>
/// Server-side: citizen gathering state machine.
/// Handles: MovingToGather → Gathering → MovingToDropoff → Depositing → repeat.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateAfter(typeof(GatherCommandSystem))]
public partial struct GatheringSystem : ISystem
{
private float _gatherTimer;
public void OnCreate(ref SystemState state)
{
_gatherTimer = 0f;
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
_gatherTimer += dt;
bool gatherTick = _gatherTimer >= GameConstants.GatherTickInterval;
if (gatherTick) _gatherTimer -= GameConstants.GatherTickInterval;
foreach (var (citizenState, gatherTarget, carried, transform, moveTarget, owner) in
SystemAPI.Query<RefRW<CitizenStateComponent>, RefRW<GatherTarget>,
RefRW<CarriedResource>, RefRO<LocalTransform>, RefRW<MoveTarget>, RefRO<OwnerPlayer>>()
.WithAll<CitizenTag>())
{
switch (citizenState.ValueRO.Value)
{
case CitizenState.MovingToGather:
// Check if arrived (MoveTarget will be deactivated by movement system)
if (!moveTarget.ValueRO.IsActive)
{
citizenState.ValueRW.Value = CitizenState.Gathering;
}
break;
case CitizenState.Gathering:
if (!gatherTick) break;
var targetEntity = gatherTarget.ValueRO.Target;
if (targetEntity == Entity.Null || !state.EntityManager.Exists(targetEntity))
{
citizenState.ValueRW.Value = CitizenState.Idle;
break;
}
var node = state.EntityManager.GetComponentData<ResourceNode>(targetEntity);
if (node.RemainingAmount <= 0)
{
citizenState.ValueRW.Value = CitizenState.Idle;
gatherTarget.ValueRW.Target = Entity.Null;
break;
}
// Gather one unit
int gatherAmount = math.min(1, node.RemainingAmount);
gatherAmount = math.min(gatherAmount, carried.ValueRO.MaxCarryCapacity - carried.ValueRO.Amount);
if (gatherAmount > 0)
{
node.RemainingAmount -= gatherAmount;
state.EntityManager.SetComponentData(targetEntity, node);
carried.ValueRW.Amount += gatherAmount;
carried.ValueRW.Type = node.Type;
}
// If carrying capacity full, go to dropoff
if (carried.ValueRO.Amount >= carried.ValueRO.MaxCarryCapacity)
{
citizenState.ValueRW.Value = CitizenState.MovingToDropoff;
// TODO: Find nearest dropoff building and set as move target
// For now, move back toward origin as placeholder
moveTarget.ValueRW = new MoveTarget
{
Position = float3.zero,
IsActive = true
};
}
break;
case CitizenState.MovingToDropoff:
if (!moveTarget.ValueRO.IsActive)
{
citizenState.ValueRW.Value = CitizenState.Depositing;
}
break;
case CitizenState.Depositing:
// Deposit resources into player economy
// TODO: Find PlayerResourcesComponent for this owner and add resources
carried.ValueRW.Amount = 0;
// Go back to gather if the node still exists
if (gatherTarget.ValueRO.Target != Entity.Null &&
state.EntityManager.Exists(gatherTarget.ValueRO.Target))
{
var nodePos = state.EntityManager.GetComponentData<LocalTransform>(gatherTarget.ValueRO.Target).Position;
moveTarget.ValueRW = new MoveTarget
{
Position = nodePos,
IsActive = true
};
citizenState.ValueRW.Value = CitizenState.MovingToGather;
}
else
{
citizenState.ValueRW.Value = CitizenState.Idle;
}
break;
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 35e43773511035f4bb29174417932b52

View File

@@ -0,0 +1,151 @@
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 system that processes MoveCommandRpc and sets MoveTarget on units.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct MoveCommandSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (cmd, source, entity) in
SystemAPI.Query<RefRO<MoveCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
.WithEntityAccess())
{
var unitEntity = cmd.ValueRO.UnitEntity;
if (state.EntityManager.Exists(unitEntity) &&
state.EntityManager.HasComponent<MoveTarget>(unitEntity))
{
state.EntityManager.SetComponentData(unitEntity, new MoveTarget
{
Position = cmd.ValueRO.TargetPosition,
IsActive = true
});
// Clear combat target when moving
if (state.EntityManager.HasComponent<CombatTarget>(unitEntity))
{
state.EntityManager.SetComponentData(unitEntity, new CombatTarget
{
Target = Entity.Null
});
}
// Set state to Moving
if (state.EntityManager.HasComponent<UnitStateComponent>(unitEntity))
{
state.EntityManager.SetComponentData(unitEntity, new UnitStateComponent
{
Value = UnitState.Moving
});
}
}
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
/// <summary>
/// Server-side system that moves units toward their MoveTarget.
/// Placeholder for flow-field pathfinding — currently uses direct steering.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
[UpdateAfter(typeof(MoveCommandSystem))]
public partial struct UnitMovementSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
foreach (var (transform, moveTarget, speed, unitState) in
SystemAPI.Query<RefRW<LocalTransform>, RefRW<MoveTarget>, RefRO<MovementSpeed>, RefRW<UnitStateComponent>>()
.WithAll<UnitTag>())
{
if (!moveTarget.ValueRO.IsActive) continue;
float3 pos = transform.ValueRO.Position;
float3 target = moveTarget.ValueRO.Position;
float3 direction = target - pos;
direction.y = 0; // Keep on ground plane
float distance = math.length(direction);
if (distance < 0.5f)
{
// Arrived
moveTarget.ValueRW.IsActive = false;
if (unitState.ValueRO.Value == UnitState.Moving)
unitState.ValueRW.Value = UnitState.Idle;
}
else
{
float3 moveDir = math.normalize(direction);
float moveAmount = speed.ValueRO.Value * dt;
moveAmount = math.min(moveAmount, distance);
var newPos = pos + moveDir * moveAmount;
transform.ValueRW.Position = newPos;
// Face movement direction
transform.ValueRW.Rotation = quaternion.LookRotationSafe(moveDir, math.up());
}
}
}
}
/// <summary>
/// Server-side: processes StopCommandRpc — halts unit movement and clears targets.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct StopCommandSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (cmd, source, entity) in
SystemAPI.Query<RefRO<StopCommandRpc>, RefRO<ReceiveRpcCommandRequest>>()
.WithEntityAccess())
{
var unitEntity = cmd.ValueRO.UnitEntity;
if (state.EntityManager.Exists(unitEntity))
{
if (state.EntityManager.HasComponent<MoveTarget>(unitEntity))
state.EntityManager.SetComponentData(unitEntity, new MoveTarget { IsActive = false });
if (state.EntityManager.HasComponent<CombatTarget>(unitEntity))
state.EntityManager.SetComponentData(unitEntity, new CombatTarget { Target = Entity.Null });
if (state.EntityManager.HasComponent<UnitStateComponent>(unitEntity))
state.EntityManager.SetComponentData(unitEntity, new UnitStateComponent { Value = UnitState.Idle });
}
ecb.DestroyEntity(entity);
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ae6e692d140e8c24a9dd56224e814d43

View File

@@ -0,0 +1,67 @@
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.NetCode;
using EE2Clone.Components;
using EE2Clone.Core;
namespace EE2Clone.Systems
{
/// <summary>
/// Server-side: sums population-providing buildings per player and updates PopulationMax.
/// </summary>
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct PopulationSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Build a map of playerId → total pop capacity from buildings
var popCapMap = new NativeHashMap<int, int>(GameConstants.MaxPlayers, Allocator.Temp);
foreach (var (pop, owner) in
SystemAPI.Query<RefRO<ProvidesPopulation>, RefRO<OwnerPlayer>>()
.WithAll<BuildingTag>()
.WithNone<UnderConstructionTag>())
{
int playerId = owner.ValueRO.PlayerId;
if (popCapMap.TryGetValue(playerId, out int current))
popCapMap[playerId] = current + pop.ValueRO.Amount;
else
popCapMap[playerId] = GameConstants.StartingPopulationCap + pop.ValueRO.Amount;
}
// Count current population per player (units)
var popCurrentMap = new NativeHashMap<int, int>(GameConstants.MaxPlayers, Allocator.Temp);
foreach (var owner in SystemAPI.Query<RefRO<OwnerPlayer>>().WithAll<UnitTag>())
{
int playerId = owner.ValueRO.PlayerId;
if (popCurrentMap.TryGetValue(playerId, out int current))
popCurrentMap[playerId] = current + 1;
else
popCurrentMap[playerId] = 1;
}
// Update PlayerStateComponent
foreach (var playerState in SystemAPI.Query<RefRW<PlayerStateComponent>>())
{
int pid = playerState.ValueRO.PlayerId;
if (popCapMap.TryGetValue(pid, out int cap))
playerState.ValueRW.PopulationMax = System.Math.Min(cap, GameConstants.MaxPopulationCap);
else
playerState.ValueRW.PopulationMax = GameConstants.StartingPopulationCap;
if (popCurrentMap.TryGetValue(pid, out int current))
playerState.ValueRW.PopulationCurrent = current;
else
playerState.ValueRW.PopulationCurrent = 0;
}
popCapMap.Dispose();
popCurrentMap.Dispose();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e2ca22d7e5c3be04fb7f4d58ea50c36b