bring back paradox anomaly (#825)

* refactor and add log

* add api needed for objective

* backport LastProfileLoaded

* fix midround antag rule

* evil twin spawning code

* evil twin yml and stuff

* m

* hopefully fully rename it

* fixy

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
deltanedas 2024-02-13 15:55:35 +00:00 committed by GitHub
parent 02b13528c1
commit 9985ee9788
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 325 additions and 25 deletions

View File

@ -0,0 +1,17 @@
using Content.Server.DeltaV.ParadoxAnomaly.Systems;
using Robust.Shared.Prototypes;
namespace Content.Server.DeltaV.ParadoxAnomaly.Components;
/// <summary>
/// Creates a random paradox anomaly and tranfers mind to it when taken by a player.
/// </summary>
[RegisterComponent, Access(typeof(ParadoxAnomalySystem))]
public sealed partial class ParadoxAnomalySpawnerComponent : Component
{
/// <summary>
/// Antag game rule to start for the paradox anomaly.
/// </summary>
[DataField]
public EntProtoId Rule = "ParadoxAnomaly";
}

View File

@ -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;
/// <summary>
/// 90% of the work is done by exterminator since its a reskin.
/// All the logic here is spawning since thats tricky.
/// </summary>
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<ParadoxAnomalySpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
}
private void OnTakeGhostRole(Entity<ParadoxAnomalySpawnerComponent> 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<GhostRoleComponent>(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<MindContainerComponent, HumanoidAppearanceComponent>();
while (query.MoveNext(out var uid, out var mindContainer, out var humanoid))
{
if (humanoid.LastProfileLoaded is not {} profile)
continue;
if (!_proto.TryIndex<SpeciesPrototype>(humanoid.Species, out var species))
continue;
if (_mind.GetMind(uid, mindContainer) is not {} mindId || !HasComp<JobComponent>(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<JobComponent>(mindId).Prototype;
var job = _proto.Index<JobPrototype>(jobId!);
// Find a suitable spawn point.
var station = _station.GetOwningStation(uid);
var latejoins = new List<EntityUid>();
var query = EntityQueryEnumerator<SpawnPointComponent>();
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<DetailExaminableComponent>(uid, out var detail))
{
var detailCopy = EnsureComp<DetailExaminableComponent>(spawned);
detailCopy.Content = detail.Content;
}
if (job.StartingGear != null && _proto.TryIndex<StartingGearPrototype>(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<PotentialPsionicComponent>(spawned);
_psionics.RollPsionics(spawned, psi, false, 100);
return spawned;
}
}

View File

@ -57,6 +57,14 @@ public sealed class GenericAntagSystem : EntitySystem
_mind.TryAddObjective(mindId, mind, id);
}
}
/// <summary>
/// DeltaV - used by paradox anomaly
/// </summary>
public void MakeAntag(EntityUid uid, string rule)
{
AddComp<GenericAntagComponent>(uid).Rule = rule;
}
}
/// <summary>

View File

@ -64,6 +64,8 @@ public sealed partial class HumanoidAppearanceSystem : SharedHumanoidAppearanceS
grammar.Gender = sourceHumanoid.Gender;
}
targetHumanoid.LastProfileLoaded = sourceHumanoid.LastProfileLoaded; // DeltaV - let paradox anomaly be cloned
Dirty(targetHumanoid);
}

View File

@ -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<string> MidRoundAntags = new[]
public List<EntProtoId> MidRoundAntags = new()
{
"SpawnPointGhostRatKing",
"SpawnPointGhostVampSpider",
"SpawnPointGhostFugitive",
"MobEvilTwinSpawn"
//"SpawnPointGhostVampSpider",
//"SpawnPointGhostFugitive",
"SpawnPointGhostParadoxAnomaly"
};
}

View File

@ -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<MidRoundAntagRuleComponent>
public sealed class MidRoundAntagRule : StationEventSystem<MidRoundAntagRuleComponent>
{
[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<MidRoundAntagSpawnLocationComponent, TransformComponent>().ToList();
var backupSpawnLocations = EntityManager.EntityQuery<VentCritterSpawnLocationComponent, TransformComponent>().ToList();
var spawnLocations = EntityQuery<MidRoundAntagSpawnLocationComponent, TransformComponent>().ToList();
var backupSpawnLocations = EntityQuery<VentCritterSpawnLocationComponent, TransformComponent>().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);
}
}

View File

@ -24,7 +24,7 @@ public sealed class TerminatorSystem : EntitySystem
private void OnMapInit(EntityUid uid, TerminatorComponent comp, MapInitEvent args)
{
// cyborg doesn't need to breathe
RemComp<RespiratorComponent>(uid);
//RemComp<RespiratorComponent>(uid); // DeltaV - paradox anomaly does actually need to breathe
}
private void OnSpawned(EntityUid uid, TerminatorComponent comp, GhostRoleSpawnerUsedEvent args)
@ -47,6 +47,15 @@ public sealed class TerminatorSystem : EntitySystem
_role.MindAddRole(mindId, new TerminatorRoleComponent(), mind);
}
/// <summary>
/// DeltaV - used for paradox anomaly.
/// </summary>
public void SetTarget(Entity<TerminatorComponent?> ent, EntityUid mindId)
{
ent.Comp ??= EnsureComp<TerminatorComponent>(ent);
ent.Comp.Target = mindId;
}
/// <summary>
/// Create a spawner at a position and return it.
/// </summary>

View File

@ -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
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public Color? CachedFacialHairColor;
/// <summary>
/// DeltaV - let paradox anomaly be cloned
/// </summary>
[ViewVariables]
public HumanoidCharacterProfile? LastProfileLoaded;
}
[DataDefinition]

View File

@ -329,6 +329,8 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
humanoid.Age = profile.Age;
humanoid.LastProfileLoaded = profile; // DeltaV - let paradox anomaly be cloned
Dirty(humanoid);
}

View File

@ -0,0 +1,5 @@
paradox-anomaly-round-end-agent-name = Paradox Anomaly
objective-issuer-self = [color=#1708EC]Self[/color]
# briefing is in terminator ftl

View File

@ -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.

View File

@ -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

View File

@ -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!

View File

@ -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

View File

@ -44,4 +44,19 @@
layers:
- state: green
- sprite: Structures/Wallmounts/signs.rsi
state: radiation
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

View File

@ -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

View File

@ -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

View File

@ -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