diff --git a/Content.IntegrationTests/Tests/Nyanotrasen/Metempsychosis/MetempsychosisTest.cs b/Content.IntegrationTests/Tests/Nyanotrasen/Metempsychosis/MetempsychosisTest.cs deleted file mode 100644 index cd6a4b4c2b9..00000000000 --- 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 c95c37312e5..524cbe80e48 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 00000000000..408e1cf24a3 --- /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 5d311f3ce10..7931fae4778 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 11e0e36166a..00000000000 --- 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 246495cee00..5f7b7af1cda 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 d07858aec5c..eaf04d64b2b 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 91184ddc162..a6ce43c4081 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 0adcc9b5b25..00000000000 --- 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 62dc1b078e0..00000000000 --- 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 00000000000..6f2af106472 --- /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 19d0ca6461c..e418e358df2 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -1678,16 +1678,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); + + /// + /// Controls whether or not Metempsychosis preserves Age. + /// + 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); /// - /// 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 Names. EG: Are you actually a new person? /// - public static readonly CVarDef BiomassEasyMode = - CVarDef.Create("biomass.easy_mode", false, CVar.SERVERONLY); + 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 @@ -2291,7 +2338,7 @@ public static readonly CVarDef /// public static readonly CVarDef StationGoalsChance = CVarDef.Create("game.station_goals_chance", 0.1f, CVar.SERVERONLY); - + #region CPR System /// @@ -2338,7 +2385,7 @@ public static readonly CVarDef /// public static readonly CVarDef CPRAirlossReductionMultiplier = CVarDef.Create("cpr.airloss_reduction_multiplier", 1f, CVar.REPLICATED | CVar.SERVER); - + #endregion #region Contests System diff --git a/Content.Shared/Cloning/CloningPodComponent.cs b/Content.Shared/Cloning/CloningPodComponent.cs index 88d587c1457..082b92e8b14 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 00000000000..a29310d45b9 --- /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 f60ffad8478..b7a7e6477fc 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 d8e791af1ed..9667ed12d46 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 fc40ea16397..6e80ec7c4e9 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 418864cd408..1e53c715af5 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 7c89c0f7d04..289efd317c5 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 dcbe23f6082..feabd9977bc 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 050dfa05cf8..0c0226e0b9d 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 65186340d7f..0dbcded5460 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 00000000000..e85f4b2cba5 Binary files /dev/null and b/Resources/Textures/Structures/Machines/metempsychotic.rsi/cloning_failed.png differ 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 7276fde67e5..da5034f7139 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 ] ] } ] }