diff --git a/Content.Server/DeltaV/ParadoxAnomaly/Components/ParadoxAnomalySpawner.cs b/Content.Server/DeltaV/ParadoxAnomaly/Components/ParadoxAnomalySpawner.cs new file mode 100644 index 00000000000..c3264584a1c --- /dev/null +++ b/Content.Server/DeltaV/ParadoxAnomaly/Components/ParadoxAnomalySpawner.cs @@ -0,0 +1,17 @@ +using Content.Server.DeltaV.ParadoxAnomaly.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Server.DeltaV.ParadoxAnomaly.Components; + +/// +/// Creates a random paradox anomaly and tranfers mind to it when taken by a player. +/// +[RegisterComponent, Access(typeof(ParadoxAnomalySystem))] +public sealed partial class ParadoxAnomalySpawnerComponent : Component +{ + /// + /// Antag game rule to start for the paradox anomaly. + /// + [DataField] + public EntProtoId Rule = "ParadoxAnomaly"; +} diff --git a/Content.Server/DeltaV/ParadoxAnomaly/Systems/ParadoxAnomalySystem.cs b/Content.Server/DeltaV/ParadoxAnomaly/Systems/ParadoxAnomalySystem.cs new file mode 100644 index 00000000000..62d994dac34 --- /dev/null +++ b/Content.Server/DeltaV/ParadoxAnomaly/Systems/ParadoxAnomalySystem.cs @@ -0,0 +1,164 @@ +using Content.Server.DeltaV.ParadoxAnomaly.Components; +using Content.Server.DetailExaminable; +using Content.Server.GenericAntag; +using Content.Server.Ghost.Roles; +using Content.Server.Ghost.Roles.Components; +using Content.Server.Psionics; +using Content.Server.Spawners.Components; +using Content.Server.Station.Systems; +using Content.Server.Terminator.Systems; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Preferences; +using Content.Shared.Roles; +using Content.Shared.Roles.Jobs; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Server.DeltaV.ParadoxAnomaly.Systems; + +/// +/// 90% of the work is done by exterminator since its a reskin. +/// All the logic here is spawning since thats tricky. +/// +public sealed class ParadoxAnomalySystem : EntitySystem +{ + [Dependency] private readonly GenericAntagSystem _genericAntag = default!; + [Dependency] private readonly GhostRoleSystem _ghostRole = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly PsionicsSystem _psionics = default!; + [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedRoleSystem _role = default!; + [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; + [Dependency] private readonly TerminatorSystem _terminator = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnTakeGhostRole); + } + + private void OnTakeGhostRole(Entity ent, ref TakeGhostRoleEvent args) + { + Log.Info($"Using paradox anomaly spawner {ent}"); + if (!TrySpawnParadoxAnomaly(ent.Comp.Rule, out var twin)) + return; + + Log.Info($"Created paradox anomaly {ToPrettyString(twin):twin}"); + var role = Comp(ent); + _ghostRole.GhostRoleInternalCreateMindAndTransfer(args.Player, ent, twin.Value, role); + _ghostRole.UnregisterGhostRole((ent.Owner, role)); + + args.TookRole = true; + QueueDel(ent); + } + + private bool TrySpawnParadoxAnomaly(string rule, [NotNullWhen(true)] out EntityUid? twin) + { + twin = null; + + // Get a list of potential candidates + var candidates = new List<(EntityUid, EntityUid, SpeciesPrototype, HumanoidCharacterProfile)>(); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var mindContainer, out var humanoid)) + { + if (humanoid.LastProfileLoaded is not {} profile) + continue; + + if (!_proto.TryIndex(humanoid.Species, out var species)) + continue; + + if (_mind.GetMind(uid, mindContainer) is not {} mindId || !HasComp(mindId)) + continue; + + if (_role.MindIsAntagonist(mindId)) + continue; + + // TODO: when metempsychosis real skip whoever has Karma + + candidates.Add((uid, mindId, species, profile)); + } + + twin = SpawnParadoxAnomaly(candidates, rule); + return twin != null; + } + + private EntityUid? SpawnParadoxAnomaly(List<(EntityUid, EntityUid, SpeciesPrototype, HumanoidCharacterProfile)> candidates, string rule) + { + // Select a candidate. + if (candidates.Count == 0) + return null; + + var (uid, mindId, species, profile) = _random.Pick(candidates); + var jobId = Comp(mindId).Prototype; + var job = _proto.Index(jobId!); + + // Find a suitable spawn point. + var station = _station.GetOwningStation(uid); + var latejoins = new List(); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var spawnUid, out var spawnPoint)) + { + if (spawnPoint.SpawnType != SpawnPointType.LateJoin) + continue; + + if (_station.GetOwningStation(spawnUid) == station) + latejoins.Add(spawnUid); + } + + if (latejoins.Count == 0) + return null; + + // Spawn the twin. + var destination = Transform(_random.Pick(latejoins)).Coordinates; + var spawned = Spawn(species.Prototype, destination); + + // Set the kill target to the chosen player + _terminator.SetTarget(spawned, mindId); + _genericAntag.MakeAntag(spawned, rule); + + ////////////////////////// + // /!\ WARNING /!\ // + // MAJOR SHITCODE BELOW // + // /!\ WARNING /!\ // + ////////////////////////// + + // Copy the details. + _humanoid.LoadProfile(spawned, profile); + _metaData.SetEntityName(spawned, Name(uid)); + + if (TryComp(uid, out var detail)) + { + var detailCopy = EnsureComp(spawned); + detailCopy.Content = detail.Content; + } + + if (job.StartingGear != null && _proto.TryIndex(job.StartingGear, out var gear)) + { + _stationSpawning.EquipStartingGear(spawned, gear, profile); + _stationSpawning.EquipIdCard(spawned, + profile.Name, + job, + station); + } + + foreach (var special in job.Special) + { + special.AfterEquip(spawned); + } + + var psi = EnsureComp(spawned); + _psionics.RollPsionics(spawned, psi, false, 100); + + return spawned; + } +} diff --git a/Content.Server/GenericAntag/GenericAntagSystem.cs b/Content.Server/GenericAntag/GenericAntagSystem.cs index 6b1774159c1..8b7a16a4cb2 100644 --- a/Content.Server/GenericAntag/GenericAntagSystem.cs +++ b/Content.Server/GenericAntag/GenericAntagSystem.cs @@ -57,6 +57,14 @@ public void MakeAntag(EntityUid uid, EntityUid mindId, GenericAntagComponent? co _mind.TryAddObjective(mindId, mind, id); } } + + /// + /// DeltaV - used by paradox anomaly + /// + public void MakeAntag(EntityUid uid, string rule) + { + AddComp(uid).Rule = rule; + } } /// diff --git a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs index 36ab038562b..770e9fc39b3 100644 --- a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs +++ b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs @@ -64,6 +64,8 @@ public void CloneAppearance(EntityUid source, EntityUid target, HumanoidAppearan grammar.Gender = sourceHumanoid.Gender; } + targetHumanoid.LastProfileLoaded = sourceHumanoid.LastProfileLoaded; // DeltaV - let paradox anomaly be cloned + Dirty(targetHumanoid); } diff --git a/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagRuleComponent.cs b/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagRuleComponent.cs index 429db920dca..df8b687c3e1 100644 --- a/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagRuleComponent.cs +++ b/Content.Server/Nyanotrasen/StationEvents/Components/MidRoundAntagRuleComponent.cs @@ -1,4 +1,5 @@ using Content.Server.StationEvents.Events; +using Robust.Shared.Prototypes; namespace Content.Server.StationEvents.Components; @@ -6,11 +7,11 @@ namespace Content.Server.StationEvents.Components; public sealed partial class MidRoundAntagRuleComponent : Component { [DataField("antags")] - public IReadOnlyList MidRoundAntags = new[] + public List MidRoundAntags = new() { "SpawnPointGhostRatKing", - "SpawnPointGhostVampSpider", - "SpawnPointGhostFugitive", - "MobEvilTwinSpawn" + //"SpawnPointGhostVampSpider", + //"SpawnPointGhostFugitive", + "SpawnPointGhostParadoxAnomaly" }; } diff --git a/Content.Server/Nyanotrasen/StationEvents/Events/MidRoundAntagRule.cs b/Content.Server/Nyanotrasen/StationEvents/Events/MidRoundAntagRule.cs index bf307737601..169f7575490 100644 --- a/Content.Server/Nyanotrasen/StationEvents/Events/MidRoundAntagRule.cs +++ b/Content.Server/Nyanotrasen/StationEvents/Events/MidRoundAntagRule.cs @@ -1,41 +1,38 @@ -using System.Linq; -using Robust.Shared.Random; using Content.Server.GameTicking.Rules.Components; using Content.Server.StationEvents.Components; +using Robust.Shared.Random; +using System.Linq; namespace Content.Server.StationEvents.Events; -internal sealed class MidRoundAntagRule : StationEventSystem +public sealed class MidRoundAntagRule : StationEventSystem { - [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly IRobustRandom _random = default!; protected override void Started(EntityUid uid, MidRoundAntagRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) { base.Started(uid, component, gameRule, args); - var spawnLocations = EntityManager.EntityQuery().ToList(); - var backupSpawnLocations = EntityManager.EntityQuery().ToList(); + var spawnLocations = EntityQuery().ToList(); + var backupSpawnLocations = EntityQuery().ToList(); TransformComponent? spawn = new(); if (spawnLocations.Count > 0) { - var spawnLoc = _robustRandom.Pick(spawnLocations); + var spawnLoc = _random.Pick(spawnLocations); spawn = spawnLoc.Item2; } else if (backupSpawnLocations.Count > 0) { - var spawnLoc = _robustRandom.Pick(backupSpawnLocations); + var spawnLoc = _random.Pick(backupSpawnLocations); spawn = spawnLoc.Item2; } - if (spawn == null) + if (spawn?.GridUid == null) return; - if (spawn.GridUid == null) - { - return; - } - - Spawn(_robustRandom.Pick(component.MidRoundAntags), spawn.Coordinates); + var proto = _random.Pick(component.MidRoundAntags); + Log.Info($"Spawning midround antag {proto} at {spawn.Coordinates}"); + Spawn(proto, spawn.Coordinates); } } diff --git a/Content.Server/Terminator/Systems/TerminatorSystem.cs b/Content.Server/Terminator/Systems/TerminatorSystem.cs index b6699352779..837778d3c41 100644 --- a/Content.Server/Terminator/Systems/TerminatorSystem.cs +++ b/Content.Server/Terminator/Systems/TerminatorSystem.cs @@ -24,7 +24,7 @@ public override void Initialize() private void OnMapInit(EntityUid uid, TerminatorComponent comp, MapInitEvent args) { // cyborg doesn't need to breathe - RemComp(uid); + //RemComp(uid); // DeltaV - paradox anomaly does actually need to breathe } private void OnSpawned(EntityUid uid, TerminatorComponent comp, GhostRoleSpawnerUsedEvent args) @@ -47,6 +47,15 @@ private void OnCreated(EntityUid uid, TerminatorComponent comp, ref GenericAntag _role.MindAddRole(mindId, new TerminatorRoleComponent(), mind); } + /// + /// DeltaV - used for paradox anomaly. + /// + public void SetTarget(Entity ent, EntityUid mindId) + { + ent.Comp ??= EnsureComp(ent); + ent.Comp.Target = mindId; + } + /// /// Create a spawner at a position and return it. /// diff --git a/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs b/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs index 82d6964522c..b0bc0eb9a0c 100644 --- a/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs +++ b/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Preferences; // DeltaV using Robust.Shared.Enums; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -82,6 +83,12 @@ public sealed partial class HumanoidAppearanceComponent : Component /// [ViewVariables(VVAccess.ReadOnly)] public Color? CachedFacialHairColor; + + /// + /// DeltaV - let paradox anomaly be cloned + /// + [ViewVariables] + public HumanoidCharacterProfile? LastProfileLoaded; } [DataDefinition] diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index 597afcbda2a..4974e283dd5 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -329,6 +329,8 @@ public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile, humanoid.Age = profile.Age; + humanoid.LastProfileLoaded = profile; // DeltaV - let paradox anomaly be cloned + Dirty(humanoid); } diff --git a/Resources/Locale/en-US/deltav/game-ticking/game-rules/rule-paradox-anomaly.ftl b/Resources/Locale/en-US/deltav/game-ticking/game-rules/rule-paradox-anomaly.ftl new file mode 100644 index 00000000000..45bd554deef --- /dev/null +++ b/Resources/Locale/en-US/deltav/game-ticking/game-rules/rule-paradox-anomaly.ftl @@ -0,0 +1,5 @@ +paradox-anomaly-round-end-agent-name = Paradox Anomaly + +objective-issuer-self = [color=#1708EC]Self[/color] + +# briefing is in terminator ftl diff --git a/Resources/Locale/en-US/deltav/ghost/roles/ghost-role-component.ftl b/Resources/Locale/en-US/deltav/ghost/roles/ghost-role-component.ftl index f2e9238dfbf..977a5f47bcb 100644 --- a/Resources/Locale/en-US/deltav/ghost/roles/ghost-role-component.ftl +++ b/Resources/Locale/en-US/deltav/ghost/roles/ghost-role-component.ftl @@ -6,3 +6,7 @@ ghost-role-information-nukie-mouse-rules = Normal syndicate antagonist rules app ghost-role-information-listeningop-name = Listening Post Operative ghost-role-information-listeningop-description = You are a Listening Post operative. Get into range, observe the station, intercept communications and assist any operatives in the area! ghost-role-information-listeningop-rules = You are a Syndicate Operative tasked with the continuous reporting and monitoring of the station and its activities, as well as assisting any fellow operatives who may be aboard the station. As an antagonist, do whatever is required for you to complete this task. Make sure your station doesn't fall into enemy hands and DO NOT abandon your station! Hide your existence at any cost! + +ghost-role-information-paradox-anomaly-name = Paradox Anomaly +ghost-role-information-paradox-anomaly-description = Replace your double, or befriend them. +ghost-role-information-paradox-anomaly-rules = Try and replace your twin with this funny roleplay antag rather than plasma flooding the station or something. You can also just befriend them. diff --git a/Resources/Locale/en-US/deltav/objectives/conditions/paradox-anomaly.ftl b/Resources/Locale/en-US/deltav/objectives/conditions/paradox-anomaly.ftl new file mode 100644 index 00000000000..2eac3339bf4 --- /dev/null +++ b/Resources/Locale/en-US/deltav/objectives/conditions/paradox-anomaly.ftl @@ -0,0 +1,2 @@ +objective-paradox-anomaly-kill-title = Kill this universe's {$targetName} +objective-paradox-anomaly-friend-title = Keep your new friend {$targetName} alive diff --git a/Resources/Locale/en-US/game-ticking/game-rules/rule-terminator.ftl b/Resources/Locale/en-US/game-ticking/game-rules/rule-terminator.ftl index 41237a5c10d..2e167431773 100644 --- a/Resources/Locale/en-US/game-ticking/game-rules/rule-terminator.ftl +++ b/Resources/Locale/en-US/game-ticking/game-rules/rule-terminator.ftl @@ -8,7 +8,11 @@ terminator-role-greeting = Use any means at your disposal to complete the mission. Glory to Cybersun. -terminator-role-briefing = Kill the target at all costs. +# DeltaV - paradox anomaly +terminator-role-briefing = + You are a bluespace anomaly that looks and sound identical to someone from this reality. + Kill them and assume their identity, or talk it out and become friends. + Your objectives support either playstyle (and you obviously can't do both). terminator-endoskeleton-gib-popup = All the battered flesh falls apart, revealing a titanium endoskeleton! terminator-endoskeleton-burn-popup = The seared flesh is burned to a crisp, revealing a titanium endoskeleton! diff --git a/Resources/Locale/en-US/prototypes/roles/antags.ftl b/Resources/Locale/en-US/prototypes/roles/antags.ftl index d12f70cda25..40f2c9a6820 100644 --- a/Resources/Locale/en-US/prototypes/roles/antags.ftl +++ b/Resources/Locale/en-US/prototypes/roles/antags.ftl @@ -31,5 +31,5 @@ roles-antag-space-ninja-objective = Use your stealth to sabotage the station, no roles-antag-thief-name = Thief roles-antag-thief-objective = Add some NT property to your personal collection without using violence. -roles-antag-terminator-name = Exterminator -roles-antag-terminator-objective = Kill the target at all costs, the future depends on it. +roles-antag-terminator-name = Paradox Anomaly # DeltaV - paradox anomaly +roles-antag-terminator-objective = Replace your double, or befriend them. # DeltaV - paradox anomaly diff --git a/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml index b43f50890c2..88a2488db09 100644 --- a/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml @@ -44,4 +44,19 @@ layers: - state: green - sprite: Structures/Wallmounts/signs.rsi - state: radiation \ No newline at end of file + state: radiation + +- type: entity + parent: MarkerBase + id: SpawnPointGhostParadoxAnomaly + name: paradox anomaly spawn point + components: + - type: GhostRole + name: ghost-role-information-paradox-anomaly-name + description: ghost-role-information-paradox-anomaly-description + rules: ghost-role-information-paradox-anomaly-rules + - type: ParadoxAnomalySpawner + - type: Sprite + sprite: Markers/jobs.rsi + layers: + - state: green diff --git a/Resources/Prototypes/DeltaV/GameRules/midround.yml b/Resources/Prototypes/DeltaV/GameRules/midround.yml new file mode 100644 index 00000000000..82b47c05718 --- /dev/null +++ b/Resources/Prototypes/DeltaV/GameRules/midround.yml @@ -0,0 +1,11 @@ +- type: entity + noSpawn: true + parent: BaseGameRule + id: ParadoxAnomaly + components: + - type: GenericAntagRule + agentName: paradox-anomaly-round-end-agent-name + objectives: + - ParadoxAnomalyKillObjective + - ParadoxAnomalyFriendObjective + - ParadoxAnomalyEscapeObjective diff --git a/Resources/Prototypes/DeltaV/Objectives/paradox_anomaly.yml b/Resources/Prototypes/DeltaV/Objectives/paradox_anomaly.yml new file mode 100644 index 00000000000..dd0b74c4616 --- /dev/null +++ b/Resources/Prototypes/DeltaV/Objectives/paradox_anomaly.yml @@ -0,0 +1,52 @@ +- type: entity + abstract: true + parent: BaseTerminatorObjective # mrp terminator real + id: BaseParadoxAnomalyObjective + components: + - type: Objective + issuer: self + +# not using base kill/keep alive objectives since these intentionally conflict with eachother +- type: entity + noSpawn: true + parent: BaseParadoxAnomalyObjective + id: ParadoxAnomalyKillObjective + description: This universe doesn't have room for both of us. + components: + - type: Objective + icon: + sprite: Objects/Weapons/Guns/Pistols/viper.rsi + state: icon + - type: TargetObjective + title: objective-paradox-anomaly-kill-title + - type: TerminatorTargetOverride + - type: KillPersonCondition + requireDead: true + +- type: entity + noSpawn: true + parent: BaseParadoxAnomalyObjective + id: ParadoxAnomalyFriendObjective + description: Perhaps there is room, as friends. + components: + - type: Objective + icon: + sprite: Objects/Misc/bureaucracy.rsi + state: folder-white + - type: TargetObjective + title: objective-paradox-anomaly-friend-title + - type: TerminatorTargetOverride + - type: KeepAliveCondition + +- type: entity + noSpawn: true + parent: [BaseParadoxAnomalyObjective, BaseLivingObjective] + id: ParadoxAnomalyEscapeObjective + name: Escape to centcom alive and unrestrained. + description: This is your universe now. + components: + - type: Objective + icon: + sprite: Structures/Furniture/chairs.rsi + state: shuttle + - type: EscapeShuttleCondition diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 2fbde82b049..785d1906730 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -280,7 +280,7 @@ lightBreakChancePerSecond: 0.0003 doorToggleChancePerSecond: 0.001 -# - type: entity +# - type: entity # DeltaV - replaced terminator with paradox anomaly in midroundantag rule # parent: BaseGameRule # id: TerminatorSpawn # noSpawn: true