using System.Linq; using Content.Server.GameTicking.Rules.Components; using Content.Server.Chat.Managers; using Content.Server.Popups; using Content.Shared.GameTicking.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.NPC.Components; using Content.Shared.Interaction; using Content.Shared.Hands.EntitySystems; using Robust.Shared.Timing; using Content.Server.KillTracking; using Content.Shared.NPC.Systems; using Content.Server.RoundEnd; namespace Content.Server.GameTicking.Rules; /// /// Handles the Valley gamemode points system for Blugoslavia vs Insurgents /// public sealed class ValleyPointsRuleSystem : GameRuleSystem { [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedHandsSystem _hands = default!; [Dependency] private readonly RoundEndSystem _roundEndSystem = default!; [Dependency] private readonly NpcFactionSystem _factionSystem = default!; // Added dependency private ISawmill _sawmill = default!; private TimeSpan _lastSupplyBoxCheck = TimeSpan.Zero; private const float SupplyBoxCheckInterval = 30f; // Check every 30 seconds public override void Initialize() { base.Initialize(); _sawmill = _logManager.GetSawmill("valley-points"); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnCaptureAreaStartup); SubscribeLocalEvent(OnKillReported); } protected override void Started(EntityUid uid, ValleyPointsComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { component.GameStartTime = _timing.CurTime; component.LastCheckpointBonusTime = _timing.CurTime; component.LastScoreAnnouncementTime = _timing.CurTime; _lastSupplyBoxCheck = _timing.CurTime; // Initialize checkpoints immediately InitializeCheckpoints(component); // Delay civilian count initialization to allow spawning Timer.Spawn(TimeSpan.FromSeconds(5), () => InitializeCivilianCount(component)); _sawmill.Info("Valley gamemode started - 50 minute timer begins now"); AnnounceToAll("Valley conflict has begun! Blugoslavia and Insurgents fight for control."); } private void OnCaptureAreaStartup(EntityUid uid, CaptureAreaComponent component, ComponentStartup args) { if (HasComp(uid)) { component.CapturableFactions.Clear(); component.CapturableFactions.Add("Blugoslavia"); component.CapturableFactions.Add("Insurgents"); } } private void OnMobStateChanged(MobStateChangedEvent args) { if (args.NewMobState == MobState.Dead && args.OldMobState == MobState.Alive) { if (TryComp(args.Target, out var factionMember) && factionMember.Factions.Any(f => f == "Blugoslavia")) { var ruleQuery = EntityQueryEnumerator(); if (ruleQuery.MoveNext(out var ruleEntity, out _)) { AwardInsurgentKill(ruleEntity); } } } } private void InitializeCivilianCount(ValleyPointsComponent component) { // Count civilian NPCs on the map var civilianCount = 0; var query = EntityQueryEnumerator(); while (query.MoveNext(out _, out var faction)) { if (faction.Factions.Any(f => f == "Civilian")) civilianCount++; } component.InitialCivilianCount = civilianCount; component.TotalCivilianNPCs = civilianCount; _sawmill.Info($"Initialized with {civilianCount} civilian NPCs"); } private void InitializeCheckpoints(ValleyPointsComponent valley) { var checkpointQuery = EntityQueryEnumerator(); while (checkpointQuery.MoveNext(out var uid, out var checkpoint, out var area)) { area.CapturableFactions.Clear(); area.CapturableFactions.Add("Blugoslavia"); area.CapturableFactions.Add("Insurgents"); _sawmill.Info($"Initialized Valley checkpoint: {area.Name}"); } } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var valley, out var gameRule)) { if (!GameTicker.IsGameRuleAdded(uid, gameRule)) continue; if (valley.GameEnded) continue; UpdateCheckpointHolding(valley); UpdateSupplyBoxSecuring(valley); CheckCaptureAreaControl(valley); CheckSupplyBoxDeliveries(valley); CheckScoreAnnouncement(valley); CheckWinConditions(uid, valley); CheckTimeLimit(uid, valley); } } private void CheckSupplyBoxDeliveries(ValleyPointsComponent valley) { var currentTime = _timing.CurTime; // Only check every 30 seconds if ((currentTime - _lastSupplyBoxCheck).TotalSeconds < SupplyBoxCheckInterval) return; _lastSupplyBoxCheck = currentTime; // Check all capture areas for nearby supply boxes var areaQuery = EntityQueryEnumerator(); while (areaQuery.MoveNext(out var areaUid, out var area)) { var areaPos = _transform.GetMapCoordinates(areaUid); var nearbyEntities = _lookup.GetEntitiesInRange(areaPos, 3f); foreach (var entity in nearbyEntities) { if (TryComp(entity, out var supplyBox) && !supplyBox.Delivered) { // Check if this is a checkpoint controlled by Blugoslavia if (HasComp(areaUid) && area.Controller == "Blugoslavia") { ProcessBlugoslavianSupplyDelivery(valley, entity, areaUid, area); } // Check if this is the Insurgent base else if (IsInsurgentBase(areaUid, area)) { ProcessInsurgentSupplyTheft(valley, entity, areaUid, area); } } } } } private bool IsInsurgentBase(EntityUid areaUid, CaptureAreaComponent area) { // Check if this area is marked as insurgent base return area.Name.ToLower().Contains("insurgent"); } private void ProcessBlugoslavianSupplyDelivery(ValleyPointsComponent valley, EntityUid supplyBox, EntityUid checkpoint, CaptureAreaComponent area) { if (!TryComp(supplyBox, out var boxComp)) return; boxComp.Delivered = true; boxComp.SecuringAtCheckpoint = checkpoint; // Start securing timer valley.SecuringSupplyBoxes[supplyBox] = _timing.CurTime; if (TryComp(checkpoint, out var checkpointComp)) { checkpointComp.SecuringBoxes.Add(supplyBox); } _sawmill.Info($"Supply box delivery started at {area.Name}, securing for {valley.SupplyBoxSecureTime} seconds"); } private void ProcessInsurgentSupplyTheft(ValleyPointsComponent valley, EntityUid supplyBox, EntityUid baseArea, CaptureAreaComponent area) { if (!TryComp(supplyBox, out var boxComp)) return; boxComp.Delivered = true; // Award points immediately for insurgent theft valley.InsurgentPoints += valley.StolenSupplyBoxPoints; _sawmill.Info($"Insurgents awarded {valley.StolenSupplyBoxPoints} points for stolen supply box at {area.Name}. Total: {valley.InsurgentPoints}"); AnnounceToAll($"Insurgents: +{valley.StolenSupplyBoxPoints} points (Supply Theft at {area.Name}) - Total: {valley.InsurgentPoints}"); // Remove the supply box QueueDel(supplyBox); } private void CheckCaptureAreaControl(ValleyPointsComponent valley) { var checkpointQuery = EntityQueryEnumerator(); while (checkpointQuery.MoveNext(out var uid, out var checkpoint, out var area)) { var blugoslaviaControlled = area.Controller == "Blugoslavia"; if (checkpoint.BlugoslaviaControlled != blugoslaviaControlled) { checkpoint.BlugoslaviaControlled = blugoslaviaControlled; SetCheckpointControl(valley, uid, blugoslaviaControlled); } } } public void SetCheckpointControl(ValleyPointsComponent valley, EntityUid checkpoint, bool blugoslaviaControlled) { if (blugoslaviaControlled) { if (!valley.BlugoslaviaHeldCheckpoints.Contains(checkpoint)) { valley.BlugoslaviaHeldCheckpoints.Add(checkpoint); valley.CheckpointHoldStartTimes[checkpoint] = _timing.CurTime; _sawmill.Info($"Blugoslavia gained control of checkpoint {checkpoint}"); } } else { if (valley.BlugoslaviaHeldCheckpoints.Contains(checkpoint)) { valley.BlugoslaviaHeldCheckpoints.Remove(checkpoint); valley.CheckpointHoldStartTimes.Remove(checkpoint); _sawmill.Info($"Blugoslavia lost control of checkpoint {checkpoint}"); } } } private void UpdateCheckpointHolding(ValleyPointsComponent valley) { var currentTime = _timing.CurTime; var checkpointsToAward = new List(); // Check individual checkpoint holding - award points every minute (60 seconds) foreach (var kvp in valley.CheckpointHoldStartTimes.ToList()) { var checkpoint = kvp.Key; var startTime = kvp.Value; if ((currentTime - startTime).TotalSeconds >= 60f) // 1 minute instead of 5 { checkpointsToAward.Add(checkpoint); valley.CheckpointHoldStartTimes[checkpoint] = currentTime; } } if (checkpointsToAward.Count > 0) { var pointsAwarded = checkpointsToAward.Count * 5; // 5 points per minute instead of 25 per 5 minutes valley.BlugoslaviaPoints += pointsAwarded; _sawmill.Info($"Blugoslavia awarded {pointsAwarded} points for holding {checkpointsToAward.Count} checkpoints"); AnnounceToAll($"Blugoslavia: +{pointsAwarded} points (Checkpoint Control) - Total: {valley.BlugoslaviaPoints}"); } // Check for all checkpoints bonus - still every 5 minutes but reduced frequency if (valley.BlugoslaviaHeldCheckpoints.Count >= 4 && // Assuming 4 checkpoints total (currentTime - valley.LastCheckpointBonusTime).TotalSeconds >= valley.CheckpointBonusInterval) { valley.BlugoslaviaPoints += valley.AllCheckpointsBonusPoints; valley.LastCheckpointBonusTime = currentTime; _sawmill.Info($"Blugoslavia awarded {valley.AllCheckpointsBonusPoints} bonus points for controlling all checkpoints"); AnnounceToAll($"Blugoslavia: +{valley.AllCheckpointsBonusPoints} points (All Checkpoints Bonus) - Total: {valley.BlugoslaviaPoints}"); } } private void UpdateSupplyBoxSecuring(ValleyPointsComponent valley) { var currentTime = _timing.CurTime; var securedBoxes = new List(); // Check all checkpoints for securing boxes var checkpointQuery = EntityQueryEnumerator(); while (checkpointQuery.MoveNext(out var checkpointUid, out var checkpoint)) { var boxesToRemove = new List(); foreach (var boxUid in checkpoint.SecuringBoxes) { if (!TryComp(boxUid, out var boxComp)) { boxesToRemove.Add(boxUid); continue; } // Check if box is still near the checkpoint var boxPos = _transform.GetMapCoordinates(boxUid); var checkpointPos = _transform.GetMapCoordinates(checkpointUid); if ((boxPos.Position - checkpointPos.Position).Length() > 3f) { // Box moved away, cancel securing boxesToRemove.Add(boxUid); boxComp.Delivered = false; boxComp.SecuringAtCheckpoint = null; _sawmill.Info("Supply box moved away from checkpoint, delivery cancelled"); continue; } // Check if securing time has elapsed if (valley.SecuringSupplyBoxes.TryGetValue(boxUid, out var startTime) && (currentTime - startTime).TotalSeconds >= valley.SupplyBoxSecureTime) { securedBoxes.Add(boxUid); boxesToRemove.Add(boxUid); } } // Remove boxes that are no longer securing foreach (var box in boxesToRemove) { checkpoint.SecuringBoxes.Remove(box); } } // Award points for secured boxes foreach (var box in securedBoxes) { valley.SecuringSupplyBoxes.Remove(box); valley.BlugoslaviaPoints += valley.SupplyBoxDeliveryPoints; _sawmill.Info($"Blugoslavia awarded {valley.SupplyBoxDeliveryPoints} points for secured supply box delivery"); AnnounceToAll($"Blugoslavia: +{valley.SupplyBoxDeliveryPoints} points (Supply Delivery) - Total: {valley.BlugoslaviaPoints}"); // Delete the secured box QueueDel(box); } } /// /// Award points to insurgents for killing a Blugoslavian soldier. /// public void AwardInsurgentKill(EntityUid ruleEntity) { if (!TryComp(ruleEntity, out var valley)) return; valley.InsurgentPoints += valley.KillPoints; _sawmill.Info($"Insurgents awarded {valley.KillPoints} points for kill. Total: {valley.InsurgentPoints}"); AnnounceToAll($"Insurgents: +{valley.KillPoints} points (Kill) - Total: {valley.InsurgentPoints}"); } /// /// Award points to Blugoslavia for successfully escorting a convoy. /// public void AwardConvoyEscort(EntityUid ruleEntity) { if (!TryComp(ruleEntity, out var valley)) return; valley.BlugoslaviaPoints += valley.ConvoyEscortPoints; _sawmill.Info($"Blugoslavia awarded {valley.ConvoyEscortPoints} points for convoy escort. Total: {valley.BlugoslaviaPoints}"); AnnounceToAll($"Blugoslavia: +{valley.ConvoyEscortPoints} points (Convoy Escort) - Total: {valley.BlugoslaviaPoints}"); } private void CheckWinConditions(EntityUid uid, ValleyPointsComponent valley) { if (valley.BlugoslaviaPoints >= valley.PointsToWin) { EndGame(uid, valley, "Blugoslavia"); } else if (valley.InsurgentPoints >= valley.PointsToWin) { EndGame(uid, valley, "Insurgents"); } } private void CheckTimeLimit(EntityUid uid, ValleyPointsComponent valley) { var elapsed = (_timing.CurTime - valley.GameStartTime).TotalMinutes; if (elapsed >= valley.MatchDurationMinutes) { // Determine winner by points if (valley.BlugoslaviaPoints > valley.InsurgentPoints) { EndGame(uid, valley, "Blugoslavia"); } else if (valley.InsurgentPoints > valley.BlugoslaviaPoints) { EndGame(uid, valley, "Insurgents"); } else { EndGame(uid, valley, "Draw"); } } } private void EndGame(EntityUid uid, ValleyPointsComponent valley, string winner) { valley.GameEnded = true; var finalMessage = winner switch { "Blugoslavia" => $"VICTORY: Blugoslavia wins with {valley.BlugoslaviaPoints} points!", "Insurgents" => $"VICTORY: Insurgents win with {valley.InsurgentPoints} points!", "Draw" => $"DRAW: Match ended in a tie! Blugoslavia: {valley.BlugoslaviaPoints}, Insurgents: {valley.InsurgentPoints}", _ => "Match ended." }; _sawmill.Info($"Valley gamemode ended: {finalMessage}"); AnnounceToAll(finalMessage); _roundEndSystem.EndRound(); } private string CheckUNObjectives(ValleyPointsComponent valley) { var civilianSurvivalRate = valley.TotalCivilianNPCs > 0 ? (float)valley.AliveCivilianNPCs / valley.TotalCivilianNPCs : 1.0f; var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var area)) { if (area.Name == "UN Hospital") { if (area.Occupied == true) { valley.UNHospitalZoneControlled = false; } else { valley.UNHospitalZoneControlled = true; } } } var unSuccess = civilianSurvivalRate >= valley.RequiredCivilianSurvivalRate && valley.UNHospitalZoneControlled && valley.UNNeutralityMaintained; var unMessage = unSuccess ? $"UN OBJECTIVES COMPLETED: {civilianSurvivalRate:P0} civilian survival rate maintained, hospital zone secured, neutrality preserved." : $"UN OBJECTIVES FAILED: {civilianSurvivalRate:P0} civilian survival rate, hospital zone: {(valley.UNHospitalZoneControlled ? "Secured" : "Lost")}, neutrality: {(valley.UNNeutralityMaintained ? "Maintained" : "Violated")}"; _sawmill.Info(unMessage); return unMessage; } private void AnnounceToAll(string message) { _chatManager.DispatchServerAnnouncement(message); } protected override void AppendRoundEndText(EntityUid uid, ValleyPointsComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args) { if (component.BlugoslaviaPoints > component.InsurgentPoints) { args.AddLine($"[color=lime]Blugoslavia[/color] has won!"); } else if (component.BlugoslaviaPoints < component.InsurgentPoints) { args.AddLine($"[color=lime]Insurgents[/color] have won!"); } else { args.AddLine("The round ended in a [color=yellow]draw[/color]!"); } args.AddLine(""); args.AddLine($"Blugoslavia: {component.BlugoslaviaPoints} points"); args.AddLine($"Insurgents: {component.InsurgentPoints} points"); args.AddLine(""); args.AddLine($"UN Objectives:"); args.AddLine(CheckUNObjectives(component)); } private void OnKillReported(ref KillReportedEvent ev) { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var valley, out var rule)) { if (!GameTicker.IsGameRuleActive(uid, rule)) continue; // Check if UN member was involved (either as killer or victim) TODO: Check killer bool unInvolved = false; // Check if victim is UN if (TryComp(ev.Entity, out var victimFaction)) { if (_factionSystem.IsMember(ev.Entity, "UnitedNations")) { unInvolved = true; } // If UN was involved, set neutrality to false if (unInvolved && valley.UNNeutralityMaintained) { valley.UNNeutralityMaintained = false; _sawmill.Info("UN neutrality violated - UN member involved in combat"); AnnounceToAll("UN neutrality has been violated!"); } // Award points to Insurgents for killing Blugoslavian soldiers if (TryComp(ev.Entity, out var deadFaction) && deadFaction.Factions.Any(f => f == "Blugoslavia")) { valley.InsurgentPoints += valley.KillPoints; _sawmill.Info($"Insurgents awarded {valley.KillPoints} points for Blugoslavian kill. Total: {valley.InsurgentPoints}"); AnnounceToAll($"Insurgents: +{valley.KillPoints} points (Kill) - Total: {valley.InsurgentPoints}"); } } } } private void CheckScoreAnnouncement(ValleyPointsComponent valley) { var currentTime = _timing.CurTime; // Announce scores every 5 minutes (300 seconds) if ((currentTime - valley.LastScoreAnnouncementTime).TotalSeconds >= 300f) { valley.LastScoreAnnouncementTime = currentTime; var elapsedMinutes = (currentTime - valley.GameStartTime).TotalMinutes; var remainingMinutes = valley.MatchDurationMinutes - elapsedMinutes; var scoreMessage = $"SCORE UPDATE ({remainingMinutes:F0} minutes remaining): " + $"Blugoslavia: {valley.BlugoslaviaPoints} points | " + $"Insurgents: {valley.InsurgentPoints} points"; _sawmill.Info($"Score announcement: {scoreMessage}"); AnnounceToAll(scoreMessage); } } }