using Content.Server._DV.CosmicCult.Components; using Content.Server.Actions; using Content.Server.Administration.Logs; using Content.Server.Antag; using Content.Server.Atmos.Components; using Content.Server.Audio; using Content.Server.Bible.Components; using Content.Server.Chat.Systems; using Content.Server.EUI; using Content.Server.GameTicking.Rules; using Content.Server.GameTicking; using Content.Server.Ghost; using Content.Server.Objectives.Components; using Content.Server.Polymorph.Components; using Content.Server.Popups; using Content.Server.RoundEnd; using Content.Server.Shuttles.Systems; using Content.Shared.Cuffs; using Content.Shared.Cuffs.Components; using Content.Server.Voting.Managers; using Content.Server.Voting; using Content.Shared._DV.CCVars; using Content.Shared._DV.CosmicCult.Components.Examine; using Content.Shared._DV.CosmicCult.Components; using Content.Shared._DV.CosmicCult.Prototypes; using Content.Shared._DV.CosmicCult; using Content.Shared._DV.Roles; using Content.Shared.Alert; using Content.Shared.Audio; using Content.Shared.Body.Systems; using Content.Shared.Coordinates; using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.GameTicking.Components; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Light.Components; using Content.Shared.Mind; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Mobs; using Content.Shared.Movement.Systems; using Content.Shared.Parallax; using Content.Shared.Popups; using Content.Shared.Radio.Components; using Content.Shared.Roles; using Content.Shared.Roles.Components; using Content.Shared.Stunnable; using Robust.Server.Audio; using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Audio; using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Timing; using System.Collections.Immutable; using System.Linq; namespace Content.Server._DV.CosmicCult; /// /// Where all the main stuff for Cosmic Cultists happens. /// public sealed class CosmicCultRuleSystem : GameRuleSystem { [Dependency] private readonly ActionsSystem _actions = default!; [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly AntagSelectionSystem _antag = default!; [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly ChatSystem _chatSystem = default!; [Dependency] private readonly DamageableSystem _damage = default!; [Dependency] private readonly EmergencyShuttleSystem _emergency = default!; [Dependency] private readonly EuiManager _euiMan = default!; [Dependency] private readonly GhostSystem _ghost = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IConfigurationManager _config = default!; [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPlayerManager _playerMan = default!; [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly IRobustRandom _rand = default!; [Dependency] private readonly IVoteManager _votes = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly MonumentSystem _monument = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly RoundEndSystem _roundEnd = default!; [Dependency] private readonly ServerGlobalSoundSystem _sound = default!; [Dependency] private readonly SharedBodySystem _body = default!; [Dependency] private readonly SharedEyeSystem _eye = default!; [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SharedRoleSystem _role = default!; [Dependency] private readonly SharedStunSystem _stun = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; [Dependency] private readonly VisibilitySystem _visibility = default!; private ISawmill _sawmill = default!; private TimeSpan _t3RevealDelay = default!; private TimeSpan _t2RevealDelay = default!; private TimeSpan _finaleDelay = default!; private TimeSpan _voteDelay = default!; private TimeSpan _voteTimer = default!; private readonly SoundSpecifier _briefingSound = new SoundPathSpecifier("/Audio/_DV/CosmicCult/antag_cosmic_briefing.ogg"); private readonly SoundSpecifier _deconvertSound = new SoundPathSpecifier("/Audio/_DV/CosmicCult/antag_cosmic_deconvert.ogg"); private readonly SoundSpecifier _tier3Sound = new SoundPathSpecifier("/Audio/_DV/CosmicCult/tier3.ogg"); private readonly SoundSpecifier _tier2Sound = new SoundPathSpecifier("/Audio/_DV/CosmicCult/tier2.ogg"); private readonly SoundSpecifier _monumentAlert = new SoundPathSpecifier("/Audio/_DV/CosmicCult/tier_up.ogg"); /// /// Mind role to add to cultists. /// public static readonly EntProtoId MindRole = "MindRoleCosmicCult"; public override void Initialize() { base.Initialize(); _sawmill = IoCManager.Resolve().GetSawmill("cosmiccult"); SubscribeLocalEvent(OnRunLevelChanged); SubscribeLocalEvent(OnAssociateRule); SubscribeLocalEvent(OnAntagSelect); SubscribeLocalEvent(OnComponentShutdown); SubscribeLocalEvent(OnGodSpawn); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnCuffStateChanged); Subs.CVar(_config, DCCVars.CosmicCultT2RevealDelaySeconds, value => _t2RevealDelay = TimeSpan.FromSeconds(value), true); Subs.CVar(_config, DCCVars.CosmicCultT3RevealDelaySeconds, value => _t3RevealDelay = TimeSpan.FromSeconds(value), true); Subs.CVar(_config, DCCVars.CosmicCultFinaleDelaySeconds, value => _finaleDelay = TimeSpan.FromSeconds(value), true); Subs.CVar(_config, DCCVars.CosmicCultStewardVoteTimer, value => _voteTimer = TimeSpan.FromSeconds(value), true); Subs.CVar(_config, DCCVars.CosmicCultStewardVoteDelayTimer, value => _voteDelay = TimeSpan.FromSeconds(value), true); } #region Starting Events protected override void Started(EntityUid uid, CosmicCultRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { component.StewardVoteTimer = _timing.CurTime + _voteDelay; } protected override void ActiveTick(EntityUid uid, CosmicCultRuleComponent component, GameRuleComponent gameRule, float frameTime) { if (component.StewardVoteTimer is { } voteTimer && _timing.CurTime >= voteTimer) { component.StewardVoteTimer = null; StewardVote(); } if (component.ExtraRiftTimer is { } riftTimer && _timing.CurTime >= riftTimer && !component.RiftStop) { component.ExtraRiftTimer = _timing.CurTime + _rand.Next(TimeSpan.FromSeconds(230), TimeSpan.FromSeconds(360)); //3min50 to 6min between new rifts. Seconds instead of minutes for granularity. SpawnRift(); } if (component.PrepareFinaleTimer is { } finalePrepTimer && _timing.CurTime >= finalePrepTimer) { component.PrepareFinaleTimer = null; var sender = Loc.GetString("cosmiccult-announcement-sender"); var mapData = _map.GetMap(_transform.GetMapId(component.MonumentInGame.Owner.ToCoordinates())); _chatSystem.DispatchStationAnnouncement(component.MonumentInGame, Loc.GetString("cosmiccult-announce-pre-finale-progress"), sender, false, null, Color.FromHex("#4cabb3")); _chatSystem.DispatchStationAnnouncement(component.MonumentInGame, Loc.GetString("cosmiccult-announce-pre-finale-warning"), null, false, null, Color.FromHex("#cae8e8")); _audio.PlayGlobal(_tier3Sound, Filter.Broadcast(), false, AudioParams.Default); EnsureComp(mapData, out var parallax); parallax.Parallax = "CosmicFinaleParallax"; Dirty(mapData, parallax); EnsureComp(mapData, out var mapLight); mapLight.AmbientLightColor = Color.FromHex("#210746"); Dirty(mapData, mapLight); var lights = EntityQueryEnumerator(); while (lights.MoveNext(out var light, out _)) { if (!_rand.Prob(0.50f)) continue; _ghost.DoGhostBooEvent(light); } if (TryComp(component.MonumentInGame, out var finaleComp)) { _monument.ReadyFinale(component.MonumentInGame, finaleComp); UpdateCultData(component.MonumentInGame); //duplicated work but it looks nicer than calling updateAppearance on it's own return; } } if (component.Tier3DelayTimer is { } tier3Timer && _timing.CurTime >= tier3Timer) { component.Tier3DelayTimer = null; //do spooky things var query = EntityQueryEnumerator(); while (query.MoveNext(out var cultist, out var cultComp)) { EnsureComp(cultist); } var sender = Loc.GetString("cosmiccult-announcement-sender"); var mapData = _map.GetMap(_transform.GetMapId(component.MonumentInGame.Owner.ToCoordinates())); _chatSystem.DispatchStationAnnouncement(component.MonumentInGame, Loc.GetString("cosmiccult-announce-tier3-progress"), sender, false, null, Color.FromHex("#4cabb3")); _chatSystem.DispatchStationAnnouncement(component.MonumentInGame, Loc.GetString("cosmiccult-announce-tier3-warning"), null, false, null, Color.FromHex("#cae8e8")); _audio.PlayGlobal(_tier2Sound, Filter.Broadcast(), false, AudioParams.Default); var lights = EntityQueryEnumerator(); while (lights.MoveNext(out var light, out _)) { if (!_rand.Prob(0.25f)) continue; _ghost.DoGhostBooEvent(light); } var collideQuery = EntityQueryEnumerator(); while (collideQuery.MoveNext(out var collideEnt, out var collideComp)) { collideComp.HasCollision = true; Dirty(collideEnt, collideComp); } if (TryComp(component.MonumentInGame, out var visComp)) _visibility.SetLayer((component.MonumentInGame, visComp), 1); component.MonumentSlowZone = Spawn("MonumentSlowZone", Transform(component.MonumentInGame).Coordinates); // spawn The Monument's slowing fixture entity that supresses non-cult / non-mindshielded / non-chaplain crew. _monument.SetCanTierUp(component.MonumentInGame, true); UpdateCultData(component.MonumentInGame); //instantly go up a tier if they manage it. _ui.SetUiState(component.MonumentInGame.Owner, MonumentKey.Key, new MonumentBuiState(component.MonumentInGame.Comp)); //not sure if this is needed but I'll be safe } if (component.Tier2DelayTimer is { } tier2Timer && _timing.CurTime >= tier2Timer) { component.Tier2DelayTimer = null; component.ExtraRiftTimer = _timing.CurTime + TimeSpan.FromSeconds(15); //don't spooky effects /* var sender = Loc.GetString("cosmiccult-announcement-sender"); var mapData = _map.GetMap(_transform.GetMapId(component.MonumentInGame.Owner.ToCoordinates())); _chatSystem.DispatchStationAnnouncement(component.MonumentInGame, Loc.GetString("cosmiccult-announce-tier2-progress"), sender, false, null, Color.FromHex("#4cabb3")); _chatSystem.DispatchStationAnnouncement(component.MonumentInGame, Loc.GetString("cosmiccult-announce-tier2-warning"), null, false, null, Color.FromHex("#cae8e8")); _audio.PlayGlobal(_tier2Sound, Filter.Broadcast(), false, AudioParams.Default); var lights = EntityQueryEnumerator(); while (lights.MoveNext(out var light, out _)) { if (!_rand.Prob(0.50f)) continue; _ghost.DoGhostBooEvent(light); } */ for (var i = 0; i < Convert.ToInt16(component.TotalCrew / 6); i++) // spawn # malign rifts equal to 16.67% of the playercount { SpawnRift(); } _monument.SetCanTierUp(component.MonumentInGame, true); UpdateCultData(component.MonumentInGame); //instantly go up a tier if they manage it _ui.SetUiState(component.MonumentInGame.Owner, MonumentKey.Key, new MonumentBuiState(component.MonumentInGame.Comp)); //not sure if this is needed but I'll be safe } } private void StewardVote() { var cultists = new List<(string, EntityUid)>(); var cultQuery = EntityQueryEnumerator(); while (cultQuery.MoveNext(out var cult, out _, out var metadata)) { var playerInfo = metadata.EntityName; if (TryComp(cult, out var polyComp) && polyComp.Parent.HasValue) // If the cultist is polymorphed, we use the original entity instead and hope that they'll polymorph back eventually cultists.Add((playerInfo, polyComp.Parent.Value)); else cultists.Add((playerInfo, cult)); } var options = new VoteOptions { Title = Loc.GetString("cosmiccult-vote-steward-title"), InitiatorText = Loc.GetString("cosmiccult-vote-steward-initiator"), Duration = _voteTimer, VoterEligibility = VoteManager.VoterEligibility.CosmicCult }; foreach (var (name, ent) in cultists) { options.Options.Add((Loc.GetString(name), ent)); } var vote = _votes.CreateVote(options); vote.OnFinished += (_, args) => { EntityUid picked; if (args.Winner == null) { picked = (EntityUid)_rand.Pick(args.Winners); } else { picked = (EntityUid)args.Winner; } EnsureComp(picked); _adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Cult stewardship vote finished: {Identity.Entity(picked, EntityManager)} is now steward."); _antag.SendBriefing(picked, Loc.GetString("cosmiccult-vote-steward-briefing"), Color.FromHex("#4cabb3"), _monumentAlert); }; } private void SpawnRift() { if (TryFindRandomTile(out var _, out var _, out var _, out var coords)) { Spawn("CosmicMalignRift", coords); } } private void OnAntagSelect(Entity uid, ref AfterAntagEntitySelectedEvent args) { TryStartCult(args.EntityUid, uid); } #endregion #region Round & Objectives private void OnGodSpawn(Entity uid, ref ComponentInit args) { var query = QueryActiveRules(); while (query.MoveNext(out var ruleUid, out _, out var cultRule, out _)) { SetWinType((ruleUid, cultRule), WinType.CultComplete); // There's no coming back from this. Cult wins this round QueueDel(cultRule.MonumentInGame); // The monument doesn't need to stick around postround! Into the bin with you. QueueDel(cultRule.MonumentSlowZone); // cease exist _roundEnd.EndRound(); //Woo game over yeaaaah var spawnPoints = EntityManager.GetAllComponents(typeof(CosmicVoidSpawnComponent)).ToImmutableList(); if (spawnPoints.IsEmpty) { return; } var endQuery = EntityQueryEnumerator(); while (endQuery.MoveNext(out var player, out _, out _)) { var newSpawn = _rand.Pick(spawnPoints); var spawnTgt = Transform(newSpawn.Uid).Coordinates; Timer.Spawn(_rand.Next(TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(25)), () => { EndRoundVoid(player, spawnTgt, cultRule); }); } } } private void EndRoundVoid(EntityUid player, EntityCoordinates spawnTgt, CosmicCultRuleComponent cultRule) { if (!_mind.TryGetMind(player, out var mind, out _) || _mobStateSystem.IsDead(player)) return; if (cultRule.Cultists.Contains(player)) { var mob = Spawn(cultRule.CosmicAscended, spawnTgt); _mind.TransferTo(mind, mob); _metaData.SetEntityName(mob, Loc.GetString("cosmiccult-astral-ascendant", ("name", player))); //Renames cultists' ascendant forms to "[CharacterName], Ascendant" } else { var mob = Spawn(_rand.Pick(cultRule.CosmicMobs), spawnTgt); _mind.TransferTo(mind, mob); _metaData.SetEntityName(mob, Loc.GetString("cosmiccult-astral-minion", ("name", player))); //Renames non-cultists to "[CharacterName], Malign" } Spawn(cultRule.WarpVFX, spawnTgt); Spawn(cultRule.WarpVFX, Transform(player).Coordinates); _audio.PlayPvs(cultRule.WarpSFX, spawnTgt, AudioParams.Default.WithVolume(3f)); _body.GibBody(player); // you don't need that body anymore } private static void SetWinType(Entity ent, WinType type) { if (ent.Comp.WinLocked) return; ent.Comp.WinType = type; if (type is WinType.CultComplete or WinType.CrewComplete) //Let's lock in our WinType to prevent us from setting a worse win if a better win's been achieved. ent.Comp.WinLocked = true; } private void OnRunLevelChanged(GameRunLevelChangedEvent ev) { if (ev.New is not GameRunLevel.PostRound) //Are we moving to post-round? return; var query = QueryActiveRules(); while (query.MoveNext(out var uid, out _, out var cultRule, out _)) { ConfirmWinState((uid, cultRule)); //If so, let's consult our Winconditions and set an appropriate WinType. } } private bool CultistsAlive() { var query = EntityQueryEnumerator(); while (query.MoveNext(out var ent, out _, out var mobComp)) { if (TryComp(ent, out var cuffed) && cuffed.CuffedHandCount > 0) continue; if (mobComp.Running && mobComp.CurrentState != MobState.Dead) return true; } return false; } private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) { CheckForActiveCultists(); } private void OnCuffStateChanged(Entity ent, ref CuffedStateChangeEvent args) { CheckForActiveCultists(); } private void CheckForActiveCultists() { if (CultistsAlive()) return; var query = QueryActiveRules(); while (query.MoveNext(out var ruleUid, out _, out var ruleComp, out _)) { ConfirmWinState((ruleUid, ruleComp)); } } private void ConfirmWinState(Entity ent) { var tier = ent.Comp.CurrentTier; var LeaderAtCentcom = false; var CultistsAtCentcom = 0; var centcomm = _emergency.GetCentcommMaps(); var wrapup = AllEntityQuery(); while (wrapup.MoveNext(out var cultist, out _, out var cultistLocation)) { if (cultistLocation.MapUid != null && centcomm.Contains(cultistLocation.MapUid.Value)) { if (TryComp(cultist, out var cuffed) && cuffed.CuffedHandCount > 0) continue; // If they are cuffed, they should be deconverted soon, so we don't count them. CultistsAtCentcom++; if (HasComp(cultist)) LeaderAtCentcom = true; } } if (tier < 3) SetWinType(ent, WinType.CrewMinor); //The monument didn't even reach tier 3, which means that either cult had a skill issue, or crew evacuated early. Minor win. else if (LeaderAtCentcom) //If the monument reached tier 3, all cultists have glowing eyes now, so you shouldn't let them evacuate without cuffs on. SetWinType(ent, WinType.CultMajor); //The Monument wasn't completed, but the cult leader's alive and at Midpoint. else if (CultistsAtCentcom >= 2) SetWinType(ent, WinType.CultMinor); //The Monument wasn't completed, but at least two cultists are alive and at Midpoint. else SetWinType(ent, WinType.Neutral); //The monument wasn't completed, no cultists escaped to midpoint. Some cultists still remain on the station, though. if (CultistsAlive()) return; //If there are no cultists alive, ignore all previous checks, crew alreay won. if (tier <= 1) //Prevent the cult getting cooked by accident before anyone even knows there's a cult. { SetWinType(ent, WinType.CrewMinor); //If we somehow get here at the end of the round, we'll still count this as a crew minor. return; } _roundEnd.DoRoundEndBehavior(ent.Comp.RoundEndBehavior, ent.Comp.EvacShuttleTime, ent.Comp.RoundEndTextSender, ent.Comp.RoundEndTextShuttleCall, ent.Comp.RoundEndTextAnnouncement); ent.Comp.RoundEndBehavior = RoundEndBehavior.Nothing; // prevent this being called multiple times. ent.Comp.RiftStop = true; // rifts can stop spawning now. var gameruleMonument = ent.Comp.MonumentInGame; if (TryComp(gameruleMonument, out var finComp)) { finComp.CurrentState = FinaleState.Unavailable; _popup.PopupCoordinates(Loc.GetString("cosmiccult-monument-powerdown"), Transform(gameruleMonument).Coordinates, PopupType.Large); _sound.StopStationEventMusic(gameruleMonument, StationEventMusicType.CosmicCult); _monument.UpdateMonumentAppearance(gameruleMonument, false); } if (ent.Comp.TotalCult == 0) SetWinType(ent, WinType.CrewComplete); // No cultists registered! That means everyone got deconverted else SetWinType(ent, WinType.CrewMajor); // There's still cultists registered, but if we got here, that means they're all dead or in cuffs } protected override void AppendRoundEndText(EntityUid uid, CosmicCultRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args) { var ftlKey = component.WinType.ToString().ToLower(); var winType = Loc.GetString($"cosmiccult-roundend-{ftlKey}"); var summaryText = Loc.GetString($"cosmiccult-summary-{ftlKey}"); args.AddLine(winType); args.AddLine(summaryText); args.AddLine(Loc.GetString("cosmiccult-roundend-cultist-count", ("initialCount", component.TotalCult))); args.AddLine(Loc.GetString("cosmiccult-roundend-cultpop-count", ("count", component.PercentConverted))); args.AddLine(Loc.GetString("cosmiccult-roundend-entropy-count", ("count", component.EntropySiphoned))); args.AddLine(Loc.GetString("cosmiccult-roundend-monument-stage", ("stage", component.CurrentTier))); } public void IncrementCultObjectiveEntropy(Entity ent) { if (AssociatedGamerule(ent) is not { } cult) return; cult.Comp.EntropySiphoned += ent.Comp.CosmicSiphonQuantity; var query = EntityQueryEnumerator(); while (query.MoveNext(out _, out var entropyComp)) { entropyComp.Siphoned = cult.Comp.EntropySiphoned; } } public void AdjustCultObjectiveConversion(int value) { var query = EntityQueryEnumerator(); while (query.MoveNext(out _, out var conversionComp)) { conversionComp.Converted += value; } } #endregion public void OnStartMonument(Entity ent) { if (AssociatedGamerule(ent) is not { } cult) return; cult.Comp.CurrentTier = 1; cult.Comp.MonumentInGame = ent; //Since there's only one Monument per round, let's store its UID for the rest of the round. Saves us on spamming enumerators. _monument.MonumentTier1(ent); UpdateCultData(ent); } public void UpdateCultData(Entity uid) // This runs every time Entropy is Inserted into The Monument, and every time a Cultist is Converted or Deconverted. { if (!TryComp(uid, out var finaleComp)) return; if (AssociatedGamerule(uid) is not { } cult) return; cult.Comp.TotalCrew = _playerMan.Sessions.Count(session => session.Status == SessionStatus.InGame && HasComp(session.AttachedEntity)); #if DEBUG if (cult.Comp.TotalCrew < 25) cult.Comp.TotalCrew = 25; #endif cult.Comp.PercentConverted = Math.Round((double)(100 * cult.Comp.TotalCult) / cult.Comp.TotalCrew); //this can probably be somewhere else but _monument.UpdateMonumentReqsForTier(uid, cult.Comp.CurrentTier); _monument.UpdateMonumentProgress(uid, cult); if (uid.Comp.CurrentProgress >= uid.Comp.TargetProgress && cult.Comp.CurrentTier == 3 && finaleComp.CurrentState == FinaleState.Unavailable) { if (!finaleComp.FinaleDelayStarted) //check if we've not already started the finale delay { finaleComp.FinaleDelayStarted = true; //set that we've started it //do everything else var timer = _finaleDelay; var cultistQuery = EntityQueryEnumerator(); while (cultistQuery.MoveNext(out var cultist, out var cultistComp)) { _antag.SendBriefing(cultist, Loc.GetString("cosmiccult-finale-autocall-briefing"), Color.FromHex("#4cabb3"), _monumentAlert); } cult.Comp.PrepareFinaleTimer = _timing.CurTime + timer; } } else if (finaleComp.CurrentState != FinaleState.Unavailable) _monument.SetTargetProgess(uid, uid.Comp.CurrentProgress); else if (uid.Comp.CurrentProgress >= uid.Comp.TargetProgress && cult.Comp.CurrentTier == 2 && uid.Comp.CanTierUp) { _monument.SetCanTierUp(uid, false); var cultistQuery = EntityQueryEnumerator(); while (cultistQuery.MoveNext(out var cultist, out var cultistComp)) { _antag.SendBriefing(cultist, Loc.GetString("cosmiccult-monument-stage3-briefing", ("time", _t3RevealDelay.TotalSeconds)), Color.FromHex("#4cabb3"), _monumentAlert); } _monument.MonumentTier3(uid); _monument.UpdateMonumentReqsForTier(uid, cult.Comp.CurrentTier); cult.Comp.CurrentTier = 3; cult.Comp.Tier3DelayTimer = _timing.CurTime + _t3RevealDelay; } else if (uid.Comp.CurrentProgress >= uid.Comp.TargetProgress && cult.Comp.CurrentTier == 1 && uid.Comp.CanTierUp) { _monument.SetCanTierUp(uid, false); var cultistQuery = EntityQueryEnumerator(); while (cultistQuery.MoveNext(out var cultist, out var cultistComp)) { _antag.SendBriefing(cultist, Loc.GetString("cosmiccult-monument-stage2-briefing", ("time", _t2RevealDelay.TotalSeconds)), Color.FromHex("#4cabb3"), _monumentAlert); } _monument.MonumentTier2(uid); cult.Comp.CurrentTier = 2; _monument.UpdateMonumentReqsForTier(uid, cult.Comp.CurrentTier); cult.Comp.Tier2DelayTimer = _timing.CurTime + _t2RevealDelay; } _monument.UpdateMonumentAppearance(uid, false); Dirty(uid); _ui.SetUiState(uid.Owner, MonumentKey.Key, new MonumentBuiState(uid.Comp)); } #region De- & Conversion public void TryStartCult(EntityUid uid, Entity rule) { if (!_mind.TryGetMind(uid, out var mindId, out var mind)) return; EnsureComp(uid, out var cultComp); EnsureComp(uid); EnsureComp(uid, out var associatedComp); associatedComp.CultGamerule = rule; _role.MindAddRole(mindId, MindRole, mind, true); _antag.SendBriefing(uid, Loc.GetString("cosmiccult-role-roundstart-fluff"), Color.FromHex("#4cabb3"), _briefingSound); _antag.SendBriefing(uid, Loc.GetString("cosmiccult-role-short-briefing"), Color.FromHex("#cae8e8"), null); var transmitter = EnsureComp(uid); var radio = EnsureComp(uid); radio.Channels.Add("CosmicRadio"); transmitter.Channels.Add("CosmicRadio"); if (_playerMan.TryGetSessionById(mind.UserId, out var session)) { _euiMan.OpenEui(new CosmicRoundStartEui(), session); } rule.Comp.TotalCult++; cultComp.StoredDamageContainer = Comp(uid).DamageContainerID!.Value; // nullable? Dirty(uid, cultComp); rule.Comp.Cultists.Add(uid); } private void OnAssociateRule(ref CosmicCultAssociateRuleEvent args) { TransferCultAssociation(args.Originator, args.Target); if (TryComp(args.Target, out var monument)) { OnStartMonument((args.Target, monument)); } } public void TransferCultAssociation(EntityUid from, EntityUid to) { if (!TryComp(from, out var source)) return; var destination = EnsureComp(to); destination.CultGamerule = source.CultGamerule; } public Entity? AssociatedGamerule(EntityUid uid) { if (!TryComp(uid, out var associated)) { _sawmill.Debug("{0} has no associated rule", uid); return null; } if (!TryComp(associated.CultGamerule, out var cult)) { _sawmill.Debug("Associated gamerule {0} is not a cult gamerule", associated.CultGamerule); return null; } return (associated.CultGamerule, cult); } public void CosmicConversion(EntityUid converter, EntityUid uid) { if (AssociatedGamerule(converter) is not { } cult) return; if (!_mind.TryGetMind(uid, out var mindId, out var mind) || !_playerMan.TryGetSessionById(mind.UserId, out var session)) return; _role.MindAddRole(mindId, MindRole, mind, true); _antag.SendBriefing(session, Loc.GetString("cosmiccult-role-conversion-fluff"), Color.FromHex("#4cabb3"), _briefingSound); _antag.SendBriefing(uid, Loc.GetString("cosmiccult-role-short-briefing"), Color.FromHex("#cae8e8"), null); var cultComp = EnsureComp(uid); cultComp.EntropyBudget = 10; // pity balance cultComp.StoredDamageContainer = Comp(uid).DamageContainerID!.Value; EnsureComp(uid); TransferCultAssociation(converter, uid); if (TryComp(cult.Comp.MonumentInGame, out var finaleComp) && finaleComp.FinaleActive) { EnsureComp(uid); } if (cult.Comp.CurrentTier == 3) { cultComp.EntropyBudget = 48; // pity balance cultComp.Respiration = false; foreach (var influenceProto in _protoMan.EnumeratePrototypes().Where(influenceProto => influenceProto.Tier == 3)) { cultComp.UnlockedInfluences.Add(influenceProto.ID); } EnsureComp(uid); EnsureComp(uid); EnsureComp(uid); } else if (cult.Comp.CurrentTier == 2) { cultComp.EntropyBudget = 26; // pity balance foreach (var influenceProto in _protoMan.EnumeratePrototypes().Where(influenceProto => influenceProto.Tier == 2)) { cultComp.UnlockedInfluences.Add(influenceProto.ID); } } Dirty(uid, cultComp); var transmitter = EnsureComp(uid); var radio = EnsureComp(uid); radio.Channels = ["CosmicRadio"]; transmitter.Channels = ["CosmicRadio"]; _mind.TryAddObjective(mindId, mind, "CosmicFinalityObjective"); _mind.TryAddObjective(mindId, mind, "CosmicMonumentObjective"); _mind.TryAddObjective(mindId, mind, "CosmicConversionObjective"); _mind.TryAddObjective(mindId, mind, "CosmicEntropyObjective"); _euiMan.OpenEui(new CosmicConvertedEui(), session); RemComp(uid); cult.Comp.TotalCult++; cult.Comp.Cultists.Add(uid); AdjustCultObjectiveConversion(1); UpdateCultData(cult.Comp.MonumentInGame); } private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) { if (AssociatedGamerule(ent) is not { } cult) return; if (TerminatingOrDeleted(ent)) return; var cosmicGamerule = cult.Comp; if(TryComp(ent, out var crawlerComp)) _stun.TryCrawling((ent, crawlerComp), TimeSpan.FromSeconds(2), refresh: true); foreach (var actionEnt in ent.Comp.ActionEntities) _actions.RemoveAction(actionEnt); if (TryComp(ent, out var transmitter)) transmitter.Channels.Remove("CosmicRadio"); if (TryComp(ent, out var radio)) radio.Channels.Remove("CosmicRadio"); RemComp(ent); RemComp(ent); RemComp(ent); RemComp(ent); RemComp(ent); RemComp(ent); RemComp(ent); _antag.SendBriefing(ent, Loc.GetString("cosmiccult-role-deconverted-fluff"), Color.FromHex("#4cabb3"), _deconvertSound); _antag.SendBriefing(ent, Loc.GetString("cosmiccult-role-deconverted-briefing"), Color.FromHex("#cae8e8"), null); if (!_mind.TryGetMind(ent, out var mindId, out var mind)) return; _mind.ClearObjectives(mindId, mind); _role.MindRemoveRole(mindId); _role.MindRemoveRole(mindId); if (_playerMan.TryGetSessionById(mind.UserId, out var session)) { _euiMan.OpenEui(new CosmicDeconvertedEui(), session); } _eye.SetVisibilityMask(ent, 1); _alerts.ClearAlert(ent.Owner, ent.Comp.EntropyAlert); cosmicGamerule.TotalCult--; cosmicGamerule.Cultists.Remove(ent); AdjustCultObjectiveConversion(-1); UpdateCultData(cosmicGamerule.MonumentInGame); _movementSpeed.RefreshMovementSpeedModifiers(ent); } #endregion }