Cloning Refactor and bugfixes (#35555)
* cloning refactor * cleanup and fixes * don't pick from 0 * give dwarves the correct species * fix dna and bloodstream reagent data cloning * don't copy helmets * be less redundant
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
using Content.Server.Body.Components;
|
using Content.Server.Body.Components;
|
||||||
using Content.Server.EntityEffects.Effects;
|
using Content.Server.EntityEffects.Effects;
|
||||||
using Content.Server.Fluids.EntitySystems;
|
using Content.Server.Fluids.EntitySystems;
|
||||||
using Content.Server.Forensics;
|
|
||||||
using Content.Server.Popups;
|
using Content.Server.Popups;
|
||||||
using Content.Shared.Alert;
|
using Content.Shared.Alert;
|
||||||
using Content.Shared.Chemistry.Components;
|
using Content.Shared.Chemistry.Components;
|
||||||
@@ -40,7 +39,6 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||||
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
|
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
|
||||||
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
|
||||||
[Dependency] private readonly ForensicsSystem _forensicsSystem = default!;
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
@@ -193,17 +191,8 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
|
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
|
||||||
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
|
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
|
||||||
|
|
||||||
// Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
|
|
||||||
|
|
||||||
if (TryComp<DnaComponent>(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
|
|
||||||
{
|
|
||||||
donorComp.DNA = _forensicsSystem.GenerateDNA();
|
|
||||||
|
|
||||||
var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
|
|
||||||
RaiseLocalEvent(entity.Owner, ref ev);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill blood solution with BLOOD
|
// Fill blood solution with BLOOD
|
||||||
|
// The DNA string might not be initialized yet, but the reagent data gets updated in the GenerateDnaEvent subscription
|
||||||
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,6 +481,8 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
reagentData.AddRange(GetEntityBloodData(entity.Owner));
|
reagentData.AddRange(GetEntityBloodData(entity.Owner));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
Log.Error("Unable to set bloodstream DNA, solution entity could not be resolved");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -502,13 +493,10 @@ public sealed class BloodstreamSystem : EntitySystem
|
|||||||
var bloodData = new List<ReagentData>();
|
var bloodData = new List<ReagentData>();
|
||||||
var dnaData = new DnaData();
|
var dnaData = new DnaData();
|
||||||
|
|
||||||
if (TryComp<DnaComponent>(uid, out var donorComp))
|
if (TryComp<DnaComponent>(uid, out var donorComp) && donorComp.DNA != null)
|
||||||
{
|
|
||||||
dnaData.DNA = donorComp.DNA;
|
dnaData.DNA = donorComp.DNA;
|
||||||
} else
|
else
|
||||||
{
|
|
||||||
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
|
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
|
||||||
}
|
|
||||||
|
|
||||||
bloodData.Add(dnaData);
|
bloodData.Add(dnaData);
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ namespace Content.Server.Cloning
|
|||||||
{
|
{
|
||||||
private readonly EntityUid _mindId;
|
private readonly EntityUid _mindId;
|
||||||
private readonly MindComponent _mind;
|
private readonly MindComponent _mind;
|
||||||
private readonly CloningSystem _cloningSystem;
|
private readonly CloningPodSystem _cloningPodSystem;
|
||||||
|
|
||||||
public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningSystem cloningSys)
|
public AcceptCloningEui(EntityUid mindId, MindComponent mind, CloningPodSystem cloningPodSys)
|
||||||
{
|
{
|
||||||
_mindId = mindId;
|
_mindId = mindId;
|
||||||
_mind = mind;
|
_mind = mind;
|
||||||
_cloningSystem = cloningSys;
|
_cloningPodSystem = cloningPodSys;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void HandleMessage(EuiMessageBase msg)
|
public override void HandleMessage(EuiMessageBase msg)
|
||||||
@@ -29,7 +29,7 @@ namespace Content.Server.Cloning
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_cloningSystem.TransferMindToClone(_mindId, _mind);
|
_cloningPodSystem.TransferMindToClone(_mindId, _mind);
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using Content.Server.Administration.Logs;
|
|||||||
using Content.Server.Cloning.Components;
|
using Content.Server.Cloning.Components;
|
||||||
using Content.Server.DeviceLinking.Systems;
|
using Content.Server.DeviceLinking.Systems;
|
||||||
using Content.Server.Medical.Components;
|
using Content.Server.Medical.Components;
|
||||||
using Content.Server.Power.Components;
|
|
||||||
using Content.Server.Power.EntitySystems;
|
using Content.Server.Power.EntitySystems;
|
||||||
using Content.Shared.UserInterface;
|
using Content.Shared.UserInterface;
|
||||||
using Content.Shared.Cloning;
|
using Content.Shared.Cloning;
|
||||||
@@ -16,19 +15,17 @@ using Content.Shared.Mind;
|
|||||||
using Content.Shared.Mobs.Components;
|
using Content.Shared.Mobs.Components;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.Mobs.Systems;
|
||||||
using Content.Shared.Power;
|
using Content.Shared.Power;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using Robust.Server.GameObjects;
|
using Robust.Server.GameObjects;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
|
|
||||||
namespace Content.Server.Cloning
|
namespace Content.Server.Cloning
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
|
||||||
public sealed class CloningConsoleSystem : EntitySystem
|
public sealed class CloningConsoleSystem : EntitySystem
|
||||||
{
|
{
|
||||||
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
[Dependency] private readonly CloningSystem _cloningSystem = default!;
|
[Dependency] private readonly CloningPodSystem _cloningPodSystem = default!;
|
||||||
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||||
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
||||||
@@ -171,7 +168,7 @@ namespace Content.Server.Cloning
|
|||||||
if (mind.UserId.HasValue == false || mind.Session == null)
|
if (mind.UserId.HasValue == false || mind.Session == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (_cloningSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
|
if (_cloningPodSystem.TryCloning(cloningPodUid, body.Value, (mindId, mind), cloningPod, scannerComp.CloningFailChanceMultiplier))
|
||||||
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
|
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} successfully cloned {ToPrettyString(body.Value)}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
323
Content.Server/Cloning/CloningPodSystem.cs
Normal file
323
Content.Server/Cloning/CloningPodSystem.cs
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
using Content.Server.Atmos.EntitySystems;
|
||||||
|
using Content.Server.Chat.Systems;
|
||||||
|
using Content.Server.Cloning.Components;
|
||||||
|
using Content.Server.DeviceLinking.Systems;
|
||||||
|
using Content.Server.EUI;
|
||||||
|
using Content.Server.Fluids.EntitySystems;
|
||||||
|
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;
|
||||||
|
using Content.Shared.Mind;
|
||||||
|
using Content.Shared.Mind.Components;
|
||||||
|
using Content.Shared.Mobs.Systems;
|
||||||
|
using Robust.Server.Containers;
|
||||||
|
using Robust.Server.Player;
|
||||||
|
using Robust.Shared.Audio.Systems;
|
||||||
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.Containers;
|
||||||
|
using Robust.Shared.Physics.Components;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Cloning;
|
||||||
|
|
||||||
|
public sealed class CloningPodSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
||||||
|
[Dependency] private readonly EuiManager _euiManager = null!;
|
||||||
|
[Dependency] private readonly CloningConsoleSystem _cloningConsoleSystem = 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 SharedTransformSystem _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 CloningSystem _cloning = default!;
|
||||||
|
[Dependency] private readonly EmagSystem _emag = default!;
|
||||||
|
|
||||||
|
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
|
||||||
|
public readonly ProtoId<CloningSettingsPrototype> SettingsId = "CloningPod";
|
||||||
|
public const float EasyModeCloningCost = 0.7f;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||||
|
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
|
||||||
|
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnComponentInit(Entity<CloningPodComponent> ent, ref ComponentInit args)
|
||||||
|
{
|
||||||
|
ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(ent.Owner, "clonepod-bodyContainer");
|
||||||
|
_signalSystem.EnsureSinkPorts(ent.Owner, ent.Comp.PodPort);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void TransferMindToClone(EntityUid mindId, MindComponent mind)
|
||||||
|
{
|
||||||
|
if (!ClonesWaitingForMind.TryGetValue(mind, out var entity) ||
|
||||||
|
!EntityManager.EntityExists(entity) ||
|
||||||
|
!TryComp<MindContainerComponent>(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<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
|
||||||
|
uid != cloningPodComponent.BodyContainer.ContainedEntity)
|
||||||
|
{
|
||||||
|
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
|
||||||
|
}
|
||||||
|
private void OnPortDisconnected(Entity<CloningPodComponent> ent, ref PortDisconnectedEvent args)
|
||||||
|
{
|
||||||
|
ent.Comp.ConnectedConsole = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAnchor(Entity<CloningPodComponent> ent, ref AnchorStateChangedEvent args)
|
||||||
|
{
|
||||||
|
if (ent.Comp.ConnectedConsole == null || !TryComp<CloningConsoleComponent>(ent.Comp.ConnectedConsole, out var console))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.Anchored)
|
||||||
|
{
|
||||||
|
_cloningConsoleSystem.RecheckConnections(ent.Comp.ConnectedConsole.Value, ent.Owner, console.GeneticScanner, console);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_cloningConsoleSystem.UpdateUserInterface(ent.Comp.ConnectedConsole.Value, console);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExamined(Entity<CloningPodComponent> ent, ref ExaminedEvent args)
|
||||||
|
{
|
||||||
|
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(ent.Owner))
|
||||||
|
return;
|
||||||
|
|
||||||
|
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(ent.Owner, ent.Comp.RequiredMaterial))));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
|
||||||
|
{
|
||||||
|
if (!Resolve(uid, ref clonePod))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (HasComp<ActiveCloningPodComponent>(uid))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var mind = mindEnt.Comp;
|
||||||
|
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
|
||||||
|
{
|
||||||
|
if (EntityManager.EntityExists(clone) &&
|
||||||
|
!_mobStateSystem.IsDead(clone) &&
|
||||||
|
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
|
||||||
|
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
|
||||||
|
return false; // Mind already has clone
|
||||||
|
|
||||||
|
ClonesWaitingForMind.Remove(mind);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
|
||||||
|
return false; // Body controlled by mind is not dead
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var cloningCost = (int)Math.Round(physics.FixturesMass);
|
||||||
|
|
||||||
|
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
|
||||||
|
cloningCost = (int)Math.Round(cloningCost * EasyModeCloningCost);
|
||||||
|
|
||||||
|
// biomass checks
|
||||||
|
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// end of biomass checks
|
||||||
|
|
||||||
|
// genetic damage checks
|
||||||
|
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
|
||||||
|
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
|
||||||
|
{
|
||||||
|
var chance = Math.Clamp((float)(cellularDmg / 100), 0, 1);
|
||||||
|
chance *= failChanceModifier;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (_robustRandom.Prob(chance))
|
||||||
|
{
|
||||||
|
clonePod.FailedClone = true;
|
||||||
|
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
|
||||||
|
AddComp<ActiveCloningPodComponent>(uid);
|
||||||
|
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
|
||||||
|
clonePod.UsedBiomass = cloningCost;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end of genetic damage checks
|
||||||
|
|
||||||
|
if (!_cloning.TryCloning(bodyToClone, _transformSystem.GetMapCoordinates(bodyToClone), SettingsId, out var mob)) // spawn a new body
|
||||||
|
{
|
||||||
|
if (clonePod.ConnectedConsole != null)
|
||||||
|
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-uncloneable-trait-error"), InGameICChatType.Speak, false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(mob.Value);
|
||||||
|
cloneMindReturn.Mind = mind;
|
||||||
|
cloneMindReturn.Parent = uid;
|
||||||
|
_containerSystem.Insert(mob.Value, clonePod.BodyContainer);
|
||||||
|
ClonesWaitingForMind.Add(mind, mob.Value);
|
||||||
|
_euiManager.OpenEui(new AcceptCloningEui(mindEnt, mind, this), client);
|
||||||
|
|
||||||
|
UpdateStatus(uid, CloningPodStatus.NoMind, clonePod);
|
||||||
|
AddComp<ActiveCloningPodComponent>(uid);
|
||||||
|
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
|
||||||
|
clonePod.UsedBiomass = cloningCost;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
|
||||||
|
{
|
||||||
|
cloningPod.Status = status;
|
||||||
|
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Update(float frameTime)
|
||||||
|
{
|
||||||
|
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
|
||||||
|
while (query.MoveNext(out var uid, out var _, out var cloning))
|
||||||
|
{
|
||||||
|
if (!_powerReceiverSystem.IsPowered(uid))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
cloning.CloningProgress += frameTime;
|
||||||
|
if (cloning.CloningProgress < cloning.CloningTime)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (cloning.FailedClone)
|
||||||
|
EndFailedCloning(uid, cloning);
|
||||||
|
else
|
||||||
|
Eject(uid, cloning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
|
||||||
|
/// </summary>
|
||||||
|
private void OnEmagged(Entity<CloningPodComponent> ent, ref GotEmaggedEvent args)
|
||||||
|
{
|
||||||
|
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_emag.CheckFlag(ent.Owner, EmagType.Interaction))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!this.IsPowered(ent.Owner, EntityManager))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), ent.Owner);
|
||||||
|
args.Handled = 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<BeingClonedComponent>(entity);
|
||||||
|
_containerSystem.Remove(entity, clonePod.BodyContainer);
|
||||||
|
clonePod.CloningProgress = 0f;
|
||||||
|
clonePod.UsedBiomass = 0;
|
||||||
|
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
||||||
|
RemCompDeferred<ActiveCloningPodComponent>(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<EmaggedComponent>(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<EmaggedComponent>(uid))
|
||||||
|
{
|
||||||
|
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int)(clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
|
||||||
|
}
|
||||||
|
|
||||||
|
clonePod.UsedBiomass = 0;
|
||||||
|
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset(RoundRestartCleanupEvent ev)
|
||||||
|
{
|
||||||
|
ClonesWaitingForMind.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,350 +1,123 @@
|
|||||||
using Content.Server.Atmos.EntitySystems;
|
|
||||||
using Content.Server.Chat.Systems;
|
|
||||||
using Content.Server.Cloning.Components;
|
|
||||||
using Content.Server.DeviceLinking.Systems;
|
|
||||||
using Content.Server.EUI;
|
|
||||||
using Content.Server.Fluids.EntitySystems;
|
|
||||||
using Content.Server.Humanoid;
|
using Content.Server.Humanoid;
|
||||||
using Content.Server.Jobs;
|
using Content.Shared.Administration.Logs;
|
||||||
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.Cloning;
|
||||||
using Content.Shared.Damage;
|
using Content.Shared.Cloning.Events;
|
||||||
using Content.Shared.DeviceLinking.Events;
|
using Content.Shared.Database;
|
||||||
using Content.Shared.Emag.Components;
|
|
||||||
using Content.Shared.Emag.Systems;
|
|
||||||
using Content.Shared.Examine;
|
|
||||||
using Content.Shared.GameTicking;
|
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Inventory;
|
||||||
using Content.Shared.Mind.Components;
|
using Content.Shared.NameModifier.Components;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.StatusEffect;
|
||||||
using Content.Shared.Roles.Jobs;
|
using Content.Shared.Whitelist;
|
||||||
using Robust.Server.Containers;
|
using Robust.Shared.Map;
|
||||||
using Robust.Server.GameObjects;
|
|
||||||
using Robust.Server.Player;
|
|
||||||
using Robust.Shared.Audio.Systems;
|
|
||||||
using Robust.Shared.Configuration;
|
|
||||||
using Robust.Shared.Containers;
|
|
||||||
using Robust.Shared.Physics.Components;
|
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Content.Server.Cloning
|
namespace Content.Server.Cloning;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System responsible for making a copy of a humanoid's body.
|
||||||
|
/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CloningSystem : EntitySystem
|
||||||
{
|
{
|
||||||
public sealed class CloningSystem : EntitySystem
|
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||||
{
|
|
||||||
[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 HumanoidAppearanceSystem _humanoidSystem = default!;
|
||||||
[Dependency] private readonly ContainerSystem _containerSystem = default!;
|
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||||
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
|
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
[Dependency] private readonly ISharedAdminLogManager _adminLogger = 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 EmagSystem _emag = default!;
|
|
||||||
|
|
||||||
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
|
/// <summary>
|
||||||
public const float EasyModeCloningCost = 0.7f;
|
/// Spawns a clone of the given humanoid mob at the specified location or in nullspace.
|
||||||
|
/// </summary>
|
||||||
public override void Initialize()
|
public bool TryCloning(EntityUid original, MapCoordinates? coords, ProtoId<CloningSettingsPrototype> settingsId, [NotNullWhen(true)] out EntityUid? clone)
|
||||||
{
|
{
|
||||||
base.Initialize();
|
clone = null;
|
||||||
|
if (!_prototype.TryIndex(settingsId, out var settings))
|
||||||
|
return false; // invalid settings
|
||||||
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, ComponentInit>(OnComponentInit);
|
if (!TryComp<HumanoidAppearanceComponent>(original, out var humanoid))
|
||||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
|
||||||
SubscribeLocalEvent<BeingClonedComponent, MindAddedMessage>(HandleMindAdded);
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, PortDisconnectedEvent>(OnPortDisconnected);
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, AnchorStateChangedEvent>(OnAnchor);
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, ExaminedEvent>(OnExamined);
|
|
||||||
SubscribeLocalEvent<CloningPodComponent, GotEmaggedEvent>(OnEmagged);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnComponentInit(EntityUid uid, CloningPodComponent clonePod, ComponentInit args)
|
|
||||||
{
|
|
||||||
clonePod.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(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<MindContainerComponent>(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<CloningPodComponent>(clonedComponent.Parent, out var cloningPodComponent) ||
|
|
||||||
uid != cloningPodComponent.BodyContainer.ContainedEntity)
|
|
||||||
{
|
|
||||||
EntityManager.RemoveComponent<BeingClonedComponent>(uid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
UpdateStatus(clonedComponent.Parent, CloningPodStatus.Cloning, cloningPodComponent);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<CloningConsoleComponent>(component.ConnectedConsole, out var console))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (args.Anchored)
|
|
||||||
{
|
|
||||||
_cloningConsoleSystem.RecheckConnections(component.ConnectedConsole.Value, uid, console.GeneticScanner, console);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_cloningConsoleSystem.UpdateUserInterface(component.ConnectedConsole.Value, console);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnExamined(EntityUid uid, CloningPodComponent component, ExaminedEvent args)
|
|
||||||
{
|
|
||||||
if (!args.IsInDetailsRange || !_powerReceiverSystem.IsPowered(uid))
|
|
||||||
return;
|
|
||||||
|
|
||||||
args.PushMarkup(Loc.GetString("cloning-pod-biomass", ("number", _material.GetMaterialAmount(uid, component.RequiredMaterial))));
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryCloning(EntityUid uid, EntityUid bodyToClone, Entity<MindComponent> mindEnt, CloningPodComponent? clonePod, float failChanceModifier = 1)
|
|
||||||
{
|
|
||||||
if (!Resolve(uid, ref clonePod))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (HasComp<ActiveCloningPodComponent>(uid))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var mind = mindEnt.Comp;
|
|
||||||
if (ClonesWaitingForMind.TryGetValue(mind, out var clone))
|
|
||||||
{
|
|
||||||
if (EntityManager.EntityExists(clone) &&
|
|
||||||
!_mobStateSystem.IsDead(clone) &&
|
|
||||||
TryComp<MindContainerComponent>(clone, out var cloneMindComp) &&
|
|
||||||
(cloneMindComp.Mind == null || cloneMindComp.Mind == mindEnt))
|
|
||||||
return false; // Mind already has clone
|
|
||||||
|
|
||||||
ClonesWaitingForMind.Remove(mind);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mind.OwnedEntity != null && !_mobStateSystem.IsDead(mind.OwnedEntity.Value))
|
|
||||||
return false; // Body controlled by mind is not dead
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
|
|
||||||
if (!TryComp<HumanoidAppearanceComponent>(bodyToClone, out var humanoid))
|
|
||||||
return false; // whatever body was to be cloned, was not a humanoid
|
return false; // whatever body was to be cloned, was not a humanoid
|
||||||
|
|
||||||
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
|
if (!_prototype.TryIndex(humanoid.Species, out var speciesPrototype))
|
||||||
return false;
|
return false; // invalid species
|
||||||
|
|
||||||
if (!TryComp<PhysicsComponent>(bodyToClone, out var physics))
|
var attemptEv = new CloningAttemptEvent(settings);
|
||||||
return false;
|
RaiseLocalEvent(original, ref attemptEv);
|
||||||
|
if (attemptEv.Cancelled && !settings.ForceCloning)
|
||||||
|
return false; // cannot clone, for example due to the unrevivable trait
|
||||||
|
|
||||||
var cloningCost = (int) Math.Round(physics.FixturesMass);
|
clone = coords == null ? Spawn(speciesPrototype.Prototype) : Spawn(speciesPrototype.Prototype, coords.Value);
|
||||||
|
_humanoidSystem.CloneAppearance(original, clone.Value);
|
||||||
|
|
||||||
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
|
var componentsToCopy = settings.Components;
|
||||||
cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
|
|
||||||
|
|
||||||
// biomass checks
|
// don't make status effects permanent
|
||||||
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);
|
if (TryComp<StatusEffectsComponent>(original, out var statusComp))
|
||||||
|
componentsToCopy.ExceptWith(statusComp.ActiveEffects.Values.Select(s => s.RelevantComponent).Where(s => s != null)!);
|
||||||
|
|
||||||
if (biomassAmount < cloningCost)
|
foreach (var componentName in componentsToCopy)
|
||||||
{
|
{
|
||||||
if (clonePod.ConnectedConsole != null)
|
if (!_componentFactory.TryGetRegistration(componentName, out var componentRegistration))
|
||||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-chat-error", ("units", cloningCost)), InGameICChatType.Speak, false);
|
{
|
||||||
return false;
|
Log.Error($"Tried to use invalid component registration for cloning: {componentName}");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_material.TryChangeMaterialAmount(uid, clonePod.RequiredMaterial, -cloningCost);
|
if (EntityManager.TryGetComponent(original, componentRegistration.Type, out var sourceComp)) // Does the original have this component?
|
||||||
clonePod.UsedBiomass = cloningCost;
|
|
||||||
// end of biomass checks
|
|
||||||
|
|
||||||
// genetic damage checks
|
|
||||||
if (TryComp<DamageableComponent>(bodyToClone, out var damageable) &&
|
|
||||||
damageable.Damage.DamageDict.TryGetValue("Cellular", out var cellularDmg))
|
|
||||||
{
|
{
|
||||||
var chance = Math.Clamp((float) (cellularDmg / 100), 0, 1);
|
if (HasComp(clone.Value, componentRegistration.Type)) // CopyComp cannot overwrite existing components
|
||||||
chance *= failChanceModifier;
|
RemComp(clone.Value, componentRegistration.Type);
|
||||||
|
CopyComp(original, clone.Value, sourceComp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cellularDmg > 0 && clonePod.ConnectedConsole != null)
|
var cloningEv = new CloningEvent(settings, clone.Value);
|
||||||
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value, Loc.GetString("cloning-console-cellular-warning", ("percent", Math.Round(100 - chance * 100))), InGameICChatType.Speak, false);
|
RaiseLocalEvent(original, ref cloningEv); // used for datafields that cannot be directly copied
|
||||||
|
|
||||||
if (_robustRandom.Prob(chance))
|
// Add equipment first so that SetEntityName also renames the ID card.
|
||||||
{
|
if (settings.CopyEquipment != null)
|
||||||
UpdateStatus(uid, CloningPodStatus.Gore, clonePod);
|
CopyEquipment(original, clone.Value, settings.CopyEquipment.Value, settings.Whitelist, settings.Blacklist);
|
||||||
clonePod.FailedClone = true;
|
|
||||||
AddComp<ActiveCloningPodComponent>(uid);
|
var originalName = Name(original);
|
||||||
|
if (TryComp<NameModifierComponent>(original, out var nameModComp)) // if the originals name was modified, use the unmodified name
|
||||||
|
originalName = nameModComp.BaseName;
|
||||||
|
|
||||||
|
// This will properly set the BaseName and EntityName for the clone.
|
||||||
|
// Adding the component first before renaming will make sure RefreshNameModifers is called.
|
||||||
|
// Without this the name would get reverted to Urist.
|
||||||
|
// If the clone has no name modifiers, NameModifierComponent will be removed again.
|
||||||
|
EnsureComp<NameModifierComponent>(clone.Value);
|
||||||
|
_metaData.SetEntityName(clone.Value, originalName);
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Chat, LogImpact.Medium, $"The body of {original:player} was cloned as {clone.Value:player}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// end of genetic damage checks
|
|
||||||
|
|
||||||
var mob = Spawn(speciesPrototype.Prototype, _transformSystem.GetMapCoordinates(uid));
|
|
||||||
_humanoidSystem.CloneAppearance(bodyToClone, mob);
|
|
||||||
|
|
||||||
var ev = new CloningEvent(bodyToClone, mob);
|
|
||||||
RaiseLocalEvent(bodyToClone, ref ev);
|
|
||||||
|
|
||||||
if (!ev.NameHandled)
|
|
||||||
_metaSystem.SetEntityName(mob, MetaData(bodyToClone).EntityName);
|
|
||||||
|
|
||||||
var cloneMindReturn = EntityManager.AddComponent<BeingClonedComponent>(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);
|
|
||||||
|
|
||||||
AddComp<ActiveCloningPodComponent>(uid);
|
|
||||||
|
|
||||||
// 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 var prototype))
|
|
||||||
{
|
|
||||||
foreach (var special in prototype.Special)
|
|
||||||
{
|
|
||||||
if (special is AddComponentSpecial)
|
|
||||||
special.AfterEquip(mob);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateStatus(EntityUid podUid, CloningPodStatus status, CloningPodComponent cloningPod)
|
|
||||||
{
|
|
||||||
cloningPod.Status = status;
|
|
||||||
_appearance.SetData(podUid, CloningPodVisuals.Status, cloningPod.Status);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Update(float frameTime)
|
|
||||||
{
|
|
||||||
var query = EntityQueryEnumerator<ActiveCloningPodComponent, CloningPodComponent>();
|
|
||||||
while (query.MoveNext(out var uid, out var _, out var cloning))
|
|
||||||
{
|
|
||||||
if (!_powerReceiverSystem.IsPowered(uid))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (cloning.BodyContainer.ContainedEntity == null && !cloning.FailedClone)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
cloning.CloningProgress += frameTime;
|
|
||||||
if (cloning.CloningProgress < cloning.CloningTime)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (cloning.FailedClone)
|
|
||||||
EndFailedCloning(uid, cloning);
|
|
||||||
else
|
|
||||||
Eject(uid, cloning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// On emag, spawns a failed clone when cloning process fails which attacks nearby crew.
|
/// Copies the equipment the original has to the clone.
|
||||||
|
/// This uses the original prototype of the items, so any changes to components that are done after spawning are lost!
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
|
public void CopyEquipment(EntityUid original, EntityUid clone, SlotFlags slotFlags, EntityWhitelist? whitelist = null, EntityWhitelist? blacklist = null)
|
||||||
{
|
{
|
||||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
if (!TryComp<InventoryComponent>(original, out var originalInventory) || !TryComp<InventoryComponent>(clone, out var cloneInventory))
|
||||||
return;
|
return;
|
||||||
|
// Iterate over all inventory slots
|
||||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
var slotEnumerator = _inventory.GetSlotEnumerator((original, originalInventory), slotFlags);
|
||||||
return;
|
while (slotEnumerator.NextItem(out var item, out var slot))
|
||||||
|
|
||||||
if (!this.IsPowered(uid, EntityManager))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
|
|
||||||
args.Handled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Eject(EntityUid uid, CloningPodComponent? clonePod)
|
|
||||||
{
|
{
|
||||||
if (!Resolve(uid, ref clonePod))
|
// Spawn a copy of the item using the original prototype.
|
||||||
return;
|
// This means any changes done to the item after spawning will be reset, but that should not be a problem for simple items like clothing etc.
|
||||||
|
// we use a whitelist and blacklist to be sure to exclude any problematic entities
|
||||||
|
|
||||||
if (clonePod.BodyContainer.ContainedEntity is not { Valid: true } entity || clonePod.CloningProgress < clonePod.CloningTime)
|
if (_whitelist.IsWhitelistFail(whitelist, item) || _whitelist.IsBlacklistPass(blacklist, item))
|
||||||
return;
|
continue;
|
||||||
|
|
||||||
EntityManager.RemoveComponent<BeingClonedComponent>(entity);
|
var prototype = MetaData(item).EntityPrototype;
|
||||||
_containerSystem.Remove(entity, clonePod.BodyContainer);
|
if (prototype != null)
|
||||||
clonePod.CloningProgress = 0f;
|
_inventory.SpawnItemInSlot(clone, slot.Name, prototype.ID, silent: true, inventory: cloneInventory);
|
||||||
clonePod.UsedBiomass = 0;
|
|
||||||
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
|
|
||||||
RemCompDeferred<ActiveCloningPodComponent>(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 (_emag.CheckFlag(uid, EmagType.Interaction))
|
|
||||||
{
|
|
||||||
_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 (!_emag.CheckFlag(uid, EmagType.Interaction))
|
|
||||||
{
|
|
||||||
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
|
|
||||||
}
|
|
||||||
|
|
||||||
clonePod.UsedBiomass = 0;
|
|
||||||
RemCompDeferred<ActiveCloningPodComponent>(uid);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Reset(RoundRestartCleanupEvent ev)
|
|
||||||
{
|
|
||||||
ClonesWaitingForMind.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Content.Shared.Cloning;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
|
||||||
|
namespace Content.Server.Cloning.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is added to a marker entity in order to spawn a clone of a random player.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, EntityCategory("Spawner")]
|
||||||
|
public sealed partial class RandomCloneSpawnerComponent : Component
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cloning settings to be used.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
|
||||||
|
}
|
||||||
47
Content.Server/Cloning/RandomCloneSpawnerSystem.cs
Normal file
47
Content.Server/Cloning/RandomCloneSpawnerSystem.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
using Content.Server.Cloning.Components;
|
||||||
|
using Content.Shared.Mind;
|
||||||
|
using Content.Shared.Mobs.Systems;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
|
namespace Content.Server.Cloning;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This deals with spawning and setting up a clone of a random crew member.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RandomCloneSpawnerSystem : EntitySystem
|
||||||
|
{
|
||||||
|
[Dependency] private readonly CloningSystem _cloning = default!;
|
||||||
|
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||||
|
[Dependency] private readonly IRobustRandom _random = default!;
|
||||||
|
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||||
|
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<RandomCloneSpawnerComponent, MapInitEvent>(OnMapInit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMapInit(Entity<RandomCloneSpawnerComponent> ent, ref MapInitEvent args)
|
||||||
|
{
|
||||||
|
QueueDel(ent.Owner);
|
||||||
|
|
||||||
|
if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
|
||||||
|
{
|
||||||
|
Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for RandomCloneSpawner");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var allHumans = _mind.GetAliveHumans();
|
||||||
|
|
||||||
|
if (allHumans.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var bodyToClone = _random.Pick(allHumans).Comp.OwnedEntity;
|
||||||
|
|
||||||
|
if (bodyToClone != null)
|
||||||
|
_cloning.TryCloning(bodyToClone.Value, _transformSystem.GetMapCoordinates(ent.Owner), settings, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Content.Server.Body.Components;
|
using Content.Server.Body.Components;
|
||||||
|
using Content.Server.Body.Systems;
|
||||||
using Content.Server.DoAfter;
|
using Content.Server.DoAfter;
|
||||||
using Content.Server.Fluids.EntitySystems;
|
using Content.Server.Fluids.EntitySystems;
|
||||||
using Content.Server.Forensics.Components;
|
using Content.Server.Forensics.Components;
|
||||||
@@ -32,8 +33,9 @@ namespace Content.Server.Forensics
|
|||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
|
SubscribeLocalEvent<FingerprintComponent, ContactInteractionEvent>(OnInteract);
|
||||||
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit);
|
SubscribeLocalEvent<FingerprintComponent, MapInitEvent>(OnFingerprintInit, after: new[] { typeof(BloodstreamSystem) });
|
||||||
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit);
|
// The solution entities are spawned on MapInit as well, so we have to wait for that to be able to set the DNA in the bloodstream correctly without ResolveSolution failing
|
||||||
|
SubscribeLocalEvent<DnaComponent, MapInitEvent>(OnDNAInit, after: new[] { typeof(BloodstreamSystem) });
|
||||||
|
|
||||||
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
|
SubscribeLocalEvent<ForensicsComponent, BeingGibbedEvent>(OnBeingGibbed);
|
||||||
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
|
SubscribeLocalEvent<ForensicsComponent, MeleeHitEvent>(OnMeleeHit);
|
||||||
@@ -65,18 +67,20 @@ namespace Content.Server.Forensics
|
|||||||
|
|
||||||
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
|
private void OnFingerprintInit(Entity<FingerprintComponent> ent, ref MapInitEvent args)
|
||||||
{
|
{
|
||||||
ent.Comp.Fingerprint = GenerateFingerprint();
|
if (ent.Comp.Fingerprint == null)
|
||||||
Dirty(ent);
|
RandomizeFingerprint((ent.Owner, ent.Comp));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDNAInit(EntityUid uid, DnaComponent component, MapInitEvent args)
|
private void OnDNAInit(Entity<DnaComponent> ent, ref MapInitEvent args)
|
||||||
{
|
{
|
||||||
if (component.DNA == String.Empty)
|
Log.Debug($"Init DNA {Name(ent.Owner)} {ent.Comp.DNA}");
|
||||||
|
if (ent.Comp.DNA == null)
|
||||||
|
RandomizeDNA((ent.Owner, ent.Comp));
|
||||||
|
else
|
||||||
{
|
{
|
||||||
component.DNA = GenerateDNA();
|
// If set manually (for example by cloning) we also need to inform the bloodstream of the correct DNA string so it can be updated
|
||||||
|
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
|
||||||
var ev = new GenerateDnaEvent { Owner = uid, DNA = component.DNA };
|
RaiseLocalEvent(ent.Owner, ref ev);
|
||||||
RaiseLocalEvent(uid, ref ev);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +88,7 @@ namespace Content.Server.Forensics
|
|||||||
{
|
{
|
||||||
string dna = Loc.GetString("forensics-dna-unknown");
|
string dna = Loc.GetString("forensics-dna-unknown");
|
||||||
|
|
||||||
if (TryComp(uid, out DnaComponent? dnaComp))
|
if (TryComp(uid, out DnaComponent? dnaComp) && dnaComp.DNA != null)
|
||||||
dna = dnaComp.DNA;
|
dna = dnaComp.DNA;
|
||||||
|
|
||||||
foreach (EntityUid part in args.GibbedParts)
|
foreach (EntityUid part in args.GibbedParts)
|
||||||
@@ -103,7 +107,7 @@ namespace Content.Server.Forensics
|
|||||||
{
|
{
|
||||||
foreach (EntityUid hitEntity in args.HitEntities)
|
foreach (EntityUid hitEntity in args.HitEntities)
|
||||||
{
|
{
|
||||||
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp))
|
if (TryComp<DnaComponent>(hitEntity, out var hitEntityComp) && hitEntityComp.DNA != null)
|
||||||
component.DNAs.Add(hitEntityComp.DNA);
|
component.DNAs.Add(hitEntityComp.DNA);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,6 +305,9 @@ namespace Content.Server.Forensics
|
|||||||
|
|
||||||
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
|
private void OnTransferDnaEvent(EntityUid uid, DnaComponent component, ref TransferDnaEvent args)
|
||||||
{
|
{
|
||||||
|
if (component.DNA == null)
|
||||||
|
return;
|
||||||
|
|
||||||
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
|
var recipientComp = EnsureComp<ForensicsComponent>(args.Recipient);
|
||||||
recipientComp.DNAs.Add(component.DNA);
|
recipientComp.DNAs.Add(component.DNA);
|
||||||
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
|
recipientComp.CanDnaBeCleaned = args.CanDnaBeCleaned;
|
||||||
@@ -308,6 +315,36 @@ namespace Content.Server.Forensics
|
|||||||
|
|
||||||
#region Public API
|
#region Public API
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
|
||||||
|
/// Does nothing if it does not have the DnaComponent.
|
||||||
|
/// </summary>
|
||||||
|
public void RandomizeDNA(Entity<DnaComponent?> ent)
|
||||||
|
{
|
||||||
|
if (!Resolve(ent, ref ent.Comp, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ent.Comp.DNA = GenerateDNA();
|
||||||
|
Dirty(ent);
|
||||||
|
|
||||||
|
Log.Debug($"Randomize DNA {Name(ent.Owner)} {ent.Comp.DNA}");
|
||||||
|
var ev = new GenerateDnaEvent { Owner = ent.Owner, DNA = ent.Comp.DNA };
|
||||||
|
RaiseLocalEvent(ent.Owner, ref ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Give the entity a new, random fingerprint string.
|
||||||
|
/// Does nothing if it does not have the FingerprintComponent.
|
||||||
|
/// </summary>
|
||||||
|
public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
|
||||||
|
{
|
||||||
|
if (!Resolve(ent, ref ent.Comp, false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
ent.Comp.Fingerprint = GenerateFingerprint();
|
||||||
|
Dirty(ent);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Transfer DNA from one entity onto the forensics of another
|
/// Transfer DNA from one entity onto the forensics of another
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -316,7 +353,7 @@ namespace Content.Server.Forensics
|
|||||||
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
|
/// <param name="canDnaBeCleaned">If this DNA be cleaned off of the recipient. e.g. cleaning a knife vs cleaning a puddle of blood</param>
|
||||||
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
|
public void TransferDna(EntityUid recipient, EntityUid donor, bool canDnaBeCleaned = true)
|
||||||
{
|
{
|
||||||
if (TryComp<DnaComponent>(donor, out var donorComp))
|
if (TryComp<DnaComponent>(donor, out var donorComp) && donorComp.DNA != null)
|
||||||
{
|
{
|
||||||
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
|
EnsureComp<ForensicsComponent>(recipient, out var recipientComp);
|
||||||
recipientComp.DNAs.Add(donorComp.DNA);
|
recipientComp.DNAs.Add(donorComp.DNA);
|
||||||
|
|||||||
@@ -216,17 +216,11 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
|
|||||||
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
|
var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species);
|
||||||
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
|
_humanoidAppearance.LoadProfile(ent, newProfile, humanoid);
|
||||||
_metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
|
_metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc.
|
||||||
if (TryComp<DnaComponent>(ent, out var dna))
|
|
||||||
{
|
|
||||||
dna.DNA = _forensicsSystem.GenerateDNA();
|
|
||||||
|
|
||||||
var ev = new GenerateDnaEvent { Owner = ent, DNA = dna.DNA };
|
// If the entity has the respecive components, then scramble the dna and fingerprint strings
|
||||||
RaiseLocalEvent(ent, ref ev);
|
_forensicsSystem.RandomizeDNA(ent);
|
||||||
}
|
_forensicsSystem.RandomizeFingerprint(ent);
|
||||||
if (TryComp<FingerprintComponent>(ent, out var fingerprint))
|
|
||||||
{
|
|
||||||
fingerprint.Fingerprint = _forensicsSystem.GenerateFingerprint();
|
|
||||||
}
|
|
||||||
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
|
RemComp<DetailExaminableComponent>(ent); // remove MRP+ custom description if one exists
|
||||||
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
|
_identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event
|
||||||
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
|
_popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ public sealed class AddAccentClothingSystem : EntitySystem
|
|||||||
SubscribeLocalEvent<AddAccentClothingComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
SubscribeLocalEvent<AddAccentClothingComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: Turn this into a relay event.
|
||||||
private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args)
|
private void OnGotEquipped(EntityUid uid, AddAccentClothingComponent component, ref ClothingGotEquippedEvent args)
|
||||||
{
|
{
|
||||||
// does the user already has this accent?
|
// does the user already has this accent?
|
||||||
|
|||||||
20
Content.Server/Traits/Assorted/UnrevivableSystem.cs
Normal file
20
Content.Server/Traits/Assorted/UnrevivableSystem.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using Content.Shared.Cloning.Events;
|
||||||
|
using Content.Shared.Traits.Assorted;
|
||||||
|
|
||||||
|
namespace Content.Server.Traits.Assorted;
|
||||||
|
|
||||||
|
public sealed class UnrevivableSystem : EntitySystem
|
||||||
|
{
|
||||||
|
public override void Initialize()
|
||||||
|
{
|
||||||
|
base.Initialize();
|
||||||
|
|
||||||
|
SubscribeLocalEvent<UnrevivableComponent, CloningAttemptEvent>(OnCloningAttempt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCloningAttempt(Entity<UnrevivableComponent> ent, ref CloningAttemptEvent args)
|
||||||
|
{
|
||||||
|
if (!ent.Comp.Cloneable)
|
||||||
|
args.Cancelled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,11 +24,11 @@ using Content.Shared.Mobs;
|
|||||||
using Content.Shared.Mobs.Components;
|
using Content.Shared.Mobs.Components;
|
||||||
using Content.Shared.Movement.Pulling.Components;
|
using Content.Shared.Movement.Pulling.Components;
|
||||||
using Content.Shared.Movement.Systems;
|
using Content.Shared.Movement.Systems;
|
||||||
|
using Content.Shared.NameModifier.EntitySystems;
|
||||||
using Content.Shared.NPC.Systems;
|
using Content.Shared.NPC.Systems;
|
||||||
using Content.Shared.Nutrition.AnimalHusbandry;
|
using Content.Shared.Nutrition.AnimalHusbandry;
|
||||||
using Content.Shared.Nutrition.Components;
|
using Content.Shared.Nutrition.Components;
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
using Content.Shared.Roles;
|
|
||||||
using Content.Shared.Weapons.Melee;
|
using Content.Shared.Weapons.Melee;
|
||||||
using Content.Shared.Zombies;
|
using Content.Shared.Zombies;
|
||||||
using Content.Shared.Prying.Components;
|
using Content.Shared.Prying.Components;
|
||||||
@@ -58,8 +58,8 @@ public sealed partial class ZombieSystem
|
|||||||
[Dependency] private readonly MindSystem _mind = default!;
|
[Dependency] private readonly MindSystem _mind = default!;
|
||||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||||
[Dependency] private readonly NPCSystem _npc = default!;
|
[Dependency] private readonly NPCSystem _npc = default!;
|
||||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
|
||||||
[Dependency] private readonly TagSystem _tag = default!;
|
[Dependency] private readonly TagSystem _tag = default!;
|
||||||
|
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles an entity turning into a zombie when they die or go into crit
|
/// Handles an entity turning into a zombie when they die or go into crit
|
||||||
@@ -235,7 +235,7 @@ public sealed partial class ZombieSystem
|
|||||||
if (hasMind && _mind.TryGetSession(mindId, out var session))
|
if (hasMind && _mind.TryGetSession(mindId, out var session))
|
||||||
{
|
{
|
||||||
//Zombie role for player manifest
|
//Zombie role for player manifest
|
||||||
_roles.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
|
_role.MindAddRole(mindId, "MindRoleZombie", mind: null, silent: true);
|
||||||
|
|
||||||
//Greeting message for new bebe zombers
|
//Greeting message for new bebe zombers
|
||||||
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
|
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
|
||||||
|
|||||||
@@ -5,18 +5,20 @@ using Content.Server.Chat;
|
|||||||
using Content.Server.Chat.Systems;
|
using Content.Server.Chat.Systems;
|
||||||
using Content.Server.Emoting.Systems;
|
using Content.Server.Emoting.Systems;
|
||||||
using Content.Server.Speech.EntitySystems;
|
using Content.Server.Speech.EntitySystems;
|
||||||
|
using Content.Server.Roles;
|
||||||
using Content.Shared.Anomaly.Components;
|
using Content.Shared.Anomaly.Components;
|
||||||
using Content.Shared.Bed.Sleep;
|
using Content.Shared.Bed.Sleep;
|
||||||
using Content.Shared.Cloning;
|
using Content.Shared.Cloning.Events;
|
||||||
using Content.Shared.Damage;
|
using Content.Shared.Damage;
|
||||||
using Content.Shared.Humanoid;
|
using Content.Shared.Humanoid;
|
||||||
using Content.Shared.Inventory;
|
using Content.Shared.Inventory;
|
||||||
using Content.Shared.Mind;
|
using Content.Shared.Mind;
|
||||||
|
using Content.Shared.Mind.Components;
|
||||||
using Content.Shared.Mobs;
|
using Content.Shared.Mobs;
|
||||||
using Content.Shared.Mobs.Components;
|
using Content.Shared.Mobs.Components;
|
||||||
using Content.Shared.Mobs.Systems;
|
using Content.Shared.Mobs.Systems;
|
||||||
using Content.Shared.NameModifier.EntitySystems;
|
|
||||||
using Content.Shared.Popups;
|
using Content.Shared.Popups;
|
||||||
|
using Content.Shared.Roles;
|
||||||
using Content.Shared.Weapons.Melee.Events;
|
using Content.Shared.Weapons.Melee.Events;
|
||||||
using Content.Shared.Zombies;
|
using Content.Shared.Zombies;
|
||||||
using Robust.Shared.Prototypes;
|
using Robust.Shared.Prototypes;
|
||||||
@@ -38,7 +40,7 @@ namespace Content.Server.Zombies
|
|||||||
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
|
[Dependency] private readonly EmoteOnDamageSystem _emoteOnDamage = default!;
|
||||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||||
[Dependency] private readonly NameModifierSystem _nameMod = default!;
|
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||||
|
|
||||||
public const SlotFlags ProtectiveSlots =
|
public const SlotFlags ProtectiveSlots =
|
||||||
SlotFlags.FEET |
|
SlotFlags.FEET |
|
||||||
@@ -63,6 +65,8 @@ namespace Content.Server.Zombies
|
|||||||
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
|
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
|
||||||
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
|
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
|
||||||
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
|
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
|
||||||
|
SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
|
||||||
|
SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
|
||||||
|
|
||||||
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
|
||||||
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
|
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
|
||||||
@@ -272,7 +276,7 @@ namespace Content.Server.Zombies
|
|||||||
/// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
|
/// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
|
||||||
/// <param name="zombiecomp"></param>
|
/// <param name="zombiecomp"></param>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// this currently only restore the name and skin/eye color from before zombified
|
/// this currently only restore the skin/eye color from before zombified
|
||||||
/// TODO: completely rethink how zombies are done to allow reversal.
|
/// TODO: completely rethink how zombies are done to allow reversal.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
|
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
|
||||||
@@ -292,14 +296,25 @@ namespace Content.Server.Zombies
|
|||||||
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
|
_humanoidAppearance.SetSkinColor(target, zombiecomp.BeforeZombifiedSkinColor, false);
|
||||||
_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
|
_bloodstream.ChangeBloodReagent(target, zombiecomp.BeforeZombifiedBloodReagent);
|
||||||
|
|
||||||
_nameMod.RefreshNameModifiers(target);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnZombieCloning(EntityUid uid, ZombieComponent zombiecomp, ref CloningEvent args)
|
private void OnZombieCloning(Entity<ZombieComponent> ent, ref CloningEvent args)
|
||||||
{
|
{
|
||||||
if (UnZombify(args.Source, args.Target, zombiecomp))
|
UnZombify(ent.Owner, args.CloneUid, ent.Comp);
|
||||||
args.NameHandled = true;
|
}
|
||||||
|
|
||||||
|
// Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
|
||||||
|
private void OnMindAdded(Entity<ZombieComponent> ent, ref MindAddedMessage args)
|
||||||
|
{
|
||||||
|
if (!_role.MindHasRole<ZombieRoleComponent>(args.Mind))
|
||||||
|
_role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
|
||||||
|
private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
|
||||||
|
{
|
||||||
|
_role.MindTryRemoveRole<ZombieRoleComponent>(args.Mind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
13
Content.Shared/Cloning/CloningEvents.cs
Normal file
13
Content.Shared/Cloning/CloningEvents.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Content.Shared.Cloning.Events;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised before a mob is cloned. Cancel to prevent cloning.
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct CloningAttemptEvent(CloningSettingsPrototype Settings, bool Cancelled = false);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised after a new mob got spawned when cloning a humanoid.
|
||||||
|
/// </summary>
|
||||||
|
[ByRefEvent]
|
||||||
|
public record struct CloningEvent(CloningSettingsPrototype Settings, EntityUid CloneUid);
|
||||||
@@ -10,8 +10,8 @@ namespace Content.Shared.Cloning;
|
|||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
public sealed partial class CloningPodComponent : Component
|
public sealed partial class CloningPodComponent : Component
|
||||||
{
|
{
|
||||||
[ValidatePrototypeId<SinkPortPrototype>]
|
[DataField]
|
||||||
public const string PodPort = "CloningPodReceiver";
|
public ProtoId<SinkPortPrototype> PodPort = "CloningPodReceiver";
|
||||||
|
|
||||||
[ViewVariables]
|
[ViewVariables]
|
||||||
public ContainerSlot BodyContainer = default!;
|
public ContainerSlot BodyContainer = default!;
|
||||||
@@ -31,23 +31,25 @@ public sealed partial class CloningPodComponent : Component
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The material that is used to clone entities.
|
/// The material that is used to clone entities.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("requiredMaterial"), ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public ProtoId<MaterialPrototype> RequiredMaterial = "Biomass";
|
public ProtoId<MaterialPrototype> RequiredMaterial = "Biomass";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current amount of time it takes to clone a body
|
/// The current amount of time it takes to clone a body.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public float CloningTime = 30f;
|
public float CloningTime = 30f;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The mob to spawn on emag
|
/// The mob to spawn on emag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)]
|
[DataField]
|
||||||
public EntProtoId MobSpawnId = "MobAbomination";
|
public EntProtoId MobSpawnId = "MobAbomination";
|
||||||
|
|
||||||
// TODO: Remove this from here when cloning and/or zombies are refactored
|
/// <summary>
|
||||||
[DataField("screamSound")]
|
/// The sound played when a mob is spawned from an emagged cloning pod.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")
|
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")
|
||||||
{
|
{
|
||||||
Params = AudioParams.Default.WithVolume(4),
|
Params = AudioParams.Default.WithVolume(4),
|
||||||
@@ -74,21 +76,3 @@ public enum CloningPodStatus : byte
|
|||||||
Gore,
|
Gore,
|
||||||
NoMind
|
NoMind
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Raised after a new mob got spawned when cloning a humanoid
|
|
||||||
/// </summary>
|
|
||||||
[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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
60
Content.Shared/Cloning/CloningSettingsPrototype.cs
Normal file
60
Content.Shared/Cloning/CloningSettingsPrototype.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using Content.Shared.Inventory;
|
||||||
|
using Content.Shared.Whitelist;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
|
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
|
||||||
|
|
||||||
|
namespace Content.Shared.Cloning;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Settings for cloning a humanoid.
|
||||||
|
/// Used to decide which components should be copied.
|
||||||
|
/// </summary>
|
||||||
|
[Prototype]
|
||||||
|
public sealed partial class CloningSettingsPrototype : IPrototype, IInheritingPrototype
|
||||||
|
{
|
||||||
|
/// <inheritdoc/>
|
||||||
|
[IdDataField]
|
||||||
|
public string ID { get; private set; } = default!;
|
||||||
|
|
||||||
|
[ParentDataField(typeof(PrototypeIdArraySerializer<CloningSettingsPrototype>))]
|
||||||
|
public string[]? Parents { get; }
|
||||||
|
|
||||||
|
[AbstractDataField]
|
||||||
|
[NeverPushInheritance]
|
||||||
|
public bool Abstract { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if cloning can be prevented by traits etc.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool ForceCloning = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Which inventory slots will receive a copy of the original's clothing.
|
||||||
|
/// Disabled when null.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public SlotFlags? CopyEquipment = SlotFlags.WITHOUT_POCKET;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whitelist for the equipment allowed to be copied.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public EntityWhitelist? Whitelist;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blacklist for the equipment allowed to be copied.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public EntityWhitelist? Blacklist;
|
||||||
|
|
||||||
|
/// TODO: Make this not a string https://github.com/space-wizards/RobustToolbox/issues/5709
|
||||||
|
/// <summary>
|
||||||
|
/// Components to copy from the original to the clone.
|
||||||
|
/// This only makes a shallow copy of datafields!
|
||||||
|
/// If you need a deep copy or additional component initialization, then subscribe to CloningEvent instead!
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
[AlwaysPushInheritance]
|
||||||
|
public HashSet<string> Components = new();
|
||||||
|
}
|
||||||
@@ -9,5 +9,5 @@ namespace Content.Shared.Forensics.Components;
|
|||||||
public sealed partial class DnaComponent : Component
|
public sealed partial class DnaComponent : Component
|
||||||
{
|
{
|
||||||
[DataField("dna"), AutoNetworkedField]
|
[DataField("dna"), AutoNetworkedField]
|
||||||
public string DNA = String.Empty;
|
public string? DNA;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public record struct TransferDnaEvent()
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An event to generate and act upon new DNA for an entity.
|
/// Raised on an entity when its DNA has been changed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ByRefEvent]
|
[ByRefEvent]
|
||||||
public record struct GenerateDnaEvent()
|
public record struct GenerateDnaEvent()
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ public sealed partial class UnrevivableComponent : Component
|
|||||||
[DataField, AutoNetworkedField]
|
[DataField, AutoNetworkedField]
|
||||||
public bool Analyzable = true;
|
public bool Analyzable = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Can this player be cloned using a cloning pod?
|
||||||
|
/// </summary>
|
||||||
|
[DataField, AutoNetworkedField]
|
||||||
|
public bool Cloneable = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The loc string used to provide a reason for being unrevivable
|
/// The loc string used to provide a reason for being unrevivable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ cloning-console-component-msg-no-cloner = Not Ready: No Cloner Detected
|
|||||||
cloning-console-component-msg-no-mind = Not Ready: No Soul Activity Detected
|
cloning-console-component-msg-no-mind = Not Ready: No Soul Activity Detected
|
||||||
|
|
||||||
cloning-console-chat-error = ERROR: INSUFFICIENT BIOMASS. CLONING THIS BODY REQUIRES {$units} UNITS OF BIOMASS.
|
cloning-console-chat-error = ERROR: INSUFFICIENT BIOMASS. CLONING THIS BODY REQUIRES {$units} UNITS OF BIOMASS.
|
||||||
cloning-console-uncloneable-trait-error = ERROR: SOUL IS ABSENT, CLONING IS IMPOSSIBLE.
|
cloning-console-uncloneable-trait-error = ERROR: CLONING IS IMPOSSIBLE DUE TO ABNORMAL BODY COMPOSITION.
|
||||||
cloning-console-cellular-warning = WARNING: GENEFSCK CONFIDENCE SCORE IS {$percent}%. CLONING MAY HAVE UNEXPECTED RESULTS.
|
cloning-console-cellular-warning = WARNING: GENEFSCK CONFIDENCE SCORE IS {$percent}%. CLONING MAY HAVE UNEXPECTED RESULTS.
|
||||||
|
|||||||
@@ -60,11 +60,13 @@
|
|||||||
id: CloningPodSender
|
id: CloningPodSender
|
||||||
name: signal-port-name-pod-receiver
|
name: signal-port-name-pod-receiver
|
||||||
description: signal-port-description-pod-sender
|
description: signal-port-description-pod-sender
|
||||||
|
defaultLinks: [ CloningPodReceiver ]
|
||||||
|
|
||||||
- type: sourcePort
|
- type: sourcePort
|
||||||
id: MedicalScannerSender
|
id: MedicalScannerSender
|
||||||
name: signal-port-name-med-scanner-sender
|
name: signal-port-name-med-scanner-sender
|
||||||
description: signal-port-description-med-scanner-sender
|
description: signal-port-description-med-scanner-sender
|
||||||
|
defaultLinks: [ MedicalScannerReceiver ]
|
||||||
|
|
||||||
- type: sourcePort
|
- type: sourcePort
|
||||||
id: ArtifactAnalyzerSender
|
id: ArtifactAnalyzerSender
|
||||||
|
|||||||
84
Resources/Prototypes/Entities/Mobs/Player/clone.yml
Normal file
84
Resources/Prototypes/Entities/Mobs/Player/clone.yml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Settings for cloning bodies
|
||||||
|
# If you add a new trait, job specific component or a component doing visual/examination changes for humanoids
|
||||||
|
# then add it here to the correct prototype.
|
||||||
|
# The datafields of the components are only shallow copied using CopyComp.
|
||||||
|
# Subscribe to CloningEvent instead if that is not enough.
|
||||||
|
|
||||||
|
- type: cloningSettings
|
||||||
|
id: BaseClone
|
||||||
|
components:
|
||||||
|
# general
|
||||||
|
- DetailExaminable
|
||||||
|
- Dna
|
||||||
|
- Fingerprint
|
||||||
|
- NpcFactionMember
|
||||||
|
# traits
|
||||||
|
# - LegsParalyzed (you get healed)
|
||||||
|
- LightweightDrunk
|
||||||
|
- Narcolepsy
|
||||||
|
- Pacified
|
||||||
|
- PainNumbness
|
||||||
|
- Paracusia
|
||||||
|
- PermanentBlindness
|
||||||
|
- Unrevivable
|
||||||
|
# job specific
|
||||||
|
- BibleUser
|
||||||
|
- CommandStaff
|
||||||
|
- Clumsy
|
||||||
|
- MindShield
|
||||||
|
- MimePowers
|
||||||
|
# accents
|
||||||
|
- Accentless
|
||||||
|
- BackwardsAccent
|
||||||
|
- BarkAccent
|
||||||
|
- BleatingAccent
|
||||||
|
- FrenchAccent
|
||||||
|
- GermanAccent
|
||||||
|
- LizardAccent
|
||||||
|
- MobsterAccent
|
||||||
|
- MonkeyAccent
|
||||||
|
- MothAccent
|
||||||
|
- MumbleAccent
|
||||||
|
- OwOAccent
|
||||||
|
- ParrotAccent
|
||||||
|
- PirateAccent
|
||||||
|
# - ReplacementAccent
|
||||||
|
# Not supported at the moment because AddAccentClothingComponent will make it permanent when cloned.
|
||||||
|
# TODO: AddAccentClothingComponent should use an inventory relay event.
|
||||||
|
# Also ZombieComponent overwrites the old replacement accent, because you can only have one at a time.
|
||||||
|
- RussianAccent
|
||||||
|
- ScrambledAccent
|
||||||
|
- SkeletonAccent
|
||||||
|
- SlurredAccent
|
||||||
|
- SouthernAccent
|
||||||
|
- SpanishAccent
|
||||||
|
- StutteringAccent
|
||||||
|
blacklist:
|
||||||
|
components:
|
||||||
|
- AttachedClothing # helmets, which are part of the suit
|
||||||
|
|
||||||
|
- type: cloningSettings
|
||||||
|
id: Antag
|
||||||
|
parent: BaseClone
|
||||||
|
components:
|
||||||
|
- HeadRevolutionary
|
||||||
|
- Revolutionary
|
||||||
|
|
||||||
|
- type: cloningSettings
|
||||||
|
id: CloningPod
|
||||||
|
parent: Antag
|
||||||
|
forceCloning: false
|
||||||
|
copyEquipment: null
|
||||||
|
|
||||||
|
# spawner
|
||||||
|
|
||||||
|
- type: entity
|
||||||
|
id: RandomCloneSpawner
|
||||||
|
name: Random Clone
|
||||||
|
suffix: Non-Antag
|
||||||
|
components:
|
||||||
|
- type: Sprite
|
||||||
|
sprite: Markers/paradox_clone.rsi
|
||||||
|
state: preview
|
||||||
|
- type: RandomCloneSpawner
|
||||||
|
settings: BaseClone
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
- type: Hunger
|
- type: Hunger
|
||||||
- type: Thirst
|
- type: Thirst
|
||||||
- type: Icon
|
- type: Icon
|
||||||
sprite: Mobs/Species/Slime/parts.rsi # It was like this beforehand, no idea why.
|
sprite: Mobs/Species/Human/parts.rsi
|
||||||
state: full
|
state: full
|
||||||
- type: Respirator
|
- type: Respirator
|
||||||
damage:
|
damage:
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
- type: Speech
|
- type: Speech
|
||||||
speechSounds: Bass
|
speechSounds: Bass
|
||||||
- type: HumanoidAppearance
|
- type: HumanoidAppearance
|
||||||
species: Human
|
species: Dwarf
|
||||||
hideLayersOnEquip:
|
hideLayersOnEquip:
|
||||||
- Hair
|
- Hair
|
||||||
- Snout
|
- Snout
|
||||||
|
|||||||
14
Resources/Textures/Markers/paradox_clone.rsi/meta.json
Normal file
14
Resources/Textures/Markers/paradox_clone.rsi/meta.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"license": "CC-BY-SA-3.0",
|
||||||
|
"copyright": "preview combined from Mobs/Species/Human/parts.rsi, Clothing/Uniforms/Jumpsuit/janitor.rsi, Clothing/Shoes/Specific/galoshes.rsi, Clothing/Belt/janitor.rsi, Clothing/Hands/Gloves/janitor.rsi and Clothing/Head/Soft/purplesoft.rsi by slarticodefast",
|
||||||
|
"size": {
|
||||||
|
"x": 32,
|
||||||
|
"y": 32
|
||||||
|
},
|
||||||
|
"states": [
|
||||||
|
{
|
||||||
|
"name": "preview"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
Resources/Textures/Markers/paradox_clone.rsi/preview.png
Normal file
BIN
Resources/Textures/Markers/paradox_clone.rsi/preview.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user