From 7cbdc99fe502a4598e11eaf545c6bb77868f9a08 Mon Sep 17 00:00:00 2001 From: Morb0 <14136326+Morb0@users.noreply.github.com> Date: Sun, 12 Nov 2023 14:30:09 +0300 Subject: [PATCH] Move out evil twin from Secrets --- .../Corvax/EvilTwin/EvilTwinComponent.cs | 7 + .../EvilTwin/EvilTwinSpawnerComponent.cs | 6 + .../Corvax/EvilTwin/EvilTwinSystem.cs | 241 ++++++++++++++++++ .../station-events/events/evil-twin.ftl | 18 ++ .../Entities/Markers/Spawners/ghost_roles.yml | 19 ++ .../Prototypes/Corvax/GameRules/events.yml | 14 + .../Prototypes/Corvax/Objectives/eviltwin.yml | 26 ++ .../Corvax/Roles/Antags/eviltwin.yml | 6 + Secrets | 2 +- 9 files changed, 338 insertions(+), 1 deletion(-) create mode 100644 Content.Server/Corvax/EvilTwin/EvilTwinComponent.cs create mode 100644 Content.Server/Corvax/EvilTwin/EvilTwinSpawnerComponent.cs create mode 100644 Content.Server/Corvax/EvilTwin/EvilTwinSystem.cs create mode 100644 Resources/Locale/ru-RU/corvax/station-events/events/evil-twin.ftl create mode 100644 Resources/Prototypes/Corvax/Entities/Markers/Spawners/ghost_roles.yml create mode 100644 Resources/Prototypes/Corvax/GameRules/events.yml create mode 100644 Resources/Prototypes/Corvax/Objectives/eviltwin.yml create mode 100644 Resources/Prototypes/Corvax/Roles/Antags/eviltwin.yml diff --git a/Content.Server/Corvax/EvilTwin/EvilTwinComponent.cs b/Content.Server/Corvax/EvilTwin/EvilTwinComponent.cs new file mode 100644 index 00000000000..a1a0a925d30 --- /dev/null +++ b/Content.Server/Corvax/EvilTwin/EvilTwinComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Corvax.EvilTwin; + +[RegisterComponent] +public sealed partial class EvilTwinComponent : Component +{ + public EntityUid TargetMindId; +} diff --git a/Content.Server/Corvax/EvilTwin/EvilTwinSpawnerComponent.cs b/Content.Server/Corvax/EvilTwin/EvilTwinSpawnerComponent.cs new file mode 100644 index 00000000000..e33f601678f --- /dev/null +++ b/Content.Server/Corvax/EvilTwin/EvilTwinSpawnerComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.Corvax.EvilTwin; + +[RegisterComponent] +public sealed partial class EvilTwinSpawnerComponent : Component +{ +} diff --git a/Content.Server/Corvax/EvilTwin/EvilTwinSystem.cs b/Content.Server/Corvax/EvilTwin/EvilTwinSystem.cs new file mode 100644 index 00000000000..0d1e284ebf0 --- /dev/null +++ b/Content.Server/Corvax/EvilTwin/EvilTwinSystem.cs @@ -0,0 +1,241 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Server.DetailExaminable; +using Content.Server.GameTicking; +using Content.Server.Humanoid; +using Content.Server.Jobs; +using Content.Server.Mind; +using Content.Server.Objectives.Components; +using Content.Server.Objectives.Systems; +using Content.Server.Preferences.Managers; +using Content.Server.Roles; +using Content.Server.Roles.Jobs; +using Content.Server.Station.Systems; +using Content.Shared.Humanoid; +using Content.Shared.Mind.Components; +using Content.Shared.Objectives.Components; +using Content.Shared.Objectives.Systems; +using Content.Shared.Preferences; +using Content.Shared.Roles; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Corvax.EvilTwin; + +public sealed class EvilTwinSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IServerPreferencesManager _prefs = default!; + [Dependency] private readonly HumanoidAppearanceSystem _humanoid = default!; + [Dependency] private readonly StationSpawningSystem _stationSpawning = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; + [Dependency] private readonly MindSystem _mindSystem = default!; + [Dependency] private readonly RoleSystem _roleSystem = default!; + [Dependency] private readonly JobSystem _jobSystem = default!; + [Dependency] private readonly MetaDataSystem _metaDataSystem = default!; + [Dependency] private readonly TargetObjectiveSystem _target = default!; + [Dependency] private readonly SharedObjectivesSystem _objectives = default!; + + [ValidatePrototypeId] + private const string EvilTwinRole = "EvilTwin"; + [ValidatePrototypeId] + private const string KillObjective = "KillTwinObjective"; + [ValidatePrototypeId] + private const string EscapeObjective = "EscapeShuttleTwinObjective"; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnMindAdded); + SubscribeLocalEvent(OnRoundEnd); + } + + private void OnPlayerAttached(EntityUid uid, EvilTwinSpawnerComponent component, PlayerAttachedEvent args) + { + if (TryGetEligibleHumanoid(out var targetUid)) + { + var spawnerCoords = Transform(uid).Coordinates; + var spawnedTwin = TrySpawnEvilTwin(targetUid.Value, spawnerCoords); + if (spawnedTwin != null && + _mindSystem.TryGetMind(args.Player, out var mindId, out var mind)) + { + mind.CharacterName = MetaData(spawnedTwin.Value).EntityName; + _mindSystem.TransferTo(mindId, spawnedTwin); + } + } + + QueueDel(uid); + } + + private void OnMindAdded(EntityUid uid, EvilTwinComponent component, MindAddedMessage args) + { + if (!TryComp(uid, out var evilTwin) || + !_mindSystem.TryGetMind(uid, out var mindId, out var mind)) + return; + + var role = new TraitorRoleComponent + { + PrototypeId = EvilTwinRole, + }; + _roleSystem.MindAddRole(mindId, role, mind); + _mindSystem.TryAddObjective(mindId, mind, EscapeObjective); + _mindSystem.TryAddObjective(mindId, mind, KillObjective); + if (TryComp(uid, out var targetObj)) + _target.SetTarget(uid, evilTwin.TargetMindId, targetObj); + } + + private void OnRoundEnd(RoundEndTextAppendEvent ev) + { + var twinsCount = EntityQuery().Count(); + if (twinsCount == 0) + return; + + var result = Loc.GetString("evil-twin-round-end-result", ("evil-twin-count", twinsCount)); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var twin)) + { + if (!_mindSystem.TryGetMind(uid, out var mindId, out var mind)) + continue; + + var name = mind.CharacterName; + _mindSystem.TryGetSession(mind.OwnedEntity, out var session); + var username = session?.Name; + + var objectives = mind.Objectives.ToArray(); + if (objectives.Length == 0) + { + if (username != null) + { + if (name == null) + result += "\n" + Loc.GetString("evil-twin-user-was-an-evil-twin", ("user", username)); + else + result += "\n" + Loc.GetString("evil-twin-user-was-an-evil-twin-named", ("user", username), ("name", name)); + } + else if (name != null) + result += "\n" + Loc.GetString("evil-twin-was-an-evil-twin-named", ("name", name)); + + continue; + } + + if (username != null) + { + if (name == null) + result += "\n" + Loc.GetString("evil-twin-user-was-an-evil-twin-with-objectives", ("user", username)); + else + result += "\n" + Loc.GetString("evil-twin-user-was-an-evil-twin-with-objectives-named", ("user", username), ("name", name)); + } + else if (name != null) + result += "\n" + Loc.GetString("evil-twin-was-an-evil-twin-with-objectives-named", ("name", name)); + + foreach (var objectiveGroup in objectives.GroupBy(o => Comp(o).Issuer)) + { + foreach (var objective in objectiveGroup) + { + var info = _objectives.GetInfo(objective, mindId, mind); + if (info == null) + continue; + + var objectiveTitle = info.Value.Title; + var progress = info.Value.Progress; + if (progress > 0.99f) + { + result += "\n- " + Loc.GetString( + "objectives-objective-success", + ("objective", objectiveTitle), + ("markupColor", "green") + ); + } + else + { + result += "\n- " + Loc.GetString( + "objectives-objective-fail", + ("objective", objectiveTitle), + ("progress", (int) (progress * 100)), + ("markupColor", "red") + ); + } + } + } + } + ev.AddLine(result); + } + + /// + /// Get first random humanoid controlled by player mob with job + /// + /// Found humanoid uid + /// false if not found + private bool TryGetEligibleHumanoid([NotNullWhen(true)] out EntityUid? uid) + { + var targets = EntityQuery().ToList(); + _random.Shuffle(targets); + foreach (var (actor, _) in targets) + { + if (!_mindSystem.TryGetMind(actor.PlayerSession, out var mindId, out var mind) || mind.OwnedEntity == null) + continue; + + if (!_jobSystem.MindTryGetJob(mindId, out _, out _)) + continue; + + // There was check for nukeops or evil twin, but ist it will be fun? + + uid = mind.OwnedEntity; + return true; + } + + uid = null; + return false; + } + + /// + /// Spawns "clone" in round start state of target human mob + /// + /// Target for cloning + /// Spawn location + /// null if target in invalid state (ghost, leave, ...) + private EntityUid? TrySpawnEvilTwin(EntityUid target, EntityCoordinates coords) + { + if (!_mindSystem.TryGetMind(target, out var mindId, out _) || + !TryComp(target, out var humanoid) || + !TryComp(target, out var actor) || + !_prototype.TryIndex(humanoid.Species, out var species)) + return null; + + var pref = (HumanoidCharacterProfile) _prefs.GetPreferences(actor.PlayerSession.UserId).SelectedCharacter; + + var twinUid = Spawn(species.Prototype, coords); + _humanoid.LoadProfile(twinUid, pref); + _metaDataSystem.SetEntityName(twinUid, MetaData(target).EntityName); + if (TryComp(target, out var detail)) + { + var detailCopy = EnsureComp(twinUid); + detailCopy.Content = detail.Content; + } + + if (_jobSystem.MindTryGetJob(mindId, out _, out var jobProto) && jobProto.StartingGear != null) + { + if (_prototype.TryIndex(jobProto.StartingGear, out var gear)) + { + _stationSpawning.EquipStartingGear(twinUid, gear, pref); + _stationSpawning.EquipIdCard(twinUid, pref.Name, jobProto, _stationSystem.GetOwningStation(target)); + } + + foreach (var special in jobProto.Special) + { + if (special is AddComponentSpecial) + special.AfterEquip(twinUid); + } + } + + EnsureComp(twinUid).TargetMindId = mindId; + + return twinUid; + } +} + diff --git a/Resources/Locale/ru-RU/corvax/station-events/events/evil-twin.ftl b/Resources/Locale/ru-RU/corvax/station-events/events/evil-twin.ftl new file mode 100644 index 00000000000..4d27f571158 --- /dev/null +++ b/Resources/Locale/ru-RU/corvax/station-events/events/evil-twin.ftl @@ -0,0 +1,18 @@ +evil-twin-round-end-result = + { $evil-twin-count -> + [one] Был один + *[other] Было { $evil-twin-count } + } { $evil-twin-count -> + [one] злой двойник + [few] злых двойника + *[other] злых двойников + }. +evil-twin-user-was-an-evil-twin = [color=gray]{ $user }[/color] был злым двойником. +evil-twin-user-was-an-evil-twin-named = [color=white]{ $name }[/color] ([color=gray]{ $user }[/color]) был злым двойником. +evil-twin-was-an-evil-twin-named = [color=white]{ $name }[/color] был злым двойником. +evil-twin-user-was-an-evil-twin-with-objectives = [color=gray]{ $user }[/color] был(а) злым двойником со следующими целями: +evil-twin-user-was-an-evil-twin-with-objectives-named = [color=White]{ $name }[/color] ([color=gray]{ $user }[/color]) был(а) злым двойником со следующими целями: +evil-twin-was-an-evil-twin-with-objectives-named = [color=white]{ $name }[/color] был(а) злым двойником со следующими целями: + +roles-antag-evil-twin-name = Злой двойник +roles-antag-evil-twin-objective = Ваша задача - устранение и замена оригинальной персоны. diff --git a/Resources/Prototypes/Corvax/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Corvax/Entities/Markers/Spawners/ghost_roles.yml new file mode 100644 index 00000000000..fd341be0838 --- /dev/null +++ b/Resources/Prototypes/Corvax/Entities/Markers/Spawners/ghost_roles.yml @@ -0,0 +1,19 @@ +- type: entity + id: SpawnPointEvilTwin + name: evil twin spawn point + parent: MarkerBase + components: + - type: EvilTwinSpawner + - type: GhostRole + name: Злой двойник + description: Вы - злой двойник какой-то другой персоны. + rules: | + Старайтесь действовать скрытно, никто не должен прознать о подмене! + Действуйте от лица вашего оригинала, хитрите, подставляйте, запутывайте. + - type: GhostTakeoverAvailable + - type: Sprite + sprite: Markers/jobs.rsi + layers: + - state: green + - sprite: Mobs/Ghosts/ghost_human.rsi + state: icon diff --git a/Resources/Prototypes/Corvax/GameRules/events.yml b/Resources/Prototypes/Corvax/GameRules/events.yml new file mode 100644 index 00000000000..070db10c4d9 --- /dev/null +++ b/Resources/Prototypes/Corvax/GameRules/events.yml @@ -0,0 +1,14 @@ +- type: entity + id: EvilTwin + parent: BaseGameRule + noSpawn: true + components: + - type: StationEvent + minimumPlayers: 2 + weight: 5 + duration: 1 + reoccurrenceDelay: 40 + - type: VentCrittersRule + specialEntries: + - id: SpawnPointEvilTwin + prob: 0.0 diff --git a/Resources/Prototypes/Corvax/Objectives/eviltwin.yml b/Resources/Prototypes/Corvax/Objectives/eviltwin.yml new file mode 100644 index 00000000000..b9cd12c450e --- /dev/null +++ b/Resources/Prototypes/Corvax/Objectives/eviltwin.yml @@ -0,0 +1,26 @@ +- type: entity + noSpawn: true + parent: [BaseTraitorObjective, BaseLivingObjective] + id: EscapeShuttleTwinObjective + name: Escape to centcom alive and unrestrained. + description: Continue your covert implementation already on Centcom. + components: + - type: Objective + difficulty: 1.3 + icon: + sprite: Structures/Furniture/chairs.rsi + state: shuttle + - type: EscapeShuttleCondition + +- type: entity + noSpawn: true + parent: [BaseTraitorObjective, BaseKillObjective] + id: KillTwinObjective + name: Kill original persona. + description: Kill your original persona and take his place. + components: + - type: Objective + difficulty: 1.75 + unique: false + - type: TargetObjective + title: objective-condition-kill-person-title diff --git a/Resources/Prototypes/Corvax/Roles/Antags/eviltwin.yml b/Resources/Prototypes/Corvax/Roles/Antags/eviltwin.yml new file mode 100644 index 00000000000..39298a8e9a2 --- /dev/null +++ b/Resources/Prototypes/Corvax/Roles/Antags/eviltwin.yml @@ -0,0 +1,6 @@ +- type: antag + id: EvilTwin + name: roles-antag-evil-twin-name + antagonist: true + setPreference: false + objective: roles-antag-evil-twin-objective diff --git a/Secrets b/Secrets index fee3c94593a..a437b686cef 160000 --- a/Secrets +++ b/Secrets @@ -1 +1 @@ -Subproject commit fee3c94593ac0df5eaeac5698aab61752fb8da80 +Subproject commit a437b686cef858721de859391f06eb0e6d769575