854 lines
38 KiB
C#
854 lines
38 KiB
C#
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.Components;
|
|
using Content.Shared.Damage.Systems;
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Where all the main stuff for Cosmic Cultists happens.
|
|
/// </summary>
|
|
public sealed class CosmicCultRuleSystem : GameRuleSystem<CosmicCultRuleComponent>
|
|
{
|
|
[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 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");
|
|
|
|
/// <summary>
|
|
/// Mind role to add to cultists.
|
|
/// </summary>
|
|
public static readonly EntProtoId MindRole = "MindRoleCosmicCult";
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("cosmiccult");
|
|
|
|
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
|
|
SubscribeLocalEvent<CosmicCultAssociateRuleEvent>(OnAssociateRule);
|
|
|
|
SubscribeLocalEvent<CosmicCultRuleComponent, AfterAntagEntitySelectedEvent>(OnAntagSelect);
|
|
|
|
SubscribeLocalEvent<CosmicCultComponent, ComponentShutdown>(OnComponentShutdown);
|
|
SubscribeLocalEvent<CosmicGodComponent, ComponentInit>(OnGodSpawn);
|
|
SubscribeLocalEvent<CosmicCultComponent, MobStateChangedEvent>(OnMobStateChanged);
|
|
SubscribeLocalEvent<CosmicCultComponent, CuffedStateChangeEvent>(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<ParallaxComponent>(mapData, out var parallax);
|
|
parallax.Parallax = "CosmicFinaleParallax";
|
|
Dirty(mapData, parallax);
|
|
|
|
EnsureComp<MapLightComponent>(mapData, out var mapLight);
|
|
mapLight.AmbientLightColor = Color.FromHex("#210746");
|
|
Dirty(mapData, mapLight);
|
|
|
|
var lights = EntityQueryEnumerator<PoweredLightComponent>();
|
|
while (lights.MoveNext(out var light, out _))
|
|
{
|
|
if (!_rand.Prob(0.50f))
|
|
continue;
|
|
_ghost.DoGhostBooEvent(light);
|
|
}
|
|
|
|
if (TryComp<CosmicFinaleComponent>(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<CosmicCultComponent>();
|
|
while (query.MoveNext(out var cultist, out var cultComp))
|
|
{
|
|
EnsureComp<CosmicSubtleMarkComponent>(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<PoweredLightComponent>();
|
|
while (lights.MoveNext(out var light, out _))
|
|
{
|
|
if (!_rand.Prob(0.25f))
|
|
continue;
|
|
_ghost.DoGhostBooEvent(light);
|
|
}
|
|
|
|
var collideQuery = EntityQueryEnumerator<MonumentCollisionComponent>();
|
|
while (collideQuery.MoveNext(out var collideEnt, out var collideComp))
|
|
{
|
|
collideComp.HasCollision = true;
|
|
Dirty(collideEnt, collideComp);
|
|
}
|
|
|
|
if (TryComp<VisibilityComponent>(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<PoweredLightComponent>();
|
|
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()
|
|
{
|
|
// If there's already an entity with steward, don't hold a vote. This allows admins to add the Cosmic Cult rule a 2nd time
|
|
// in the case that there is only one cultist and they've been chosen as the steward already.
|
|
if (EntityQuery<CosmicCultLeadComponent>().Any())
|
|
{
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Medium,
|
|
$"Cosmic cult steward already exists. Cancelling steward vote.");
|
|
return;
|
|
}
|
|
var cultists = new List<(string, EntityUid)>();
|
|
|
|
var cultQuery = EntityQueryEnumerator<CosmicCultComponent, MetaDataComponent>();
|
|
|
|
while (cultQuery.MoveNext(out var cult, out _, out var metadata))
|
|
{
|
|
var playerInfo = metadata.EntityName;
|
|
if (TryComp<PolymorphedEntityComponent>(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
|
|
};
|
|
|
|
// If there are no cultists, don't hold a vote, or the server will crash.
|
|
if (cultists.Count == 0)
|
|
{
|
|
Log.Warning($"There are no cosmic cultists present for the steward vote. Voting is cancelled to prevent the server crashing.");
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"There are no cosmic cultists for the steward vote. Steward vote is cancelled to prevent the server crashing.");
|
|
return;
|
|
}
|
|
|
|
foreach (var (name, ent) in cultists)
|
|
{
|
|
options.Options.Add((Loc.GetString(name), ent));
|
|
}
|
|
|
|
// If somehow there are cultists but no options, still don't hold a vote.
|
|
// Holding a vote with zero options crashes the server.
|
|
if (options.Options.Count == 0)
|
|
{
|
|
Log.Warning($"There are {cultists.Count} cosmic cultists but no options for the steward vote. Voting is cancelled to prevent the server crashing.");
|
|
_adminLogger.Add(LogType.Vote, LogImpact.Extreme, $"There are {cultists.Count} cosmic cultists but no options for the steward vote. Steward vote is cancelled to prevent the server crashing.");
|
|
return;
|
|
}
|
|
|
|
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<CosmicCultLeadComponent>(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<CosmicCultRuleComponent> uid, ref AfterAntagEntitySelectedEvent args)
|
|
{
|
|
TryStartCult(args.EntityUid, uid);
|
|
}
|
|
#endregion
|
|
|
|
#region Round & Objectives
|
|
|
|
private void OnGodSpawn(Entity<CosmicGodComponent> 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<HumanoidAppearanceComponent, MobStateComponent>();
|
|
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<CosmicCultRuleComponent> 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<CosmicCultComponent, MobStateComponent>();
|
|
while (query.MoveNext(out var ent, out _, out var mobComp))
|
|
{
|
|
if (TryComp<CuffableComponent>(ent, out var cuffed) && cuffed.CuffedHandCount > 0) continue;
|
|
if (mobComp.Running && mobComp.CurrentState != MobState.Dead)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void OnMobStateChanged(Entity<CosmicCultComponent> ent, ref MobStateChangedEvent args)
|
|
{
|
|
CheckForActiveCultists();
|
|
}
|
|
|
|
private void OnCuffStateChanged(Entity<CosmicCultComponent> 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<CosmicCultRuleComponent> ent)
|
|
{
|
|
var tier = ent.Comp.CurrentTier;
|
|
var LeaderAtCentcom = false;
|
|
var CultistsAtCentcom = 0;
|
|
var centcomm = _emergency.GetCentcommMaps();
|
|
var wrapup = AllEntityQuery<CosmicCultComponent, TransformComponent>();
|
|
while (wrapup.MoveNext(out var cultist, out _, out var cultistLocation))
|
|
{
|
|
if (cultistLocation.MapUid != null && centcomm.Contains(cultistLocation.MapUid.Value))
|
|
{
|
|
if (TryComp<CuffableComponent>(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<CosmicCultLeadComponent>(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<CosmicFinaleComponent>(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<CosmicCultComponent> ent)
|
|
{
|
|
if (AssociatedGamerule(ent) is not { } cult)
|
|
return;
|
|
|
|
cult.Comp.EntropySiphoned += ent.Comp.CosmicSiphonQuantity;
|
|
var query = EntityQueryEnumerator<CosmicEntropyConditionComponent>();
|
|
while (query.MoveNext(out _, out var entropyComp))
|
|
{
|
|
entropyComp.Siphoned = cult.Comp.EntropySiphoned;
|
|
}
|
|
}
|
|
|
|
public void AdjustCultObjectiveConversion(int value)
|
|
{
|
|
var query = EntityQueryEnumerator<CosmicConversionConditionComponent>();
|
|
while (query.MoveNext(out _, out var conversionComp))
|
|
{
|
|
conversionComp.Converted += value;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
public void OnStartMonument(Entity<MonumentComponent> 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<MonumentComponent> uid) // This runs every time Entropy is Inserted into The Monument, and every time a Cultist is Converted or Deconverted.
|
|
{
|
|
if (!TryComp<CosmicFinaleComponent>(uid, out var finaleComp))
|
|
return;
|
|
|
|
if (AssociatedGamerule(uid) is not { } cult)
|
|
return;
|
|
|
|
cult.Comp.TotalCrew = _playerMan.Sessions.Count(session => session.Status == SessionStatus.InGame && HasComp<HumanoidAppearanceComponent>(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<CosmicCultComponent>();
|
|
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<CosmicCultComponent>();
|
|
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<CosmicCultComponent>();
|
|
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<CosmicCultRuleComponent> rule)
|
|
{
|
|
if (!_mind.TryGetMind(uid, out var mindId, out var mind))
|
|
return;
|
|
|
|
EnsureComp<CosmicCultComponent>(uid, out var cultComp);
|
|
EnsureComp<IntrinsicRadioReceiverComponent>(uid);
|
|
EnsureComp<CosmicCultAssociatedRuleComponent>(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<IntrinsicRadioTransmitterComponent>(uid);
|
|
var radio = EnsureComp<ActiveRadioComponent>(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<DamageableComponent>(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<MonumentComponent>(args.Target, out var monument))
|
|
{
|
|
OnStartMonument((args.Target, monument));
|
|
}
|
|
}
|
|
|
|
public void TransferCultAssociation(EntityUid from, EntityUid to)
|
|
{
|
|
if (!TryComp<CosmicCultAssociatedRuleComponent>(from, out var source))
|
|
return;
|
|
|
|
var destination = EnsureComp<CosmicCultAssociatedRuleComponent>(to);
|
|
destination.CultGamerule = source.CultGamerule;
|
|
}
|
|
|
|
public Entity<CosmicCultRuleComponent>? AssociatedGamerule(EntityUid uid)
|
|
{
|
|
if (!TryComp<CosmicCultAssociatedRuleComponent>(uid, out var associated))
|
|
{
|
|
_sawmill.Debug("{0} has no associated rule", uid);
|
|
return null;
|
|
}
|
|
|
|
if (!TryComp<CosmicCultRuleComponent>(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<CosmicCultComponent>(uid);
|
|
cultComp.EntropyBudget = 10; // pity balance
|
|
cultComp.StoredDamageContainer = Comp<DamageableComponent>(uid).DamageContainerID!.Value;
|
|
EnsureComp<IntrinsicRadioReceiverComponent>(uid);
|
|
TransferCultAssociation(converter, uid);
|
|
if (TryComp<CosmicFinaleComponent>(cult.Comp.MonumentInGame, out var finaleComp) && finaleComp.FinaleActive)
|
|
{
|
|
EnsureComp<CosmicStarMarkComponent>(uid);
|
|
}
|
|
if (cult.Comp.CurrentTier == 3)
|
|
{
|
|
cultComp.EntropyBudget = 48; // pity balance
|
|
cultComp.Respiration = false;
|
|
|
|
foreach (var influenceProto in _protoMan.EnumeratePrototypes<InfluencePrototype>().Where(influenceProto => influenceProto.Tier == 3))
|
|
{
|
|
cultComp.UnlockedInfluences.Add(influenceProto.ID);
|
|
}
|
|
|
|
EnsureComp<CosmicSubtleMarkComponent>(uid);
|
|
EnsureComp<PressureImmunityComponent>(uid);
|
|
EnsureComp<TemperatureImmunityComponent>(uid);
|
|
}
|
|
else if (cult.Comp.CurrentTier == 2)
|
|
{
|
|
cultComp.EntropyBudget = 26; // pity balance
|
|
|
|
foreach (var influenceProto in _protoMan.EnumeratePrototypes<InfluencePrototype>().Where(influenceProto => influenceProto.Tier == 2))
|
|
{
|
|
cultComp.UnlockedInfluences.Add(influenceProto.ID);
|
|
}
|
|
}
|
|
|
|
Dirty(uid, cultComp);
|
|
|
|
var transmitter = EnsureComp<IntrinsicRadioTransmitterComponent>(uid);
|
|
var radio = EnsureComp<ActiveRadioComponent>(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<BibleUserComponent>(uid);
|
|
|
|
cult.Comp.TotalCult++;
|
|
cult.Comp.Cultists.Add(uid);
|
|
|
|
AdjustCultObjectiveConversion(1);
|
|
UpdateCultData(cult.Comp.MonumentInGame);
|
|
}
|
|
|
|
private void OnComponentShutdown(Entity<CosmicCultComponent> ent, ref ComponentShutdown args)
|
|
{
|
|
if (AssociatedGamerule(ent) is not { } cult)
|
|
return;
|
|
if (TerminatingOrDeleted(ent))
|
|
return;
|
|
var cosmicGamerule = cult.Comp;
|
|
|
|
if(TryComp<CrawlerComponent>(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<IntrinsicRadioTransmitterComponent>(ent, out var transmitter))
|
|
transmitter.Channels.Remove("CosmicRadio");
|
|
if (TryComp<ActiveRadioComponent>(ent, out var radio))
|
|
radio.Channels.Remove("CosmicRadio");
|
|
RemComp<CosmicCultLeadComponent>(ent);
|
|
RemComp<InfluenceVitalityComponent>(ent);
|
|
RemComp<InfluenceStrideComponent>(ent);
|
|
RemComp<PressureImmunityComponent>(ent);
|
|
RemComp<TemperatureImmunityComponent>(ent);
|
|
RemComp<CosmicStarMarkComponent>(ent);
|
|
RemComp<CosmicSubtleMarkComponent>(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<CosmicCultRoleComponent>(mindId);
|
|
_role.MindRemoveRole<RoleBriefingComponent>(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
|
|
}
|