From 1dc993be444fda3425a021fe03eeb3206c34a3ee Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 2 Sep 2024 11:08:11 -0400 Subject: [PATCH 1/6] Psionic Extraplanar Creatures (#829) # Description Certain things in the game were intended to be classed as Psionic(And mostly without powers), but were apparently lacking the components. To clarify, ANYTHING that comes from an alternate layer of reality, alternative plane of existence, extra dimensions, other universes, bluespace, etc, is intended to have a PsionicComponent to abstract represent their nature as a magical being of some variety. The importance of this is largely related to the use of Metapsionics to detect them, but also for the valid target lists for Anti-Psychic abilities, such as the bonus damage from the Anti-Psychic Knife. While here, I've also added the "Loto Oil Slime" from Psionic Refactor Version 1, now that Reagent Slimes(as Extraplanar creatures brought to this world by Liquid Anomalies) have a PsionicComponent. needs https://github.com/Simple-Station/Einstein-Engines/pull/824 # Changelog :cl: - add: Revenants, Reagent Slimes, and Ore Crabs are now considered to be Psionic(But cannot gain powers randomly). This is due to their status as "Magical And/Or Extraplanar Creatures", which makes them valid targets for anti-psychic abilities such as the Psionic Mantis' Anti-Psychic Knife. - add: Some Reagent Slimes can now contain Lotophagoi Oil. --- .../DeltaV/Entities/Mobs/NPCs/familiars.yml | 1 + .../Entities/Mobs/NPCs/elemental.yml | 31 +++++++++++++++++++ .../Entities/Mobs/NPCs/revenant.yml | 7 ++++- .../Entities/Mobs/Player/familiars.yml | 6 ++++ .../Entities/Mobs/Player/guardian.yml | 5 +++ .../Structures/Specific/Anomaly/anomalies.yml | 8 +++-- .../Structures/Research/glimmer_prober.yml | 5 +++ 7 files changed, 60 insertions(+), 3 deletions(-) diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml index 173c7e43ec..4c623cb02e 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/NPCs/familiars.yml @@ -95,6 +95,7 @@ - type: InnatePsionicPowers powersToAdd: - PyrokinesisPower + - TelepathyPower - type: Grammar attributes: proper: true diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index c2380c4027..11c6f926ba 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -101,6 +101,11 @@ - SimpleHostile - type: Damageable damageContainer: StructuralInorganic + - type: Psionic + removable: false + - type: InnatePsionicPowers + powersToAdd: + - TelepathyPower - type: entity parent: MobOreCrab @@ -293,6 +298,11 @@ solution: bloodstream - type: DrainableSolution solution: bloodstream + - type: Psionic + removable: false + - type: InnatePsionicPowers + powersToAdd: + - TelepathyPower - type: entity name: Reagent Slime Spawner @@ -319,6 +329,7 @@ - ReagentSlimeNorepinephricAcid - ReagentSlimeEphedrine - ReagentSlimeRobustHarvest + - ReagentSlimeLotophagoiOil chance: 1 - type: entity @@ -530,3 +541,23 @@ - map: [ "enum.DamageStateVisualLayers.Base" ] state: alive color: "#3e901c" + +- type: entity + id: ReagentSlimeLotophagoiOil + parent: ReagentSlime + suffix: Lotophagoi Oil + components: + - type: Bloodstream + bloodReagent: LotophagoiOil + - type: PointLight + color: "#FFBF00" + - type: Sprite + drawdepth: Mobs + sprite: Mobs/Aliens/elemental.rsi + layers: + - map: [ "enum.DamageStateVisualLayers.Base" ] + state: alive + color: "#3e901c" + - type: GhostRole + prob: 1 #it's significantly more psionic than the others + description: ghost-role-information-angry-slimes-description \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index 1c6bda6fd3..bc049abb83 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -97,4 +97,9 @@ - RevenantTheme - type: Speech speechVerb: Ghost - - type: UniversalLanguageSpeaker + - type: Psionic + removable: false + - type: InnatePsionicPowers + powersToAdd: + - XenoglossyPower + - TelepathyPower diff --git a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml index 6510c8af99..11c47972f3 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml @@ -36,6 +36,9 @@ - type: Familiar - type: Psionic #Nyano - Summary: Makes psionic on creation. removable: false + - type: InnatePsionicPowers + powersToAdd: + - TelepathyPower - type: entity name: Cerberus @@ -93,6 +96,9 @@ - type: Dispellable - type: Psionic #Nyano - Summary: makes psionic on creation. removable: false + - type: InnatePsionicPowers + powersToAdd: + - TelepathyPower - type: Vocal sounds: Male: Cerberus diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index 4e824f38ad..03253a79b3 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -159,6 +159,11 @@ map: [ "enum.DamageStateVisualLayers.BaseUnshaded" ] color: "#40a7d7" shader: unshaded + - type: Psionic + removable: false + - type: InnatePsionicPowers + powersToAdd: + - TelepathyPower - type: entity name: HoloClown diff --git a/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml b/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml index 4f474765ba..64e247144d 100644 --- a/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml +++ b/Resources/Prototypes/Entities/Structures/Specific/Anomaly/anomalies.yml @@ -48,8 +48,12 @@ - type: EmitSoundOnSpawn sound: path: /Audio/Effects/teleport_arrival.ogg - - type: Psionic #Nyano - Summary: makes psionic on creation. - - type: GlimmerSource #Nyano - Summary: makes this a potential source of Glimmer. + - type: Psionic + removable: false + - type: InnatePsionicPowers + powersToAdd: + - TelepathyPower + - type: GlimmerSource active: false - type: SecretDataAnomaly randomStartSecretMin: 0 diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/glimmer_prober.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/glimmer_prober.yml index 102000f8b2..abdc8d6eeb 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/glimmer_prober.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/glimmer_prober.yml @@ -5,6 +5,10 @@ description: Probes the noƶsphere to generate research points. Might be worth turning off if glimmer is a problem. components: - type: Psionic + removable: false + - type: InnatePsionicPowers + powersToAdd: + - TelepathyPower - type: GlimmerSource - type: Construction graph: GlimmerDevices @@ -91,6 +95,7 @@ description: Uses electricity to try and sort out the noƶsphere, reducing its level of entropy. components: - type: Psionic + removable: false - type: GlimmerSource addToGlimmer: false - type: Construction From 52b2e3f4dee97d07edf4174e1e688cdc7c9da529 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Mon, 2 Sep 2024 15:08:37 +0000 Subject: [PATCH 2/6] Automatic Changelog Update (#829) --- Resources/Changelog/Changelog.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 49b4a2eed2..9fc7345867 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -5775,3 +5775,16 @@ Entries: message: 'Added Jukebox with basic songs. ' id: 6309 time: '2024-09-02T02:28:11.0000000+00:00' +- author: VMSolidus + changes: + - type: Add + message: >- + Revenants, Reagent Slimes, and Ore Crabs are now considered to be + Psionic(But cannot gain powers randomly). This is due to their status as + "Magical And/Or Extraplanar Creatures", which makes them valid targets + for anti-psychic abilities such as the Psionic Mantis' Anti-Psychic + Knife. + - type: Add + message: Some Reagent Slimes can now contain Lotophagoi Oil. + id: 6310 + time: '2024-09-02T15:08:11.0000000+00:00' From 071389e6b04ddb6d89e186bcfcc70fe41bbfba21 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 2 Sep 2024 11:08:51 -0400 Subject: [PATCH 3/6] Cloning Refactor (#735) # Description Since Cloning code is effectively abandonware by it's original codeowners, and I was the last person in this entire game to update it, I am technically the codeowner of Cloning. And by extension, it's also my responsibility to maintain the Cloning code. I've been putting this off for awhile due to how busy I've been with other projects, but since I'm now waiting on all my other refactors to be reviewed, I decided to finally sit down and comprehensively refactor Cloning. In addition to massive substantial code cleanup(Cloning machines no longer run on Frametime for one!), here's most of the changes. - Cloning Pods must be powered for the entire 30 second duration of the cloning process. - Said "30 second duration" is no longer hardcoded. Although no methods currently exist to reduce it. I plan on revisiting this after I bring back Machine Upgrading. - Cloning can now FAIL partway through. If the cloning pod is Depowered, Unanchored, or Emagged, it will automatically swap to the "Gore" state. - When in a Gore state, Cloning Pods will destroy the entity they were trying to clone, replacing them with a pool of blood and ammonia that scales with the mass of the entity that was to be cloned! - Clones come out of the pod with a significant quantity of Cellular damage, and are almost always in need of resuscitation. Consider using Cryogenics to "Finish" your clones. Doxarubixadone is literally named after this process, and is a perfectly suitable cryo chem for resuscitating clones.

Media

New gore sprites for the Metem machine, because it can now have gore mode. ![Metem gore spites](https://github.com/user-attachments/assets/7cc06ce2-c8eb-413c-b996-85e555b67db3)

# Changelog :cl: - add: Cloning & Metempsychosis Machines have been refactored! - add: Cloning can now fail at any point during the cloning process, turning the would-be clone into a soup of blood and ammonia. - add: "Clone Soup" scales directly with the mass of the entity you're attempting to clone. Fail to clone a Lamia, and you'll be greeted with an Olympic swimming pool worth of blood when the machine opens. - add: Cloning will fail if at any point during the procedure, the machine is depowered, unanchored, or emagged. - add: Clones come out of the machine with severe Cellular damage. Consider using Doxarubixadone in a Cryo tube as an affordable means of "Finishing" clones. - tweak: Cloning Time is now increased proportionally if an entity being cloned is larger than a standard human(smaller entities are unchanged) - tweak: The cost to clone an entity can now be configured on a per-server basis via CCVar "cloning.biomass_cost_multiplier" - tweak: The Biomass Reclaimer can now be toggled to round-remove ensouled bodies or not via CCVar "cloning.reclaim_souled_bodies" - add: The effects of Metempsychosis now scale with a Psion's relevant caster stats. More powerful psychics are more likely to get favorable results from being forcibly reincarnated. --------- Signed-off-by: VMSolidus Co-authored-by: Pspritechologist <81725545+Pspritechologist@users.noreply.github.com> Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com> Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com> --- .../Metempsychosis/MetempsychosisTest.cs | 53 -- .../Cloning/CloningConsoleSystem.cs | 90 ++- .../Cloning/CloningSystem.Utility.cs | 360 ++++++++++ Content.Server/Cloning/CloningSystem.cs | 648 ++++++++---------- .../Components/ActiveCloningPodComponent.cs | 9 - .../MetempsychosisKarmaComponent.cs | 4 +- .../BiomassReclaimerSystem.cs | 84 +-- .../Medical/MedicalScannerSystem.cs | 94 +-- .../Cloning/MetempsychoticMachineComponent.cs | 22 - .../Cloning/MetempsychoticMachineSystem.cs | 47 -- .../Traits/Assorted/UncloneableSystem.cs | 23 + Content.Shared/CCVar/CCVars.cs | 61 +- Content.Shared/Cloning/CloningPodComponent.cs | 119 +++- .../Cloning/CloningSystem.Events.cs | 58 ++ .../Circuitboards/Machine/production.yml | 21 + .../Machines/metempsychoticMachine.yml | 15 +- .../Devices/CircuitBoards/production.yml | 21 - .../Recipes/Lathes/electronics.yml | 9 - .../Nyanotrasen/Research/experimental.yml | 15 - .../metempsychoticNonHumanoids.yml | 4 +- .../Prototypes/Recipes/Lathes/electronics.yml | 17 +- .../Prototypes/Research/experimental.yml | 15 + .../metempsychotic.rsi/cloning_active.png} | Bin .../metempsychotic.rsi/cloning_failed.png | Bin 0 -> 1469 bytes .../metempsychotic.rsi/cloning_idle.png} | Bin .../Machines/metempsychotic.rsi/meta.json | 10 +- 26 files changed, 1043 insertions(+), 756 deletions(-) delete mode 100644 Content.IntegrationTests/Tests/Nyanotrasen/Metempsychosis/MetempsychosisTest.cs create mode 100644 Content.Server/Cloning/CloningSystem.Utility.cs delete mode 100644 Content.Server/Cloning/Components/ActiveCloningPodComponent.cs rename Content.Server/{Nyanotrasen/Cloning => Cloning/Components}/MetempsychosisKarmaComponent.cs (80%) delete mode 100644 Content.Server/Nyanotrasen/Cloning/MetempsychoticMachineComponent.cs delete mode 100644 Content.Server/Nyanotrasen/Cloning/MetempsychoticMachineSystem.cs create mode 100644 Content.Server/Traits/Assorted/UncloneableSystem.cs create mode 100644 Content.Shared/Cloning/CloningSystem.Events.cs rename Resources/Prototypes/{Nyanotrasen => }/Entities/Structures/Machines/metempsychoticMachine.yml (59%) rename Resources/Textures/{Nyanotrasen/Structures/Machines/metempsychotic.rsi/pod_1.png => Structures/Machines/metempsychotic.rsi/cloning_active.png} (100%) create mode 100644 Resources/Textures/Structures/Machines/metempsychotic.rsi/cloning_failed.png rename Resources/Textures/{Nyanotrasen/Structures/Machines/metempsychotic.rsi/pod_0.png => Structures/Machines/metempsychotic.rsi/cloning_idle.png} (100%) rename Resources/Textures/{Nyanotrasen => }/Structures/Machines/metempsychotic.rsi/meta.json (54%) diff --git a/Content.IntegrationTests/Tests/Nyanotrasen/Metempsychosis/MetempsychosisTest.cs b/Content.IntegrationTests/Tests/Nyanotrasen/Metempsychosis/MetempsychosisTest.cs deleted file mode 100644 index cd6a4b4c2b..0000000000 --- a/Content.IntegrationTests/Tests/Nyanotrasen/Metempsychosis/MetempsychosisTest.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Content.Server.Nyanotrasen.Cloning; -using Content.Shared.Humanoid.Prototypes; -using Content.Shared.Random; -using Robust.Shared.Prototypes; - -namespace Content.IntegrationTests.Tests.DeltaV; - -[TestFixture] -[TestOf(typeof(MetempsychoticMachineSystem))] -public sealed class MetempsychosisTest -{ - [Test] - public async Task AllHumanoidPoolSpeciesExist() - { - await using var pair = await PoolManager.GetServerClient(); - var server = pair.Server; - // Per RobustIntegrationTest.cs, wait until state is settled to access it. - await server.WaitIdleAsync(); - - var prototypeManager = server.ResolveDependency(); - - var metemComponent = new MetempsychoticMachineComponent(); - - await server.WaitAssertion(() => - { - prototypeManager.TryIndex(metemComponent.MetempsychoticHumanoidPool, - out var humanoidPool); - prototypeManager.TryIndex(metemComponent.MetempsychoticNonHumanoidPool, - out var nonHumanoidPool); - - Assert.That(humanoidPool, Is.Not.Null, "MetempsychoticHumanoidPool is null!"); - Assert.That(nonHumanoidPool, Is.Not.Null, "MetempsychoticNonHumanoidPool is null!"); - - Assert.That(humanoidPool.Weights, Is.Not.Empty, - "MetempsychoticHumanoidPool has no valid prototypes!"); - Assert.That(nonHumanoidPool.Weights, Is.Not.Empty, - "MetempsychoticNonHumanoidPool has no valid prototypes!"); - - foreach (var key in humanoidPool.Weights.Keys) - { - Assert.That(prototypeManager.TryIndex(key, out _), - $"MetempsychoticHumanoidPool has invalid prototype {key}!"); - } - - foreach (var key in nonHumanoidPool.Weights.Keys) - { - Assert.That(prototypeManager.TryIndex(key, out _), - $"MetempsychoticNonHumanoidPool has invalid prototype {key}!"); - } - }); - await pair.CleanReturnAsync(); - } -} diff --git a/Content.Server/Cloning/CloningConsoleSystem.cs b/Content.Server/Cloning/CloningConsoleSystem.cs index c95c37312e..524cbe80e4 100644 --- a/Content.Server/Cloning/CloningConsoleSystem.cs +++ b/Content.Server/Cloning/CloningConsoleSystem.cs @@ -32,7 +32,7 @@ public sealed class CloningConsoleSystem : EntitySystem [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!; [Dependency] private readonly SharedMindSystem _mindSystem = default!; - + public override void Initialize() { base.Initialize(); @@ -52,14 +52,16 @@ private void OnInit(EntityUid uid, CloningConsoleComponent component, ComponentI } private void OnButtonPressed(EntityUid uid, CloningConsoleComponent consoleComponent, UiButtonPressedMessage args) { - if (!_powerReceiverSystem.IsPowered(uid)) + if (!_powerReceiverSystem.IsPowered(uid) + || consoleComponent.GeneticScanner is null + || consoleComponent.CloningPod is null + || !TryComp(consoleComponent.CloningPod.Value, out var cloningPod)) return; switch (args.Button) { case UiButton.Clone: - if (consoleComponent.GeneticScanner != null && consoleComponent.CloningPod != null) - TryClone(uid, consoleComponent.CloningPod.Value, consoleComponent.GeneticScanner.Value, consoleComponent: consoleComponent); + TryClone(uid, consoleComponent.CloningPod.Value, consoleComponent.GeneticScanner.Value, cloningPod, consoleComponent: consoleComponent); break; } UpdateUserInterface(uid, consoleComponent); @@ -93,13 +95,15 @@ private void OnMapInit(EntityUid uid, CloningConsoleComponent component, MapInit private void OnNewLink(EntityUid uid, CloningConsoleComponent component, NewLinkEvent args) { - if (TryComp(args.Sink, out var scanner) && args.SourcePort == CloningConsoleComponent.ScannerPort) + if (TryComp(args.Sink, out var scanner) + && args.SourcePort == CloningConsoleComponent.ScannerPort) { component.GeneticScanner = args.Sink; scanner.ConnectedConsole = uid; } - if (TryComp(args.Sink, out var pod) && args.SourcePort == CloningConsoleComponent.PodPort) + if (TryComp(args.Sink, out var pod) + && args.SourcePort == CloningConsoleComponent.PodPort) { component.CloningPod = args.Sink; pod.ConnectedConsole = uid; @@ -125,11 +129,10 @@ private void OnUIOpen(EntityUid uid, CloningConsoleComponent component, AfterAct private void OnAnchorChanged(EntityUid uid, CloningConsoleComponent component, ref AnchorStateChangedEvent args) { - if (args.Anchored) - { - RecheckConnections(uid, component.CloningPod, component.GeneticScanner, component); + if (!args.Anchored + || !RecheckConnections(uid, component.CloningPod, component.GeneticScanner, component)) return; - } + UpdateUserInterface(uid, component); } @@ -148,49 +151,52 @@ public void UpdateUserInterface(EntityUid consoleUid, CloningConsoleComponent co _uiSystem.SetUiState(ui, newState); } - public void TryClone(EntityUid uid, EntityUid cloningPodUid, EntityUid scannerUid, CloningPodComponent? cloningPod = null, MedicalScannerComponent? scannerComp = null, CloningConsoleComponent? consoleComponent = null) + public void TryClone(EntityUid uid, EntityUid cloningPodUid, EntityUid scannerUid, CloningPodComponent cloningPod, MedicalScannerComponent? scannerComp = null, CloningConsoleComponent? consoleComponent = null) { - if (!Resolve(uid, ref consoleComponent) || !Resolve(cloningPodUid, ref cloningPod) || !Resolve(scannerUid, ref scannerComp)) - return; - - if (!Transform(cloningPodUid).Anchored || !Transform(scannerUid).Anchored) - return; - - if (!consoleComponent.CloningPodInRange || !consoleComponent.GeneticScannerInRange) + if (!Resolve(uid, ref consoleComponent) + || !Resolve(scannerUid, ref scannerComp) + || !Transform(cloningPodUid).Anchored + || !Transform(scannerUid).Anchored + || !consoleComponent.CloningPodInRange + || !consoleComponent.GeneticScannerInRange) return; var body = scannerComp.BodyContainer.ContainedEntity; - if (body is null) + if (body is null + || !_mindSystem.TryGetMind(body.Value, out var mindId, out var mind) + || mind.UserId.HasValue == false + || mind.Session == null) return; - if (!_mindSystem.TryGetMind(body.Value, out var mindId, out var mind)) - return; - - if (mind.UserId.HasValue == false || mind.Session == null) - return; - // Nyano: Adds scannerComp.MetemKarmaBonus - if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier, scannerComp.MetemKarmaBonus)) - _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}."); + if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier)) + { + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} started cloning {ToPrettyString(body.Value)}."); + _cloningSystem.AttemptCloning(cloningPodUid, cloningPod); + } } - public void RecheckConnections(EntityUid console, EntityUid? cloningPod, EntityUid? scanner, CloningConsoleComponent? consoleComp = null) + public bool RecheckConnections(EntityUid console, EntityUid? cloningPod, EntityUid? scanner, CloningConsoleComponent? consoleComp = null) { if (!Resolve(console, ref consoleComp)) - return; + return false; + var connected = true; if (scanner != null) { - Transform(scanner.Value).Coordinates.TryDistance(EntityManager, Transform((console)).Coordinates, out float scannerDistance); + Transform(scanner.Value).Coordinates.TryDistance(EntityManager, Transform(console).Coordinates, out float scannerDistance); consoleComp.GeneticScannerInRange = scannerDistance <= consoleComp.MaxDistance; + connected = false; } if (cloningPod != null) { - Transform(cloningPod.Value).Coordinates.TryDistance(EntityManager, Transform((console)).Coordinates, out float podDistance); + Transform(cloningPod.Value).Coordinates.TryDistance(EntityManager, Transform(console).Coordinates, out float podDistance); consoleComp.CloningPodInRange = podDistance <= consoleComp.MaxDistance; + connected = false; } UpdateUserInterface(console, consoleComp); + return connected; } private CloningConsoleBoundUserInterfaceState GetUserInterfaceState(CloningConsoleComponent consoleComponent) { @@ -206,25 +212,19 @@ private CloningConsoleBoundUserInterfaceState GetUserInterfaceState(CloningConso EntityUid? scanBody = scanner.BodyContainer.ContainedEntity; // GET STATE - if (scanBody == null || !HasComp(scanBody)) + if (scanBody == null + || !HasComp(scanBody)) clonerStatus = ClonerStatus.ScannerEmpty; else { scanBodyInfo = MetaData(scanBody.Value).EntityName; if (!_mobStateSystem.IsDead(scanBody.Value)) - { clonerStatus = ClonerStatus.ScannerOccupantAlive; - } - else - { - if (!_mindSystem.TryGetMind(scanBody.Value, out _, out var mind) || - mind.UserId == null || - !_playerManager.TryGetSessionById(mind.UserId.Value, out _)) - { - clonerStatus = ClonerStatus.NoMindDetected; - } - } + else if (!_mindSystem.TryGetMind(scanBody.Value, out _, out var mind) + || mind.UserId == null + || !_playerManager.TryGetSessionById(mind.UserId.Value, out _)) + clonerStatus = ClonerStatus.NoMindDetected; } } @@ -240,7 +240,7 @@ private CloningConsoleBoundUserInterfaceState GetUserInterfaceState(CloningConso EntityUid? cloneBody = clonePod.BodyContainer.ContainedEntity; clonerMindPresent = clonePod.Status == CloningPodStatus.Cloning; - if (HasComp(consoleComponent.CloningPod)) + if (clonePod.ActivelyCloning) { if (cloneBody != null) cloneBodyInfo = Identity.Name(cloneBody.Value, EntityManager); @@ -248,9 +248,7 @@ private CloningConsoleBoundUserInterfaceState GetUserInterfaceState(CloningConso } } else - { clonerStatus = ClonerStatus.NoClonerDetected; - } return new CloningConsoleBoundUserInterfaceState( scanBodyInfo, diff --git a/Content.Server/Cloning/CloningSystem.Utility.cs b/Content.Server/Cloning/CloningSystem.Utility.cs new file mode 100644 index 0000000000..408e1cf24a --- /dev/null +++ b/Content.Server/Cloning/CloningSystem.Utility.cs @@ -0,0 +1,360 @@ +using Content.Server.Cloning.Components; +using Content.Shared.Atmos; +using Content.Shared.CCVar; +using Content.Shared.Chemistry.Components; +using Content.Shared.Cloning; +using Content.Shared.Damage; +using Content.Shared.Emag.Components; +using Content.Shared.Humanoid; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Robust.Shared.Physics.Components; +using Robust.Shared.Random; +using Content.Shared.Speech; +using Content.Shared.Preferences; +using Content.Shared.Emoting; +using Content.Server.Speech.Components; +using Content.Server.StationEvents.Components; +using Content.Server.Ghost.Roles.Components; +using Robust.Shared.GameObjects.Components.Localization; +using Content.Shared.SSDIndicator; +using Content.Shared.Damage.ForceSay; +using Content.Shared.Chat; +using Content.Server.Body.Components; +using Content.Shared.Abilities.Psionics; +using Content.Shared.Language.Components; +using Content.Shared.Language; +using Content.Shared.Nutrition.Components; +using Robust.Shared.Enums; + +namespace Content.Server.Cloning; + +public sealed partial class CloningSystem +{ + internal void TransferMindToClone(EntityUid mindId, MindComponent mind) + { + if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) + || !EntityManager.EntityExists(entity) + || !TryComp(entity, out var mindComp) + || mindComp.Mind != null) + return; + + _mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind); + _mindSystem.UnVisit(mindId, mind); + ClonesWaitingForMind.Remove(mind); + } + private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message) + { + if (clonedComponent.Parent == EntityUid.Invalid + || !EntityManager.EntityExists(clonedComponent.Parent) + || !TryComp(clonedComponent.Parent, out var cloningPodComponent) + || uid != cloningPodComponent.BodyContainer.ContainedEntity) + { + EntityManager.RemoveComponent(uid); + return; + } + UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent); + } + + /// + /// Test if the body to be cloned has any conditions that would prevent cloning from taking place. + /// Or, if the body has a particular reason to make cloning more difficult. + /// + private bool CheckUncloneable(EntityUid uid, EntityUid bodyToClone, CloningPodComponent clonePod, out float cloningCostMultiplier) + { + var ev = new AttemptCloningEvent(uid, clonePod.DoMetempsychosis); + RaiseLocalEvent(bodyToClone, ref ev); + cloningCostMultiplier = ev.CloningCostMultiplier; + + if (ev.Cancelled && ev.CloningFailMessage is not null) + { + _chatSystem.TrySendInGameICMessage(uid, + Loc.GetString(ev.CloningFailMessage), + InGameICChatType.Speak, false); + return false; + } + + return true; + } + + /// + /// Checks the body's physics component and any previously obtained modifiers to determine biomass cost. + /// If there is insufficient biomass, the cloning cannot start. + /// + private bool CheckBiomassCost(EntityUid uid, PhysicsComponent physics, CloningPodComponent clonePod, float cloningCostMultiplier = 1) + { + if (clonePod.ConnectedConsole is null) + return false; + + var cloningCost = (int) Math.Round(physics.FixturesMass + * _config.GetCVar(CCVars.CloningBiomassCostMultiplier) + * clonePod.BiomassCostMultiplier + * cloningCostMultiplier); + + if (_material.GetMaterialAmount(uid, clonePod.RequiredMaterial) < cloningCost) + { + _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false); + return false; + } + + _material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost); + clonePod.UsedBiomass = cloningCost; + + return true; + } + + /// + /// Tests the original body for genetic damage, while returning the cloning damage for later damage. + /// The body's cellular damage is also used as a potential failure state, giving a chance for the cloning to fail immediately. + /// + private bool CheckGeneticDamage(EntityUid uid, EntityUid bodyToClone, CloningPodComponent clonePod, out float geneticDamage, float failChanceModifier = 1) + { + geneticDamage = 0; + if (clonePod.DoMetempsychosis) + return false; + + if (TryComp(bodyToClone, out var damageable) + && damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg) + && clonePod.ConnectedConsole is not null) + { + geneticDamage += (float) cellularDmg; + var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1); + chance *= failChanceModifier; + + if (cellularDmg > 0) + _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false); + + if (_random.Prob(chance)) + { + CauseCloningFail(uid, clonePod); + return true; + } + } + return false; + } + + /// + /// When this condition is called, it sets the cloning pod to its fail condition. + /// Such that when the cloning timer ends, the body that would be created, is turned into clone soup. + /// + private void CauseCloningFail(EntityUid uid, CloningPodComponent component) + { + UpdateStatus(uid, CloningPodStatus.Gore, component); + component.FailedClone = true; + component.ActivelyCloning = true; + } + + /// + /// This is the success condition for cloning. At the end of the timer, if nothing interrupted it, this function is called to finish the cloning by dispensing the body. + /// + private void Eject(EntityUid uid, CloningPodComponent? clonePod) + { + if (!Resolve(uid, ref clonePod) + || clonePod.BodyContainer.ContainedEntity is null) + return; + + var entity = clonePod.BodyContainer.ContainedEntity.Value; + EntityManager.RemoveComponent(entity); + _containerSystem.Remove(entity, clonePod.BodyContainer); + clonePod.CloningProgress = 0f; + clonePod.UsedBiomass = 0; + UpdateStatus(uid, CloningPodStatus.Idle, clonePod); + clonePod.ActivelyCloning = false; + } + + /// + /// And now we turn it over to Chef Pod to make soup! + /// + private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod) + { + if (clonePod.BodyContainer.ContainedEntity is not null) + { + var entity = clonePod.BodyContainer.ContainedEntity.Value; + if (TryComp(entity, out var physics) + && TryComp(entity, out var bloodstream)) + MakeAHugeMess(uid, physics, bloodstream); + else MakeAHugeMess(uid); + + QueueDel(entity); + } + else MakeAHugeMess(uid); + + clonePod.FailedClone = false; + clonePod.CloningProgress = 0f; + UpdateStatus(uid, CloningPodStatus.Idle, clonePod); + if (HasComp(uid)) + { + _audio.PlayPvs(clonePod.ScreamSound, uid); + Spawn(clonePod.MobSpawnId, Transform(uid).Coordinates); + } + + if (!HasComp(uid)) + _material.SpawnMultipleFromMaterial(_random.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates); + + clonePod.UsedBiomass = 0; + clonePod.ActivelyCloning = false; + } + + /// + /// The body coming out of the machine isn't guaranteed to even be a Humanoid. + /// This function makes sure the body is "Human Playable", with no funny business. + /// + private void CleanupCloneComponents(EntityUid uid, EntityUid bodyToClone, bool forceOldProfile, bool doMetempsychosis) + { + if (forceOldProfile + && TryComp(bodyToClone, out var psionic)) + { + var newPsionic = _serialization.CreateCopy(psionic, null, false, true); + AddComp(uid, newPsionic, true); + } + + if (TryComp(bodyToClone, out var oldKnowLangs)) + { + var newKnowLangs = _serialization.CreateCopy(oldKnowLangs, null, false, true); + AddComp(uid, newKnowLangs, true); + } + + + if (TryComp(bodyToClone, out var oldSpeakLangs)) + { + var newSpeakLangs = _serialization.CreateCopy(oldSpeakLangs, null, false, true); + AddComp(uid, newSpeakLangs, true); + } + + if (doMetempsychosis) + EnsureComp(uid); + + EnsureComp(uid); + EnsureComp(uid); + EnsureComp(uid); + EnsureComp(uid); + EnsureComp(uid); + RemComp(uid); + RemComp(uid); + RemComp(uid); + RemComp(uid); + _tag.AddTag(uid, "DoorBumpOpener"); + } + + /// + /// When failing to clone, much of the failed body is dissolved into a slurry of Ammonia and Blood, which spills from the machine. + /// + /// + /// WOE BEFALLS WHOEVER FAILS TO CLONE A LAMIA + /// + private void MakeAHugeMess(EntityUid uid, PhysicsComponent? physics = null, BloodstreamComponent? blood = null) + { + var tileMix = _atmosphereSystem.GetTileMixture(Transform(uid).GridUid, null, _transformSystem.GetGridTilePositionOrDefault((uid, Transform(uid))), true); + Solution bloodSolution = new(); + + tileMix?.AdjustMoles(Gas.Ammonia, 0.5f + * ((physics is not null) + ? physics.Mass + : 71)); + + bloodSolution.AddReagent("blood", 0.8f + * ((blood is not null) + ? blood.BloodMaxVolume + : 300)); + + _puddleSystem.TrySpillAt(uid, bloodSolution, out _); + } + + /// + /// Modify the clone's hunger and thirst values by an amount set in the cloningPod. + /// + private void UpdateHungerAndThirst(EntityUid uid, CloningPodComponent cloningPod) + { + if (cloningPod.HungerAdjustment != 0 + && TryComp(uid, out var hungerComponent)) + _hunger.SetHunger(uid, cloningPod.HungerAdjustment, hungerComponent); + + if (cloningPod.ThirstAdjustment != 0 + && TryComp(uid, out var thirstComponent)) + _thirst.SetThirst(uid, thirstComponent, cloningPod.ThirstAdjustment); + + if (cloningPod.DrunkTimer != 0) + _drunk.TryApplyDrunkenness(uid, cloningPod.DrunkTimer); + } + + /// + /// Updates the HumanoidAppearanceComponent of the clone. + /// If a species swap is occuring, this updates all relevant information as per server config. + /// + private void UpdateCloneAppearance( + EntityUid mob, + HumanoidCharacterProfile pref, + HumanoidAppearanceComponent humanoid, + List sexes, + Gender oldGender, + bool switchingSpecies, + bool forceOldProfile, + out Gender gender) + { + gender = oldGender; + if (!TryComp(mob, out var newHumanoid)) + return; + + if (switchingSpecies && !forceOldProfile) + { + var flavorText = _serialization.CreateCopy(pref.FlavorText, null, false, true); + var oldName = _serialization.CreateCopy(pref.Name, null, false, true); + + pref = HumanoidCharacterProfile.RandomWithSpecies(newHumanoid.Species); + + if (sexes.Contains(humanoid.Sex) + && _config.GetCVar(CCVars.CloningPreserveSex)) + pref = pref.WithSex(humanoid.Sex); + + if (_config.GetCVar(CCVars.CloningPreserveGender)) + pref = pref.WithGender(humanoid.Gender); + else gender = humanoid.Gender; + + if (_config.GetCVar(CCVars.CloningPreserveAge)) + pref = pref.WithAge(humanoid.Age); + + if (_config.GetCVar(CCVars.CloningPreserveHeight)) + pref = pref.WithHeight(humanoid.Height); + + if (_config.GetCVar(CCVars.CloningPreserveWidth)) + pref = pref.WithWidth(humanoid.Width); + + if (_config.GetCVar(CCVars.CloningPreserveName)) + pref = pref.WithName(oldName); + + if (_config.GetCVar(CCVars.CloningPreserveFlavorText)) + pref = pref.WithFlavorText(flavorText); + + _humanoidSystem.LoadProfile(mob, pref); + } + } + + /// + /// Optionally makes sure that pronoun preferences are preserved by the clone. + /// Although handled here, the swap (if it occurs) happens during UpdateCloneAppearance. + /// + /// + /// + private void UpdateGrammar(EntityUid mob, Gender gender) + { + var grammar = EnsureComp(mob); + grammar.ProperNoun = true; + grammar.Gender = gender; + Dirty(mob, grammar); + } + + /// + /// Optionally puts the clone in crit with high Cellular damage. + /// Medbay should use Cryogenics to "Finish" clones. Doxarubixadone is perfect for this. + /// + private void UpdateCloneDamage(EntityUid mob, CloningPodComponent clonePodComp, float geneticDamage) + { + if (!clonePodComp.DoGeneticDamage + || !HasComp(mob) + || !_thresholds.TryGetThresholdForState(mob, Shared.Mobs.MobState.Critical, out var threshold)) + return; + DamageSpecifier damage = new(); + damage.DamageDict.Add("Cellular", (int) threshold + 1 + geneticDamage); + _damageable.TryChangeDamage(mob, damage, true); + } +} diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index 5d311f3ce1..7931fae477 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -9,13 +9,9 @@ using Content.Server.Materials; using Content.Server.Popups; using Content.Server.Power.EntitySystems; -using Content.Shared.Atmos; -using Content.Shared.CCVar; -using Content.Shared.Chemistry.Components; using Content.Shared.Cloning; using Content.Shared.Damage; using Content.Shared.DeviceLinking.Events; -using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; using Content.Shared.Examine; using Content.Shared.GameTicking; @@ -23,6 +19,7 @@ using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Mobs.Systems; +using Content.Shared.Random; using Content.Shared.Roles.Jobs; using Robust.Server.Containers; using Robust.Server.GameObjects; @@ -33,431 +30,342 @@ using Robust.Shared.Physics.Components; using Robust.Shared.Prototypes; using Robust.Shared.Random; -using Content.Server.Traits.Assorted; //Nyano - Summary: allows the potential psionic ability to be written to the character. -using Content.Server.Psionics; //DeltaV needed for Psionic Systems -using Content.Shared.Speech; //DeltaV Start Metem Usings using Content.Shared.Tag; using Content.Shared.Preferences; -using Content.Shared.Emoting; -using Content.Server.Speech.Components; -using Content.Server.StationEvents.Components; -using Content.Server.Ghost.Roles.Components; -using Content.Server.Nyanotrasen.Cloning; using Content.Shared.Humanoid.Prototypes; -using Robust.Shared.GameObjects.Components.Localization; //DeltaV End Metem Usings -using Content.Server.EntityList; -using Content.Shared.SSDIndicator; -using Content.Shared.Damage.ForceSay; -using Content.Server.Polymorph.Components; -using Content.Shared.Chat; -using Content.Shared.Abilities.Psionics; - -namespace Content.Server.Cloning +using Content.Shared.Random.Helpers; +using Content.Shared.Contests; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Utility; +using Timer = Robust.Shared.Timing.Timer; +using Content.Server.Power.Components; +using Content.Shared.Drunk; +using Content.Shared.Nutrition.EntitySystems; + +namespace Content.Server.Cloning; + +public sealed partial class CloningSystem : EntitySystem { - public sealed class CloningSystem : EntitySystem + [Dependency] private readonly DeviceLinkSystem _signalSystem = default!; + [Dependency] private readonly IPlayerManager _playerManager = null!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly EuiManager _euiManager = null!; + [Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!; + [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; + [Dependency] private readonly ContainerSystem _containerSystem = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; + [Dependency] private readonly TransformSystem _transformSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly PuddleSystem _puddleSystem = default!; + [Dependency] private readonly ChatSystem _chatSystem = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly MaterialStorageSystem _material = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly SharedMindSystem _mindSystem = default!; + [Dependency] private readonly MetaDataSystem _metaSystem = default!; + [Dependency] private readonly SharedJobSystem _jobs = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly ContestsSystem _contests = default!; + [Dependency] private readonly ISerializationManager _serialization = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly HungerSystem _hunger = default!; + [Dependency] private readonly ThirstSystem _thirst = default!; + [Dependency] private readonly SharedDrunkSystem _drunk = default!; + [Dependency] private readonly MobThresholdSystem _thresholds = default!; + public readonly Dictionary ClonesWaitingForMind = new(); + + public override void Initialize() { - [Dependency] private readonly DeviceLinkSystem _signalSystem = default!; - [Dependency] private readonly IPlayerManager _playerManager = null!; - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly EuiManager _euiManager = null!; - [Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; - [Dependency] private readonly ContainerSystem _containerSystem = default!; - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; - [Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!; - [Dependency] private readonly IRobustRandom _robustRandom = default!; - [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; - [Dependency] private readonly TransformSystem _transformSystem = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - [Dependency] private readonly PuddleSystem _puddleSystem = default!; - [Dependency] private readonly ChatSystem _chatSystem = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly IConfigurationManager _configManager = default!; - [Dependency] private readonly MaterialStorageSystem _material = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly SharedMindSystem _mindSystem = default!; - [Dependency] private readonly MetaDataSystem _metaSystem = default!; - [Dependency] private readonly SharedJobSystem _jobs = default!; - [Dependency] private readonly MetempsychoticMachineSystem _metem = default!; //DeltaV - [Dependency] private readonly TagSystem _tag = default!; //DeltaV - - public readonly Dictionary ClonesWaitingForMind = new(); - public const float EasyModeCloningCost = 0.7f; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(Reset); - SubscribeLocalEvent(HandleMindAdded); - SubscribeLocalEvent(OnPortDisconnected); - SubscribeLocalEvent(OnAnchor); - SubscribeLocalEvent(OnExamined); - SubscribeLocalEvent(OnEmagged); - } - - private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args) - { - clonePod.BodyContainer = _containerSystem.EnsureContainer(uid, "clonepod-bodyContainer"); - _signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort); - } - - internal void TransferMindToClone(EntityUid mindId, MindComponent mind) - { - if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) || - !EntityManager.EntityExists(entity) || - !TryComp(entity, out var mindComp) || - mindComp.Mind != null) - return; - - _mindSystem.TransferTo(mindId, entity, ghostCheckOverride: true, mind: mind); - _mindSystem.UnVisit(mindId, mind); - ClonesWaitingForMind.Remove(mind); - } - - private void HandleMindAdded(EntityUid uid, BeingClonedComponent clonedComponent, MindAddedMessage message) - { - if (clonedComponent.Parent == EntityUid.Invalid || - !EntityManager.EntityExists(clonedComponent.Parent) || - !TryComp(clonedComponent.Parent, out var cloningPodComponent) || - uid != cloningPodComponent.BodyContainer.ContainedEntity) - { - EntityManager.RemoveComponent(uid); - return; - } - UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent); - } + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(Reset); + SubscribeLocalEvent(HandleMindAdded); + SubscribeLocalEvent(OnPortDisconnected); + SubscribeLocalEvent(OnAnchor); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnEmagged); + SubscribeLocalEvent(OnPowerChanged); + } - private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args) - { - pod.ConnectedConsole = null; - } + private void OnPortDisconnected(EntityUid uid, CloningPodComponent pod, PortDisconnectedEvent args) + { + pod.ConnectedConsole = null; + } - private void OnAnchor(EntityUid uid, CloningPodComponent component, ref AnchorStateChangedEvent args) - { - if (component.ConnectedConsole == null || !TryComp(component.ConnectedConsole, out var console)) - return; + private void OnAnchor(EntityUid uid, CloningPodComponent component, ref AnchorStateChangedEvent args) + { + if (component.ActivelyCloning) + CauseCloningFail(uid, component); - if (args.Anchored) - { - _cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console); - return; - } - _cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console); - } + if (component.ConnectedConsole == null + || !TryComp(component.ConnectedConsole, out var console) + || !args.Anchored + || !_cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console)) + return; - private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid)) - return; + _cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console); + } - args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial)))); - } - // Nyano: Adds float karmaBonus - public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1, float karmaBonus = 0.25f) - { - if (!Resolve(uid, ref clonePod)) - return false; + private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args) + { + if (!args.IsInDetailsRange + || !_powerReceiverSystem.IsPowered(uid)) + return; - if (HasComp(uid)) - return false; + args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial)))); + } + private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args) + { + clonePod.BodyContainer = _containerSystem.EnsureContainer(uid, "clonepod-bodyContainer"); + _signalSystem.EnsureSinkPorts(uid, CloningPodComponent.PodPort); + } - var mind = mindEnt.Comp; - if (ClonesWaitingForMind.TryGetValue(mind, out var clone)) - { - if (EntityManager.EntityExists(clone) && - !_mobStateSystem.IsDead(clone) && - TryComp(clone, out var cloneMindComp) && - (cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt)) - return false; // Mind already has clone + private void OnPowerChanged(EntityUid uid, CloningPodComponent component, PowerChangedEvent args) + { + if (!args.Powered && component.ActivelyCloning) + CauseCloningFail(uid, component); + } - ClonesWaitingForMind.Remove(mind); - } + /// + /// On emag, spawns a failed clone when cloning process fails which attacks nearby crew. + /// + private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args) + { + if (!this.IsPowered(uid, EntityManager)) + return; - if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value)) - return false; // Body controlled by mind is not dead + if (clonePod.ActivelyCloning) + CauseCloningFail(uid, clonePod); - // Yes, we still need to track down the client because we need to open the Eui - if (mind.UserId == null || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client)) - return false; // If we can't track down the client, we can't offer transfer. That'd be quite bad. + _audio.PlayPvs(clonePod.SparkSound, uid); + _popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid); + args.Handled = true; + } - if (!TryComp(bodyToClone, out var humanoid)) - return false; // whatever body was to be cloned, was not a humanoid + private void Reset(RoundRestartCleanupEvent ev) + { + ClonesWaitingForMind.Clear(); + } - // Begin Nyano-code: allow paradox anomalies to be cloned. - var pref = humanoid.LastProfileLoaded; + /// + /// The master function behind Cloning, called by the cloning console via button press to start the cloning process. + /// + public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity mindEnt, CloningPodComponent clonePod, float failChanceModifier = 1) + { + if (!_mobStateSystem.IsDead(bodyToClone) + || clonePod.ActivelyCloning + || clonePod.ConnectedConsole == null + || !CheckUncloneable(uid, bodyToClone, clonePod, out var cloningCostMultiplier) + || !TryComp(bodyToClone, out var humanoid) + || !TryComp(bodyToClone, out var physics)) + return false; + + var mind = mindEnt.Comp; + if (ClonesWaitingForMind.TryGetValue(mind, out var clone)) + { + if (EntityManager.EntityExists(clone) && + !_mobStateSystem.IsDead(clone) && + TryComp(clone, out var cloneMindComp) && + (cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt)) + return false; // Mind already has clone - if (pref == null) - return false; - // End Nyano-code - if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype)) - return false; + ClonesWaitingForMind.Remove(mind); + } - if (!TryComp(bodyToClone, out var physics)) - return false; + if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value) + || mind.UserId == null + || !_playerManager.TryGetSessionById(mind.UserId.Value, out var client) + || !CheckBiomassCost(uid, physics, clonePod, cloningCostMultiplier)) + return false; - var cloningCost = (int) Math.Round(physics.FixturesMass); + // Special handling for humanoid data related to metempsychosis. This function is needed for Paradox Anomaly code to play nice with reincarnated people + var pref = humanoid.LastProfileLoaded; + if (pref == null + || !_prototypeManager.TryIndex(humanoid.Species, out var speciesPrototype)) + return false; - if (_configManager.GetCVar(CCVars.BiomassEasyMode)) - cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost); + // Yes, this can return true without making a body. If it returns true, we're making clone soup instead. + if (CheckGeneticDamage(uid, bodyToClone, clonePod, out var geneticDamage, failChanceModifier)) + return true; - // Check if they have the uncloneable trait - if (TryComp(bodyToClone, out _)) - { - if (clonePod.ConnectedConsole != null) - { - _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, - Loc.GetString("cloning-console-uncloneable-trait-error"), - InGameICChatType.Speak, false); - } + var mob = FetchAndSpawnMob(uid, clonePod, pref, speciesPrototype, humanoid, bodyToClone, geneticDamage); - return false; - } + var ev = new CloningEvent(bodyToClone, mob); + RaiseLocalEvent(bodyToClone, ref ev); - // biomass checks - var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial); + if (!ev.NameHandled) + _metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName); - if (biomassAmount < cloningCost) - { - if (clonePod.ConnectedConsole != null) - _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false); - return false; - } + var cloneMindReturn = EntityManager.AddComponent(mob); + cloneMindReturn.Mind = mindEnt.Comp; + cloneMindReturn.Parent = uid; + _containerSystem.Insert(mob, clonePod.BodyContainer); + ClonesWaitingForMind.Add(mindEnt.Comp, mob); + UpdateStatus(uid, CloningPodStatus.NoMind, clonePod); + _euiManager.OpenEui(new AcceptCloningEui(mindEnt, mindEnt.Comp, this), client); - _material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost); - clonePod.UsedBiomass = cloningCost; - // end of biomass checks + clonePod.ActivelyCloning = true; - // genetic damage checks - if (TryComp(bodyToClone, out var damageable) && - damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg)) - { - var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1); - chance *= failChanceModifier; + if (_jobs.MindTryGetJob(mindEnt, out _, out var prototype)) + foreach (var special in prototype.Special) + if (special is AddComponentSpecial) + special.AfterEquip(mob); - if (cellularDmg > 0 && clonePod.ConnectedConsole != null) - _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false); + return true; + } - if (_robustRandom.Prob(chance)) - { - UpdateStatus(uid, CloningPodStatus.Gore, clonePod); - clonePod.FailedClone = true; - AddComp(uid); - return true; - } - // End Nyano-code. - } - // end of genetic damage checks + /// + /// Begins the cloning timer, which at the end can either produce clone soup, or a functional body, depending on if anything interrupts the procedure. + /// + public void AttemptCloning(EntityUid cloningPod, CloningPodComponent cloningPodComponent) + { + if (cloningPodComponent.BodyContainer.ContainedEntity is { Valid: true } entity + && TryComp(entity, out var physics) + && physics.Mass > 71) + Timer.Spawn(TimeSpan.FromSeconds(cloningPodComponent.CloningTime * _contests.MassContest(entity, physics, true)), () => EndCloning(cloningPod, cloningPodComponent)); - var mob = FetchAndSpawnMob(clonePod, pref, speciesPrototype, humanoid, bodyToClone, karmaBonus); //DeltaV Replaces CloneAppearance with Metem/Clone via FetchAndSpawnMob + Timer.Spawn(TimeSpan.FromSeconds(cloningPodComponent.CloningTime), () => EndCloning(cloningPod, cloningPodComponent)); + } - ///Nyano - Summary: adds the potential psionic trait to the reanimated mob. - EnsureComp(mob); + /// + /// Ding, your body is ready. Time to find out if it's soup or solid. + /// + public void EndCloning(EntityUid cloningPod, CloningPodComponent cloningPodComponent) + { + if (!cloningPodComponent.ActivelyCloning + || !_powerReceiverSystem.IsPowered(cloningPod) + || cloningPodComponent.BodyContainer.ContainedEntity == null + || cloningPodComponent.FailedClone) + EndFailedCloning(cloningPod, cloningPodComponent); //Surprise, it's soup! - var ev = new CloningEvent(bodyToClone, mob); - RaiseLocalEvent(bodyToClone, ref ev); + Eject(cloningPod, cloningPodComponent); //Hey look, a body! + } - if (!ev.NameHandled) - _metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName); + public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod) + { + cloningPod.Status = status; + _appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status); + } - var cloneMindReturn = EntityManager.AddComponent(mob); - cloneMindReturn.Mind = mind; - cloneMindReturn.Parent = uid; - _containerSystem.Insert(mob, clonePod.BodyContainer); - ClonesWaitingForMind.Add(mind, mob); - UpdateStatus(uid, CloningPodStatus.NoMind, clonePod); - _euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client); + /// + /// This function handles the Clone vs. Metem logic, as well as creation of the new body. + /// + private EntityUid FetchAndSpawnMob( + EntityUid clonePod, + CloningPodComponent clonePodComp, + HumanoidCharacterProfile pref, + SpeciesPrototype speciesPrototype, + HumanoidAppearanceComponent humanoid, + EntityUid bodyToClone, + float geneticDamage + ) + { + List sexes = new(); + bool switchingSpecies = false; + var toSpawn = speciesPrototype.Prototype; + var forceOldProfile = true; + var oldKarma = 0; + var oldGender = humanoid.Gender; + if (TryComp(bodyToClone, out var oldKarmaComp)) + oldKarma += oldKarmaComp.Score; + + if (clonePodComp.DoMetempsychosis) + { + toSpawn = GetSpawnEntity(bodyToClone, clonePodComp, speciesPrototype, oldKarma, out var newSpecies, out var changeProfile); + forceOldProfile = !changeProfile; + oldKarma++; - AddComp(uid); + if (changeProfile) + geneticDamage = 0; - // TODO: Ideally, components like this should be components on the mind entity so this isn't necessary. - // Add on special job components to the mob. - if (_jobs.MindTryGetJob(mindEnt, out _, out var prototype)) + if (newSpecies != null) { - foreach (var special in prototype.Special) - { - if (special is AddComponentSpecial) - special.AfterEquip(mob); - } - } - - return true; - } + sexes = newSpecies.Sexes; - public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod) - { - cloningPod.Status = status; - _appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status); + if (speciesPrototype.ID != newSpecies.ID) + switchingSpecies = true; + } } + EntityUid mob = Spawn(toSpawn, _transformSystem.GetMapCoordinates(clonePod)); + EnsureComp(mob, out var newKarma); + newKarma.Score += oldKarma; - public override void Update(float frameTime) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var _, out var cloning)) - { - if (!_powerReceiverSystem.IsPowered(uid)) - continue; + UpdateCloneDamage(mob, clonePodComp, geneticDamage); + UpdateCloneAppearance(mob, pref, humanoid, sexes, oldGender, switchingSpecies, forceOldProfile, out var gender); + var ev = new CloningEvent(bodyToClone, mob); + RaiseLocalEvent(bodyToClone, ref ev); - if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone) - continue; + if (!ev.NameHandled) + _metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName); - cloning.CloningProgress += frameTime; - if (cloning.CloningProgress < cloning.CloningTime) - continue; + UpdateGrammar(mob, gender); + CleanupCloneComponents(mob, bodyToClone, forceOldProfile, clonePodComp.DoMetempsychosis); + UpdateHungerAndThirst(mob, clonePodComp); - if (cloning.FailedClone) - EndFailedCloning(uid, cloning); - else - Eject(uid, cloning); - } - } + return mob; + } - /// - /// On emag, spawns a failed clone when cloning process fails which attacks nearby crew. - /// - private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args) + public string GetSpawnEntity(EntityUid oldBody, CloningPodComponent component, SpeciesPrototype oldSpecies, int karma, out SpeciesPrototype? species, out bool changeProfile) + { + changeProfile = true; + species = oldSpecies; + if (!_prototypeManager.TryIndex(component.MetempsychoticHumanoidPool, out var humanoidPool) + || !_prototypeManager.TryIndex(humanoidPool.Pick(), out var speciesPrototype) + || !_prototypeManager.TryIndex(component.MetempsychoticNonHumanoidPool, out var nonHumanoidPool) + || !_prototypeManager.TryIndex(nonHumanoidPool.Pick(), out var entityPrototype)) { - if (!this.IsPowered(uid, EntityManager)) - return; - - _audio.PlayPvs(clonePod.SparkSound, uid); - _popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid); - args.Handled = true; + DebugTools.Assert("Could not index species for metempsychotic machine."); + changeProfile = false; + return oldSpecies.Prototype; } + var chance = (component.HumanoidBaseChance - karma * component.KarmaOffset) * _contests.MindContest(oldBody, true); - public void Eject(EntityUid uid, CloningPodComponent? clonePod) - { - if (!Resolve(uid, ref clonePod)) - return; - - if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime) - return; - - EntityManager.RemoveComponent(entity); - _containerSystem.Remove(entity, clonePod.BodyContainer); - clonePod.CloningProgress = 0f; - clonePod.UsedBiomass = 0; - UpdateStatus(uid, CloningPodStatus.Idle, clonePod); - RemCompDeferred(uid); - } - private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod) - { - clonePod.FailedClone = false; - clonePod.CloningProgress = 0f; - UpdateStatus(uid, CloningPodStatus.Idle, clonePod); - var transform = Transform(uid); - var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform)); - var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true); - if (HasComp(uid)) - { - _audio.PlayPvs(clonePod.ScreamSound, uid); - Spawn(clonePod.MobSpawnId, transform.Coordinates); - } - - Solution bloodSolution = new(); - - var i = 0; - while (i < 1) - { - tileMix?.AdjustMoles(Gas.Ammonia, 6f); - bloodSolution.AddReagent("Blood", 50); - if (_robustRandom.Prob(0.2f)) - i++; - } - _puddleSystem.TrySpillAt(uid, bloodSolution, out _); - - if (!HasComp(uid)) - { - _material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates); - } + var ev = new ReincarnatingEvent(oldBody, chance); + RaiseLocalEvent(oldBody, ref ev); - clonePod.UsedBiomass = 0; - RemCompDeferred(uid); - } + chance = ev.OverrideChance + ? ev.ReincarnationChances + : chance * ev.ReincarnationChanceModifier; - /// - /// Start Nyano Code: Handles fetching the mob and any appearance stuff... - /// - private EntityUid FetchAndSpawnMob(CloningPodComponent clonePod, HumanoidCharacterProfile pref, SpeciesPrototype speciesPrototype, HumanoidAppearanceComponent humanoid, EntityUid bodyToClone, float karmaBonus) + switch (ev.ForcedType) { - List sexes = new(); - bool switchingSpecies = false; - bool applyKarma = false; - var toSpawn = speciesPrototype.Prototype; - TryComp(bodyToClone, out var oldKarma); - - if (TryComp(clonePod.Owner, out var metem)) - { - toSpawn = _metem.GetSpawnEntity(clonePod.Owner, karmaBonus, metem, speciesPrototype, out var newSpecies, oldKarma?.Score); - applyKarma = true; - - if (newSpecies != null) + case ForcedMetempsychosisType.None: + if (!ev.NeverTrulyClone + && chance > 1 + && _random.Prob(chance - 1)) { - sexes = newSpecies.Sexes; - - if (speciesPrototype.ID != newSpecies.ID) - switchingSpecies = true; - - speciesPrototype = newSpecies; + changeProfile = false; + return oldSpecies.Prototype; } - } - var mob = Spawn(toSpawn, Transform(clonePod.Owner).MapPosition); - if (TryComp(mob, out var newHumanoid)) - { - if (switchingSpecies || HasComp(bodyToClone)) + chance = Math.Clamp(chance, 0, 1); + if (_random.Prob(chance)) { - pref = HumanoidCharacterProfile.RandomWithSpecies(newHumanoid.Species); - if (sexes.Contains(humanoid.Sex)) - pref = pref.WithSex(humanoid.Sex); - - pref = pref.WithGender(humanoid.Gender); - pref = pref.WithAge(humanoid.Age); - + species = speciesPrototype; + return speciesPrototype.Prototype; } - _humanoidSystem.LoadProfile(mob, pref); - } + species = null; + return entityPrototype.ID; - if (applyKarma) - { - var karma = EnsureComp(mob); - karma.Score++; - if (oldKarma != null) - karma.Score += oldKarma.Score; - } + case ForcedMetempsychosisType.Clone: + changeProfile = false; + return oldSpecies.Prototype; - var ev = new CloningEvent(bodyToClone, mob); - RaiseLocalEvent(bodyToClone, ref ev); + case ForcedMetempsychosisType.RandomHumanoid: + species = speciesPrototype; + return speciesPrototype.Prototype; - if (!ev.NameHandled) - _metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName); - - var grammar = EnsureComp(mob); - grammar.ProperNoun = true; - grammar.Gender = humanoid.Gender; - Dirty(grammar); - - EnsureComp(mob); - EnsureComp(mob); - EnsureComp(mob); - EnsureComp(mob); - EnsureComp(mob); - EnsureComp(mob); - RemComp(mob); - RemComp(mob); - RemComp(mob); - RemComp(mob); - - _tag.AddTag(mob, "DoorBumpOpener"); - - return mob; - } - //End Nyano Code - public void Reset(RoundRestartCleanupEvent ev) - { - ClonesWaitingForMind.Clear(); + case ForcedMetempsychosisType.RandomNonHumanoid: + species = null; + return entityPrototype.ID; } + changeProfile = false; + return oldSpecies.Prototype; } } diff --git a/Content.Server/Cloning/Components/ActiveCloningPodComponent.cs b/Content.Server/Cloning/Components/ActiveCloningPodComponent.cs deleted file mode 100644 index 11e0e36166..0000000000 --- a/Content.Server/Cloning/Components/ActiveCloningPodComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Content.Server.Cloning.Components; - -/// -/// Shrimply a tracking component for pods that are cloning. -/// -[RegisterComponent] -public sealed partial class ActiveCloningPodComponent : Component -{ -} diff --git a/Content.Server/Nyanotrasen/Cloning/MetempsychosisKarmaComponent.cs b/Content.Server/Cloning/Components/MetempsychosisKarmaComponent.cs similarity index 80% rename from Content.Server/Nyanotrasen/Cloning/MetempsychosisKarmaComponent.cs rename to Content.Server/Cloning/Components/MetempsychosisKarmaComponent.cs index 246495cee0..5f7b7af1cd 100644 --- a/Content.Server/Nyanotrasen/Cloning/MetempsychosisKarmaComponent.cs +++ b/Content.Server/Cloning/Components/MetempsychosisKarmaComponent.cs @@ -1,4 +1,4 @@ -namespace Content.Server.Nyanotrasen.Cloning +namespace Content.Server.Cloning.Components { /// /// This tracks how many times you have already been cloned and lowers your chance of getting a humanoid each time. @@ -6,7 +6,7 @@ namespace Content.Server.Nyanotrasen.Cloning [RegisterComponent] public sealed partial class MetempsychosisKarmaComponent : Component { - [DataField("score")] + [DataField] public int Score = 0; } } diff --git a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs index d07858aec5..eaf04d64b2 100644 --- a/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs +++ b/Content.Server/Medical/BiomassReclaimer/BiomassReclaimerSystem.cs @@ -28,7 +28,6 @@ using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; using Robust.Shared.Physics.Components; -using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.Medical.BiomassReclaimer @@ -81,11 +80,9 @@ public override void Update(float frameTime) } if (reclaimer.ProcessingTimer > 0) - { continue; - } - var actualYield = (int) (reclaimer.CurrentExpectedYield); // can only have integer biomass + var actualYield = (int) reclaimer.CurrentExpectedYield; // Can only have integer biomass physically reclaimer.CurrentExpectedYield = reclaimer.CurrentExpectedYield - actualYield; // store non-integer leftovers _material.SpawnMultipleFromMaterial(actualYield, BiomassPrototype, Transform(uid).Coordinates); @@ -109,13 +106,9 @@ public override void Initialize() private void OnSuicide(Entity ent, ref SuicideEvent args) { - if (args.Handled) - return; - - if (HasComp(ent)) - return; - - if (TryComp(ent, out var power) && !power.Powered) + if (args.Handled + || HasComp(ent) + || TryComp(ent, out var power) && !power.Powered) return; _popup.PopupEntity(Loc.GetString("biomass-reclaimer-suicide-others", ("victim", args.Victim)), ent, PopupType.LargeCaution); @@ -138,11 +131,9 @@ private void OnShutdown(EntityUid uid, ActiveBiomassReclaimerComponent component private void OnPowerChanged(EntityUid uid, BiomassReclaimerComponent component, ref PowerChangedEvent args) { - if (args.Powered) - { - if (component.ProcessingTimer > 0) - EnsureComp(uid); - } + if (args.Powered + && component.ProcessingTimer > 0) + EnsureComp(uid); else RemComp(uid); } @@ -153,16 +144,14 @@ private void OnUnanchorAttempt(EntityUid uid, ActiveBiomassReclaimerComponent co } private void OnAfterInteractUsing(Entity reclaimer, ref AfterInteractUsingEvent args) { - if (!args.CanReach || args.Target == null) - return; - - if (!CanGib(reclaimer, args.Used)) + if (!args.CanReach + || args.Target == null + || !CanGib(reclaimer, args.Used)) return; - if (!TryComp(args.Used, out var physics)) - return; - - var delay = reclaimer.Comp.BaseInsertionDelay * physics.FixturesMass; + var delay = reclaimer.Comp.BaseInsertionDelay * (TryComp(args.Used, out var physics) + ? physics.FixturesMass + : 1); _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, delay, new ReclaimerDoAfterEvent(), reclaimer, target: args.Target, used: args.Used) { BreakOnTargetMove = true, @@ -186,10 +175,11 @@ private void OnClimbedOn(Entity reclaimer, ref Climbe private void OnDoAfter(Entity reclaimer, ref ReclaimerDoAfterEvent args) { - if (args.Handled || args.Cancelled) - return; - - if (args.Args.Used == null || args.Args.Target == null || !HasComp(args.Args.Target.Value)) + if (args.Handled + || args.Cancelled + || args.Args.Used == null + || args.Args.Target == null + || !HasComp(args.Args.Target.Value)) return; _adminLogger.Add(LogType.Action, LogImpact.Extreme, $"{ToPrettyString(args.Args.User):player} used a biomass reclaimer to gib {ToPrettyString(args.Args.Target.Value):target} in {ToPrettyString(reclaimer):reclaimer}"); @@ -207,18 +197,13 @@ private void StartProcessing(EntityUid toProcess, Entity(ent); if (TryComp(toProcess, out var stream)) - { component.BloodReagent = stream.BloodReagent; - } if (TryComp(toProcess, out var butcherableComponent)) - { component.SpawnedEntities = butcherableComponent.SpawnedEntities; - } - var expectedYield = physics.FixturesMass * component.YieldPerUnitMass; - if (HasComp(toProcess)) - expectedYield *= component.ProduceYieldMultiplier; - component.CurrentExpectedYield += expectedYield; + component.CurrentExpectedYield += HasComp(toProcess) + ? physics.FixturesMass * component.YieldPerUnitMass * component.ProduceYieldMultiplier + : physics.FixturesMass * component.YieldPerUnitMass; component.ProcessingTimer = physics.FixturesMass * component.ProcessingTimePerUnitMass; @@ -227,31 +212,22 @@ private void StartProcessing(EntityUid toProcess, Entity reclaimer, EntityUid dragged) { - if (HasComp(reclaimer)) + if (HasComp(reclaimer) + || !Transform(reclaimer).Anchored + || TryComp(reclaimer, out var power) && !power.Powered) return false; bool isPlant = HasComp(dragged); - if (!isPlant && !HasComp(dragged)) - return false; - - if (!Transform(reclaimer).Anchored) - return false; - - if (TryComp(reclaimer, out var power) && !power.Powered) + if (!HasComp(dragged) && (!HasComp(dragged) || reclaimer.Comp.SafetyEnabled && !_mobState.IsDead(dragged))) return false; - if (!isPlant && reclaimer.Comp.SafetyEnabled && !_mobState.IsDead(dragged)) + if (_configManager.GetCVar(CCVars.CloningReclaimSouledBodies) + && HasComp(dragged) + && _minds.TryGetMind(dragged, out _, out var mind) + && mind.UserId != null + && _playerManager.TryGetSessionById(mind.UserId.Value, out _)) return false; - // Reject souled bodies in easy mode. - if (_configManager.GetCVar(CCVars.BiomassEasyMode) && - HasComp(dragged) && - _minds.TryGetMind(dragged, out _, out var mind)) - { - if (mind.UserId != null && _playerManager.TryGetSessionById(mind.UserId.Value, out _)) - return false; - } - return true; } } diff --git a/Content.Server/Medical/MedicalScannerSystem.cs b/Content.Server/Medical/MedicalScannerSystem.cs index 91184ddc16..a6ce43c408 100644 --- a/Content.Server/Medical/MedicalScannerSystem.cs +++ b/Content.Server/Medical/MedicalScannerSystem.cs @@ -15,7 +15,7 @@ using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Robust.Server.Containers; -using static Content.Shared.MedicalScanner.SharedMedicalScannerComponent; // Hmm... +using static Content.Shared.MedicalScanner.SharedMedicalScannerComponent; namespace Content.Server.Medical { @@ -78,22 +78,18 @@ private void OnRelayMovement(EntityUid uid, MedicalScannerComponent scannerCompo private void AddInsertOtherVerb(EntityUid uid, MedicalScannerComponent component, GetVerbsEvent args) { - if (args.Using == null || - !args.CanAccess || - !args.CanInteract || - IsOccupied(component) || - !CanScannerInsert(uid, args.Using.Value, component)) + if (args.Using == null + || !args.CanAccess + || !args.CanInteract + || IsOccupied(component) + || !CanScannerInsert(uid, args.Using.Value, component)) return; - var name = "Unknown"; - if (TryComp(args.Using.Value, out var metadata)) - name = metadata.EntityName; - InteractionVerb verb = new() { Act = () => InsertBody(uid, args.Target, component), Category = VerbCategory.Insert, - Text = name + Text = MetaData(args.Using.Value).EntityName }; args.Verbs.Add(verb); } @@ -115,11 +111,8 @@ private void AddAlternativeVerbs(EntityUid uid, MedicalScannerComponent componen }; args.Verbs.Add(verb); } - - // Self-insert verb - if (!IsOccupied(component) && - CanScannerInsert(uid, args.User, component) && - _blocker.CanMove(args.User)) + else if (CanScannerInsert(uid, args.User, component) + && _blocker.CanMove(args.User)) { AlternativeVerb verb = new() { @@ -147,59 +140,48 @@ private void OnPortDisconnected(EntityUid uid, MedicalScannerComponent component private void OnAnchorChanged(EntityUid uid, MedicalScannerComponent component, ref AnchorStateChangedEvent args) { - if (component.ConnectedConsole == null || !TryComp(component.ConnectedConsole, out var console)) + if (component.ConnectedConsole == null + || !args.Anchored + || !TryComp(component.ConnectedConsole, out var console) + || !_cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, console.CloningPod, uid, console)) return; - if (args.Anchored) - { - _cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, console.CloningPod, uid, console); - return; - } _cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console); } + private MedicalScannerStatus GetStatus(EntityUid uid, MedicalScannerComponent scannerComponent) { - if (this.IsPowered(uid, EntityManager)) - { - var body = scannerComponent.BodyContainer.ContainedEntity; - if (body == null) - return MedicalScannerStatus.Open; + if (!this.IsPowered(uid, EntityManager)) + return MedicalScannerStatus.Off; - if (!TryComp(body.Value, out var state)) - { // Is not alive or dead or critical - return MedicalScannerStatus.Yellow; - } + var body = scannerComponent.BodyContainer.ContainedEntity; + if (body == null) + return MedicalScannerStatus.Open; - return GetStatusFromDamageState(body.Value, state); - } - return MedicalScannerStatus.Off; - } + if (!TryComp(body.Value, out var state)) + return MedicalScannerStatus.Yellow; - public static bool IsOccupied(MedicalScannerComponent scannerComponent) - { - return scannerComponent.BodyContainer.ContainedEntity != null; - } - - private MedicalScannerStatus GetStatusFromDamageState(EntityUid uid, MobStateComponent state) - { - if (_mobStateSystem.IsAlive(uid, state)) + if (_mobStateSystem.IsAlive(body.Value, state)) return MedicalScannerStatus.Green; - if (_mobStateSystem.IsCritical(uid, state)) + if (_mobStateSystem.IsCritical(body.Value, state)) return MedicalScannerStatus.Red; - if (_mobStateSystem.IsDead(uid, state)) + if (_mobStateSystem.IsDead(body.Value, state)) return MedicalScannerStatus.Death; return MedicalScannerStatus.Yellow; } + public static bool IsOccupied(MedicalScannerComponent scannerComponent) + { + return scannerComponent.BodyContainer.ContainedEntity != null; + } + private void UpdateAppearance(EntityUid uid, MedicalScannerComponent scannerComponent) { if (TryComp(uid, out var appearance)) - { _appearance.SetData(uid, MedicalScannerVisuals.Status, GetStatus(uid, scannerComponent), appearance); - } } public override void Update(float frameTime) @@ -214,20 +196,14 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var scanner)) - { UpdateAppearance(uid, scanner); - } } public void InsertBody(EntityUid uid, EntityUid to_insert, MedicalScannerComponent? scannerComponent) { - if (!Resolve(uid, ref scannerComponent)) - return; - - if (scannerComponent.BodyContainer.ContainedEntity != null) - return; - - if (!HasComp(to_insert)) + if (!Resolve(uid, ref scannerComponent) + || scannerComponent.BodyContainer.ContainedEntity != null + || !HasComp(to_insert)) return; _containerSystem.Insert(to_insert, scannerComponent.BodyContainer); @@ -236,10 +212,8 @@ public void InsertBody(EntityUid uid, EntityUid to_insert, MedicalScannerCompone public void EjectBody(EntityUid uid, MedicalScannerComponent? scannerComponent) { - if (!Resolve(uid, ref scannerComponent)) - return; - - if (scannerComponent.BodyContainer.ContainedEntity is not { Valid: true } contained) + if (!Resolve(uid, ref scannerComponent) + || scannerComponent.BodyContainer.ContainedEntity is not { Valid: true } contained) return; _containerSystem.Remove(contained, scannerComponent.BodyContainer); diff --git a/Content.Server/Nyanotrasen/Cloning/MetempsychoticMachineComponent.cs b/Content.Server/Nyanotrasen/Cloning/MetempsychoticMachineComponent.cs deleted file mode 100644 index 0adcc9b5b2..0000000000 --- a/Content.Server/Nyanotrasen/Cloning/MetempsychoticMachineComponent.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Content.Shared.Random; - -namespace Content.Server.Nyanotrasen.Cloning -{ - [RegisterComponent] - public sealed partial class MetempsychoticMachineComponent : Component - { - /// - /// Chance you will spawn as a humanoid instead of a non humanoid. - /// - [DataField("humanoidBaseChance")] - public float HumanoidBaseChance = 0.75f; - - [ValidatePrototypeId] - [DataField("metempsychoticHumanoidPool")] - public string MetempsychoticHumanoidPool = "MetempsychoticHumanoidPool"; - - [ValidatePrototypeId] - [DataField("metempsychoticNonHumanoidPool")] - public string MetempsychoticNonHumanoidPool = "MetempsychoticNonhumanoidPool"; - } -} diff --git a/Content.Server/Nyanotrasen/Cloning/MetempsychoticMachineSystem.cs b/Content.Server/Nyanotrasen/Cloning/MetempsychoticMachineSystem.cs deleted file mode 100644 index 62dc1b078e..0000000000 --- a/Content.Server/Nyanotrasen/Cloning/MetempsychoticMachineSystem.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Content.Shared.Humanoid.Prototypes; -using Content.Shared.Random; -using Content.Shared.Random.Helpers; -using Robust.Shared.Random; -using Robust.Shared.Prototypes; - -namespace Content.Server.Nyanotrasen.Cloning -{ - public sealed class MetempsychoticMachineSystem : EntitySystem - { - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; - - private ISawmill _sawmill = default!; - - public string GetSpawnEntity(EntityUid uid, float karmaBonus, MetempsychoticMachineComponent component, SpeciesPrototype oldSpecies, out SpeciesPrototype? species, int? karma = null) - { - var chance = component.HumanoidBaseChance + karmaBonus; - - if (karma != null) - chance -= ((1 - component.HumanoidBaseChance) * (float) karma); - - if (chance > 1 && _random.Prob(chance - 1)) - { - species = oldSpecies; - return oldSpecies.Prototype; - } - else - chance = 1; - - chance = Math.Clamp(chance, 0, 1); - if (_random.Prob(chance) && - _prototypeManager.TryIndex(component.MetempsychoticHumanoidPool, out var humanoidPool) && - _prototypeManager.TryIndex(humanoidPool.Pick(), out var speciesPrototype)) - { - species = speciesPrototype; - return speciesPrototype.Prototype; - } - else - { - species = null; - _sawmill.Error("Could not index species for metempsychotic machine..."); - return "MobHuman"; - } - } - } -} diff --git a/Content.Server/Traits/Assorted/UncloneableSystem.cs b/Content.Server/Traits/Assorted/UncloneableSystem.cs new file mode 100644 index 0000000000..6f2af10647 --- /dev/null +++ b/Content.Server/Traits/Assorted/UncloneableSystem.cs @@ -0,0 +1,23 @@ +using Content.Shared.Cloning; + +namespace Content.Server.Traits.Assorted +{ + public sealed class UncloneableSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnAttemptCloning); + } + + private void OnAttemptCloning(EntityUid uid, UncloneableComponent component, ref AttemptCloningEvent args) + { + if (args.CloningFailMessage is not null + || args.Cancelled) + return; + + args.CloningFailMessage = "cloning-console-uncloneable-trait-error"; + args.Cancelled = true; + } + } +} diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 099f358105..84a8e460c5 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -1684,16 +1684,63 @@ public static readonly CVarDef public static readonly CVarDef CrewManifestUnsecure = CVarDef.Create("crewmanifest.unsecure", true, CVar.REPLICATED); - /* - * Biomass - */ + #region Cloning + + /// + /// How much should the cost to clone an entity be multiplied by. + /// + public static readonly CVarDef CloningBiomassCostMultiplier = + CVarDef.Create("cloning.biomass_cost_multiplier", 1f, CVar.SERVERONLY); + + /// + /// Whether or not the Biomass Reclaimer is allowed to roundremove bodies with a soul. + /// + public static readonly CVarDef CloningReclaimSouledBodies = + CVarDef.Create("cloning.reclaim_souled_bodies", true, CVar.SERVERONLY); + + /// + /// Controls whether or not Metempsychosis will potentially give people a sex change. + /// + public static readonly CVarDef CloningPreserveSex = + CVarDef.Create("cloning.preserve_sex", false, CVar.SERVERONLY); + + /// + /// Controls whether or not Metempsychosis preserves Pronouns when reincarnating people. + /// + public static readonly CVarDef CloningPreserveGender = + CVarDef.Create("cloning.preserve_gender", true, CVar.SERVERONLY); /// - /// Enabled: Cloning has 70% cost and reclaimer will refuse to reclaim corpses with souls. (For LRP). - /// Disabled: Cloning has full biomass cost and reclaimer can reclaim corpses with souls. (Playtested and balanced for MRP+). + /// Controls whether or not Metempsychosis preserves Age. /// - public static readonly CVarDef BiomassEasyMode = - CVarDef.Create("biomass.easy_mode", false, CVar.SERVERONLY); + public static readonly CVarDef CloningPreserveAge = + CVarDef.Create("cloning.preserve_age", false, CVar.SERVERONLY); + + /// + /// Controls whether or not Metempsychosis preserves height. + /// + public static readonly CVarDef CloningPreserveHeight = + CVarDef.Create("cloning.preserve_height", false, CVar.SERVERONLY); + + /// + /// Controls whether or not Metempsychosis preserves width. + /// + public static readonly CVarDef CloningPreserveWidth = + CVarDef.Create("cloning.preserve_width", false, CVar.SERVERONLY); + + /// + /// Controls whether or not Metempsychosis preserves Names. EG: Are you actually a new person? + /// + public static readonly CVarDef CloningPreserveName = + CVarDef.Create("cloning.preserve_name", true, CVar.SERVERONLY); + + /// + /// Controls whether or not Metempsychosis preserves Flavor Text. + /// + public static readonly CVarDef CloningPreserveFlavorText = + CVarDef.Create("cloning.preserve_flavor_text", true, CVar.SERVERONLY); + + #endregion /* * Anomaly diff --git a/Content.Shared/Cloning/CloningPodComponent.cs b/Content.Shared/Cloning/CloningPodComponent.cs index 88d587c145..082b92e8b1 100644 --- a/Content.Shared/Cloning/CloningPodComponent.cs +++ b/Content.Shared/Cloning/CloningPodComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.DeviceLinking; using Content.Shared.Materials; +using Content.Shared.Random; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.Prototypes; @@ -17,11 +18,14 @@ public sealed partial class CloningPodComponent : Component public ContainerSlot BodyContainer = default!; /// - /// How long the cloning has been going on for. + /// How long the cloning has been going on for /// [ViewVariables] public float CloningProgress = 0; + [DataField] + public float BiomassCostMultiplier = 1; + [ViewVariables] public int UsedBiomass = 70; @@ -29,34 +33,34 @@ public sealed partial class CloningPodComponent : Component public bool FailedClone = false; /// - /// The material that is used to clone entities. + /// The material that is used to clone entities /// - [DataField("requiredMaterial"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public ProtoId RequiredMaterial = "Biomass"; /// - /// The current amount of time it takes to clone a body + /// The current amount of time it takes to clone a body /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public float CloningTime = 30f; /// - /// The mob to spawn on emag + /// The mob to spawn on emag /// - [DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)] + [DataField] public EntProtoId MobSpawnId = "MobAbomination"; /// - /// Emag sound effects. + /// Emag sound effects /// - [DataField("sparkSound")] + [DataField] public SoundSpecifier SparkSound = new SoundCollectionSpecifier("sparks") { Params = AudioParams.Default.WithVolume(8), }; // TODO: Remove this from here when cloning and/or zombies are refactored - [DataField("screamSound")] + [DataField] public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams") { Params = AudioParams.Default.WithVolume(4), @@ -67,6 +71,80 @@ public sealed partial class CloningPodComponent : Component [ViewVariables] public EntityUid? ConnectedConsole; + + /// + /// Tracks whether a Cloner is actively cloning someone + /// + [ViewVariables(VVAccess.ReadWrite)] + public bool ActivelyCloning; + + /// + /// Controls whether a Cloning Pod will add genetic damage to a clone, scaling as the body's crit threshold + 1 + the genetic damage of the body to be cloned + /// + [DataField] + public bool DoGeneticDamage = true; + + /// + /// How much should the cloning pod adjust the hunger of an entity by + /// + [DataField] + public float HungerAdjustment = 50; + + /// + /// How much should the cloning pod adjust the thirst of an entity by + /// + [DataField] + public float ThirstAdjustment = 50; + + /// + /// How much time should the cloning pod give an entity the durnk condition, in seconds + /// + [DataField] + public float DrunkTimer = 300; + + #region Metempsychosis + + /// + /// Controls whether a cloning machine performs the Metempsychosis functions, EG: Is this a Cloner or a Metem Machine? + /// Metempsychosis refers to the metaphysical process of Reincarnation. + /// + /// + /// A Machine with this enabled will essentially create a random new character instead of creating a living version of the old character. + /// Although, the specifics of how much of the new body is a "new character" is highly adjustable in server configuration. + /// + [DataField] + public bool DoMetempsychosis; + + /// + /// How much should each point of Karma decrease the odds of reincarnating as a humanoid + /// + [DataField] + public float KarmaOffset = 0.5f; + + /// + /// The base chances for a Metem Machine to produce a Humanoid. + /// > 1 has a chance of acting like a true Cloner. + /// On a successful roll, produces a random Humanoid. + /// A failed roll poduces a random NonHumanoid. + /// + [DataField] + public float HumanoidBaseChance = 1; + + /// + /// The proto that the Metem Machine picks a random Humanoid from + /// + [ValidatePrototypeId] + [DataField] + public string MetempsychoticHumanoidPool = "MetempsychoticHumanoidPool"; + + /// + /// The proto that the Metem Machine picks a random Non-Humanoid from + /// + [ValidatePrototypeId] + [DataField] + public string MetempsychoticNonHumanoidPool = "MetempsychoticNonhumanoidPool"; + + #endregion } [Serializable, NetSerializable] @@ -84,20 +162,11 @@ public enum CloningPodStatus : byte NoMind } -/// -/// Raised after a new mob got spawned when cloning a humanoid -/// -[ByRefEvent] -public struct CloningEvent +[Serializable, NetSerializable] +public enum ForcedMetempsychosisType : byte { - public bool NameHandled = false; - - public readonly EntityUid Source; - public readonly EntityUid Target; - - public CloningEvent(EntityUid source, EntityUid target) - { - Source = source; - Target = target; - } + None, + Clone, + RandomHumanoid, + RandomNonHumanoid } diff --git a/Content.Shared/Cloning/CloningSystem.Events.cs b/Content.Shared/Cloning/CloningSystem.Events.cs new file mode 100644 index 0000000000..a29310d45b --- /dev/null +++ b/Content.Shared/Cloning/CloningSystem.Events.cs @@ -0,0 +1,58 @@ +namespace Content.Shared.Cloning; + +/// +/// Raised after a new mob got spawned when cloning a humanoid +/// +[ByRefEvent] +public struct CloningEvent +{ + public bool NameHandled = false; + + public readonly EntityUid Source; + public readonly EntityUid Target; + + public CloningEvent(EntityUid source, EntityUid target) + { + Source = source; + Target = target; + } +} + +/// +/// Raised on a corpse being subjected to forced reincarnation(Metempsychosis). +/// Allowing for innate effects from the mob to influence the reincarnation. +/// +[ByRefEvent] +public struct ReincarnatingEvent +{ + public bool OverrideChance; + public bool NeverTrulyClone; + public ForcedMetempsychosisType ForcedType = ForcedMetempsychosisType.None; + public readonly EntityUid OldBody; + public float ReincarnationChanceModifier = 1; + public float ReincarnationChances; + public ReincarnatingEvent(EntityUid oldBody, float reincarnationChances) + { + OldBody = oldBody; + ReincarnationChances = reincarnationChances; + } +} + +/// +/// Raised on a corpse that someone is attempting to clone, but before the process actually begins. +/// Allows for Entities to influence whether the cloning can begin in the first place, either by canceling it, or modifying the cost. +/// +[ByRefEvent] +public struct AttemptCloningEvent +{ + public bool Cancelled; + public bool DoMetempsychosis; + public EntityUid CloningPod; + public string? CloningFailMessage; + public float CloningCostMultiplier = 1; + public AttemptCloningEvent(EntityUid cloningPod, bool doMetempsychosis) + { + DoMetempsychosis = doMetempsychosis; + CloningPod = cloningPod; + } +} diff --git a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml index 13e581bcb3..62f3bbcb3f 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Circuitboards/Machine/production.yml @@ -544,6 +544,27 @@ recipes: - CloningPodMachineCircuitboard +- type: entity + id: MetempsychoticMachineCircuitboard + parent: BaseMachineCircuitboard + name: metempsychotic machine machine board + description: A machine printed circuit board for a cloning pod + components: + - type: Sprite + state: medical + - type: MachineBoard + prototype: MetempsychoticMachine + requirements: + Capacitor: 2 + Manipulator: 2 + materialRequirements: + Glass: 1 + Cable: 1 + - type: ReverseEngineering + difficulty: 3 + recipes: + - MetempsychoticMachineCircuitboard + - type: entity id: MedicalScannerMachineCircuitboard parent: BaseMachineCircuitboard diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/metempsychoticMachine.yml b/Resources/Prototypes/Entities/Structures/Machines/metempsychoticMachine.yml similarity index 59% rename from Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/metempsychoticMachine.yml rename to Resources/Prototypes/Entities/Structures/Machines/metempsychoticMachine.yml index d8e791af1e..9667ed12d4 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/metempsychoticMachine.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/metempsychoticMachine.yml @@ -4,22 +4,23 @@ name: metempsychotic machine description: Speeds along the transmigration of a soul to its next vessel. components: - - type: MetempsychoticMachine - type: CloningPod + doMetempsychosis: true + biomassCostMultiplier: 0.5 - type: Machine board: MetempsychoticMachineCircuitboard - type: Sprite - sprite: Nyanotrasen/Structures/Machines/metempsychotic.rsi + sprite: Structures/Machines/metempsychotic.rsi snapCardinals: true layers: - - state: pod_0 + - state: cloning_idle - type: Appearance - type: GenericVisualizer visuals: enum.CloningPodVisuals.Status: base: - Cloning: { state: pod_1 } - NoMind: { state: pod_1 } - Gore: { state: pod_1 } - Idle: { state: pod_0 } + Cloning: { state: cloning_active } + NoMind: { state: cloning_active } + Gore: { state: cloning_failed } + Idle: { state: cloning_idle } - type: Psionic diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/CircuitBoards/production.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/CircuitBoards/production.yml index fc40ea1639..6e80ec7c4e 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/CircuitBoards/production.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/CircuitBoards/production.yml @@ -1,24 +1,3 @@ -- type: entity - id: MetempsychoticMachineCircuitboard - parent: BaseMachineCircuitboard - name: metempsychotic machine machine board - description: A machine printed circuit board for a cloning pod - components: - - type: Sprite - state: medical - - type: MachineBoard - prototype: MetempsychoticMachine - requirements: - Capacitor: 2 - Manipulator: 2 - materialRequirements: - Glass: 1 - Cable: 1 - - type: ReverseEngineering - difficulty: 3 - recipes: - - MetempsychoticMachineCircuitboard - - type: entity id: ReverseEngineeringMachineCircuitboard parent: BaseMachineCircuitboard diff --git a/Resources/Prototypes/Nyanotrasen/Recipes/Lathes/electronics.yml b/Resources/Prototypes/Nyanotrasen/Recipes/Lathes/electronics.yml index 418864cd40..1e53c715af 100644 --- a/Resources/Prototypes/Nyanotrasen/Recipes/Lathes/electronics.yml +++ b/Resources/Prototypes/Nyanotrasen/Recipes/Lathes/electronics.yml @@ -1,12 +1,3 @@ -- type: latheRecipe - id: MetempsychoticMachineCircuitboard - result: MetempsychoticMachineCircuitboard - completetime: 4 - materials: - Steel: 100 - Glass: 900 - Gold: 100 - - type: latheRecipe id: ReverseEngineeringMachineCircuitboard result: ReverseEngineeringMachineCircuitboard diff --git a/Resources/Prototypes/Nyanotrasen/Research/experimental.yml b/Resources/Prototypes/Nyanotrasen/Research/experimental.yml index 7c89c0f7d0..289efd317c 100644 --- a/Resources/Prototypes/Nyanotrasen/Research/experimental.yml +++ b/Resources/Prototypes/Nyanotrasen/Research/experimental.yml @@ -14,21 +14,6 @@ - ClothingHeadCage # - ShellSoulbreaker # DeltaV - Placing it under Exotic Ammunition because that's what it is. -- type: technology - id: Metempsychosis - name: research-technology-metempsychosis - icon: - sprite: Nyanotrasen/Structures/Machines/metempsychotic.rsi - state: pod_0 - discipline: Experimental - tier: 2 - cost: 15000 - recipeUnlocks: - - BiomassReclaimerMachineCircuitboard - - CloningConsoleComputerCircuitboard - - MedicalScannerMachineCircuitboard - - MetempsychoticMachineCircuitboard - # Tier 3 diff --git a/Resources/Prototypes/Nyanotrasen/metempsychoticNonHumanoids.yml b/Resources/Prototypes/Nyanotrasen/metempsychoticNonHumanoids.yml index dcbe23f608..feabd9977b 100644 --- a/Resources/Prototypes/Nyanotrasen/metempsychoticNonHumanoids.yml +++ b/Resources/Prototypes/Nyanotrasen/metempsychoticNonHumanoids.yml @@ -3,7 +3,7 @@ weights: MobMonkey: 1 MobGorilla: 1 - # MobKangaroo: 0.5 # Mobs here need to be either VERY funny or up to standard. + MobKangaroo: 0.5 MobXenoQueen: 0.01 MobCrab: 0.01 - MobPenguin: 1 #ODJ's orders + MobPenguin: 1 diff --git a/Resources/Prototypes/Recipes/Lathes/electronics.yml b/Resources/Prototypes/Recipes/Lathes/electronics.yml index 60c0fc1524..12b5bedf10 100644 --- a/Resources/Prototypes/Recipes/Lathes/electronics.yml +++ b/Resources/Prototypes/Recipes/Lathes/electronics.yml @@ -117,6 +117,15 @@ Glass: 900 Gold: 100 +- type: latheRecipe + id: MetempsychoticMachineCircuitboard + result: MetempsychoticMachineCircuitboard + completetime: 4 + materials: + Steel: 100 + Glass: 900 + Gold: 100 + - type: latheRecipe id: ThermomachineFreezerMachineCircuitBoard result: ThermomachineFreezerMachineCircuitBoard @@ -921,7 +930,7 @@ materials: Steel: 100 Glass: 900 - + - type: latheRecipe id: ShuttleGunPerforatorCircuitboard result: ShuttleGunPerforatorCircuitboard @@ -930,7 +939,7 @@ Steel: 100 Glass: 900 Gold: 100 - + - type: latheRecipe id: ShuttleGunKineticCircuitboard result: ShuttleGunKineticCircuitboard @@ -938,7 +947,7 @@ materials: Steel: 100 Glass: 900 - + - type: latheRecipe id: ShuttleGunFriendshipCircuitboard result: ShuttleGunFriendshipCircuitboard @@ -947,7 +956,7 @@ Steel: 100 Glass: 900 Gold: 50 - + - type: latheRecipe id: ShuttleGunDusterCircuitboard result: ShuttleGunDusterCircuitboard diff --git a/Resources/Prototypes/Research/experimental.yml b/Resources/Prototypes/Research/experimental.yml index 65186340d7..0dbcded546 100644 --- a/Resources/Prototypes/Research/experimental.yml +++ b/Resources/Prototypes/Research/experimental.yml @@ -137,6 +137,21 @@ - WeaponParticleDecelerator - HoloprojectorField +- type: technology + id: Metempsychosis + name: research-technology-metempsychosis + icon: + sprite: Structures/Machines/metempsychotic.rsi + state: cloning_idle + discipline: Experimental + tier: 2 + cost: 15000 + recipeUnlocks: + - BiomassReclaimerMachineCircuitboard + - CloningConsoleComputerCircuitboard + - MedicalScannerMachineCircuitboard + - MetempsychoticMachineCircuitboard + # Tier 3 #- type: technology # DeltaV - LRP diff --git a/Resources/Textures/Nyanotrasen/Structures/Machines/metempsychotic.rsi/pod_1.png b/Resources/Textures/Structures/Machines/metempsychotic.rsi/cloning_active.png similarity index 100% rename from Resources/Textures/Nyanotrasen/Structures/Machines/metempsychotic.rsi/pod_1.png rename to Resources/Textures/Structures/Machines/metempsychotic.rsi/cloning_active.png diff --git a/Resources/Textures/Structures/Machines/metempsychotic.rsi/cloning_failed.png b/Resources/Textures/Structures/Machines/metempsychotic.rsi/cloning_failed.png new file mode 100644 index 0000000000000000000000000000000000000000..e85f4b2cba5881f708f0e0824e88feb528139d99 GIT binary patch literal 1469 zcmchW`7_%I7{0D=AHM4cjldWay@Y_a!@rW0044su1?-! zI{Yw1N}SbQ*`LLbNb+`Z1eg;VAH)ObGTH+T0BuFE?-#-19m*5y>l_#z6PA>cm|H}l z{~k+9Uju*&0GI%P*8tEoINk>UCCtYOxrHPESfAz1mV|d<&3e48o6$xcXOYz=stqXp zr+K6WX3bJ})np8=>^OpEAU|H1JzaZewtsvJKWDmlt*ZU z1NJw0mgI&@#k;JP2A`|KBXYcG*Xb-N4F?~E0g#7l zC`ZQCBStM8qE=V(s^BocvZ;DgBqD3U!zN?Wc-r6N|d(O4}l{1c^2l zi`CnUrA4qd^>u1t5M9b$9JQ}lH`^X!GV)R7B+0$Cxml3R+Ar4EQVPZWy|W)_qvQ`r zmUBVneVC=ZnbuRPaP6(B;<0eaEx%N0w?(KYAY^Chix^QkXDhJMh@14t(+7%0F#nnM zIDCkLld^KNyLy4nIBW1I#4?GP4BN!ih4%C#EbgEo$_UH~Eo5rB&THxj`hdGt_#BGi zH8wIBf#*+7pZ2iA4m}6Rubb{&wx*b3`8C4sEXEs5dNPGz{$k%yN9RrFo7Dhk`O{$8 zv9H;zAwAxF+^pt%X==}>bn~#DJvn3a+K?t>5Z7Z5>E#VrubfN;i zq(H-Sh1ceJbGR982t%;+$C}X1eB8jtUGWx~NGQ)|Q^BXSl;~3jmCAGaRzCw{e{}iZ=g)oaK{;fWxoC0iIhSc z&vkyNscyug+Re2kd>_fOHtKQw6~pQVtuo+O}L7(bBHq;6SFm(T}l`-XSNh OE#T&ib7DG%lK%s28`sbP literal 0 HcmV?d00001 diff --git a/Resources/Textures/Nyanotrasen/Structures/Machines/metempsychotic.rsi/pod_0.png b/Resources/Textures/Structures/Machines/metempsychotic.rsi/cloning_idle.png similarity index 100% rename from Resources/Textures/Nyanotrasen/Structures/Machines/metempsychotic.rsi/pod_0.png rename to Resources/Textures/Structures/Machines/metempsychotic.rsi/cloning_idle.png diff --git a/Resources/Textures/Nyanotrasen/Structures/Machines/metempsychotic.rsi/meta.json b/Resources/Textures/Structures/Machines/metempsychotic.rsi/meta.json similarity index 54% rename from Resources/Textures/Nyanotrasen/Structures/Machines/metempsychotic.rsi/meta.json rename to Resources/Textures/Structures/Machines/metempsychotic.rsi/meta.json index 7276fde67e..da5034f713 100644 --- a/Resources/Textures/Nyanotrasen/Structures/Machines/metempsychotic.rsi/meta.json +++ b/Resources/Textures/Structures/Machines/metempsychotic.rsi/meta.json @@ -1,18 +1,22 @@ { "version": 1, "license": "CC-BY-4.0", - "copyright": "Created by discord user Four Hydra Heads#2075 (971500282364178512)", + "copyright": "Created by discord user Four Hydra Heads#2075 (971500282364178512), failed state edited by VMSolidus", "size": { "x": 32, "y": 32 }, "states": [ { - "name": "pod_0" + "name": "cloning_idle" }, { - "name": "pod_1", + "name": "cloning_active", "delays": [ [ 0.1, 0.1, 0.1, 0.1, 0.1, 0.1 ] ] + }, + { + "name": "cloning_failed", + "delays": [ [ 0.1, 0.1, 0.1, 0.1 ] ] } ] } From ade6f99b13c9dff310ce42a45fe8182bddd6c577 Mon Sep 17 00:00:00 2001 From: SimpleStation Changelogs Date: Mon, 2 Sep 2024 15:09:18 +0000 Subject: [PATCH 4/6] Automatic Changelog Update (#735) --- Resources/Changelog/Changelog.yml | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 9fc7345867..7814a96918 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -5788,3 +5788,44 @@ Entries: message: Some Reagent Slimes can now contain Lotophagoi Oil. id: 6310 time: '2024-09-02T15:08:11.0000000+00:00' +- author: VMSolidus + changes: + - type: Add + message: Cloning & Metempsychosis Machines have been refactored! + - type: Add + message: >- + Cloning can now fail at any point during the cloning process, turning + the would-be clone into a soup of blood and ammonia. + - type: Add + message: >- + "Clone Soup" scales directly with the mass of the entity you're + attempting to clone. Fail to clone a Lamia, and you'll be greeted with + an Olympic swimming pool worth of blood when the machine opens. + - type: Add + message: >- + Cloning will fail if at any point during the procedure, the machine is + depowered, unanchored, or emagged. + - type: Add + message: >- + Clones come out of the machine with severe Cellular damage. Consider + using Doxarubixadone in a Cryo tube as an affordable means of + "Finishing" clones. + - type: Tweak + message: >- + Cloning Time is now increased proportionally if an entity being cloned + is larger than a standard human(smaller entities are unchanged) + - type: Tweak + message: >- + The cost to clone an entity can now be configured on a per-server basis + via CCVar "cloning.biomass_cost_multiplier" + - type: Tweak + message: >- + The Biomass Reclaimer can now be toggled to round-remove ensouled bodies + or not via CCVar "cloning.reclaim_souled_bodies" + - type: Add + message: >- + The effects of Metempsychosis now scale with a Psion's relevant caster + stats. More powerful psychics are more likely to get favorable results + from being forcibly reincarnated. + id: 6311 + time: '2024-09-02T15:08:52.0000000+00:00' From 7085fc9a20334305be48a0f2f2c4b54469c13dc5 Mon Sep 17 00:00:00 2001 From: VMSolidus Date: Mon, 2 Sep 2024 14:10:49 -0400 Subject: [PATCH 5/6] Delete All Then Ghost Fault Tolerance (#787) # Description ![image](https://github.com/user-attachments/assets/4135c3fa-d2e0-41ca-b8f4-49e149d43ef3) I'm putting this here as an option to deal with our Heisentest problems, by making the tests "Fault-Tolerant" wherever practical, but I don't want this merged without Death and Psprite agreeing to this. For the most part I believe that these tests are failing because they are essentially checking that "Random events are not creating entities", by creating their own enforced Race Conditions. This particular test is repeatedly failing because the Mood System has no way of deducing that it's in a test. Even though the alleged issue is a nothingburger. ![image](https://github.com/user-attachments/assets/777b31f1-87a7-4eee-8a62-993acb322315) Tests absolutely shouldn't have been designed around race conditions. # Changelog No changelog because this isn't playerfacing. --- .../Tests/Minds/MindTest.DeleteAllThenGhost.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs index 7bc62dfe2b..ab9e96ab91 100644 --- a/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs +++ b/Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs @@ -34,7 +34,7 @@ public async Task DeleteAllThenGhost() Console.WriteLine(pair.Client.EntMan.ToPrettyString(ent)); } - Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0)); + Assert.That(pair.Client.EntMan.EntityCount, Is.AtMost(1)); // Tolerate at most one client entity // Create a new map. int mapId = 1; From 7c838786b4290de4ae3786b7dedaffd52f556444 Mon Sep 17 00:00:00 2001 From: stellar-novas Date: Mon, 2 Sep 2024 14:11:38 -0400 Subject: [PATCH 6/6] Update Issue Templates (#797) # Description Rewrites the issue templates and removes the redundant security vulnerability issue Also, sorry Death, titles are now sentence case to maintain consistency with the auto generated security policy one Blame GitHub not me tags or you won't see it -->

Media

Here's the old one, can't make issues on a fork so I can't show how it looks now. ![image.png](https://github.com/user-attachments/assets/ae8319ed-b66c-4415-9f6d-1048ddc53e07)

--- # Changelog N/A Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/config.yml | 4 ---- .github/ISSUE_TEMPLATE/feature_request.md | 6 +++--- .github/ISSUE_TEMPLATE/issue_report.md | 6 +++--- 3 files changed, 6 insertions(+), 10 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml deleted file mode 100644 index 7ff9017cb0..0000000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,4 +0,0 @@ -contact_links: - - name: Report a Security Vulnerability - url: https://github.com/space-wizards/space-station-14/blob/master/SECURITY.md - about: Please report security vulnerabilities to the Space Wizards privately so they can fix them before they are publicly disclosed. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index aba549332f..51f9de8baf 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,8 +1,8 @@ --- -name: Request a Feature -about: "Template for noting future planned features. Please ask for approval in the Discord if you aren't an organization Member before posting a feature request" +name: Request a feature +about: "Please outline your request in Discord first if you aren't a maintainer." title: '' -labels: '' +labels: ["Type: Feature"] assignees: '' --- diff --git a/.github/ISSUE_TEMPLATE/issue_report.md b/.github/ISSUE_TEMPLATE/issue_report.md index ab82181197..c74f24554a 100644 --- a/.github/ISSUE_TEMPLATE/issue_report.md +++ b/.github/ISSUE_TEMPLATE/issue_report.md @@ -1,8 +1,8 @@ --- -name: Report an Issue -about: "Any general issues you have during play or with the codebase" +name: Report an issue +about: "Any issues found in gameplay or the codebase" title: '' -labels: '' +labels: 'Type: Bug' assignees: '' ---