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.Server.Language; using Content.Shared.Abilities.Psionics; using Content.Shared.Language.Components; 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) { if (clonePod.ConnectedConsole is not null) _chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, 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, Math.Max(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, loadExtensions: true, generateLoadouts: false); return; } _humanoidSystem.LoadProfile(mob, pref, loadExtensions: true, generateLoadouts: false); } /// /// 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); } }