Files
wwdpublic/Content.Server/Cloning/CloningSystem.Utility.cs
RedFoxIV daf4f66414 "Proper" "Softcrit" "Support" (#1545)
# Description

Implements the softcrit functionality.
Similiar to critical state but spessmen will be able to communicate and
crawl around, but not pick up items.
Also supports configuring what is and isn't allowed in different
MobStates (per mob prototype): you can enable picking up items while in
softcrit so people can pick up their lasgun and continue shooting after
taking a 40x46mm to their ass cheeks from the guest nukies while being
dragged to safety.

![escape-from-tarkov-raid](https://github.com/user-attachments/assets/7f31702d-5677-4daf-a13d-8a9525fd3f9f)

<details> <summary><h1>Technical details</h1></summary>
New prototype type: "mobStateParams" (`MobStateParametersPrototype`)
Used to specify what can and can't be done when in a certain mobstate.
Of note that they are not actually bound to any `MobState` by
themselves. To assign a params prototype to a mobstate, use
`InitMobStateParams` in `MobStateComponent`.
It has to be a prototype because if I just did something akin to
`Dictionary<MobState, Dictionary<string, bool>>`, you'd have to check
the parent and copy every flag besides the one you wish to modify. That
is, if I understand how the prototype system works correctly, which I
frankly doubt. <!-- Working on softcrit made me hate prototypes. -->

MobStateComponent now has:
- `Dictionary<string, string> InitMobStateParams`, for storing "mobstate
- parameter prototype" pairs. `<string, string>` because it has to be
editable via mob prototypes. Named "mobStateParams" for mob prototypes.
- `public Dictionary<MobState, MobStateParametersPrototype>
MobStateParams` for actually storing the params for each state
- `public Dictionary<MobState, MobStateParametersOverride>
MobStateParamsOverrides` for storing overrides.
`MobStateParametersOverride` is a struct which mirrors all
`MobStateParametersPrototype`'s fields, except they're all nullable.
This is meant for code which wants to temporarily override some setting,
like a spell which allows dead people to talk. This is not the best
solution, but it should do at first. A better option would be tracking
each change separately, instead of hoping different systems overriding
the same flag will play nicely with eachother.
- a shitton of getter methods

TraitModifyMobState now has:
- `public Dictionary<string, string> Params` to specify a new prototype
to use.
- Important note: All values of `MobStateParametersPrototype` are
nullable, which is a hack to support `TraitModifyMobState`. This trait
takes one `MobStateParametersPrototype` per mobstate and applies all of
its non-null values. This way, a params prototype can be created which
will only have `pointing: true` and the trait can apply it (e.g. to
critstate, so we can spam pointing while dying like it's a game of turbo
dota)
- The above is why that wall of getters exists: They check the relevant
override struct, then the relevant prototype. If both are null, they
default to false (0f for floats.) The only exception is
OxyDamageOverlay, because it's used both for oxy damage overlay (if
null) and as a vision-limiting black void in crit..

MobStateSystem now has:
- a bunch of new "IsSomething"/"CanDoSomething" methods to check the
various flags, alongside rewritten old ones.
-
![image](https://github.com/user-attachments/assets/33a6b296-c12c-4311-9abe-90ca4288e871)
lookin ahh predicate factory

</details>
---

# TODO

done:
- [x] Make proper use of `MobStateSystem.IsIncapacitated()`.
done: some checks were changed, some left as they did what was (more or
less) intended.
<details>Previous `IsIncapacitated()` implementation simply checked if
person was in crit or dead. Now there is a `IsIncapacitated` flag in the
parameters, but it's heavily underutilized. I may need some help on this
one, since I don't know where would be a good place to check for it and
I absolutely will not just scour the entire build in search for them.
</details>

- [x] Separate force-dropping items from being downed
done: dropItemsOnEntering bool field. If true, will drop items upon
entering linked mobstate.
- [x] Don't drop items if `ForceDown` is true but `PickingUp` is also
true.
done: dropItemsOnEntering bool field. If true, will drop items upon
entering linked mobstate.
- [x] Actually check what are "conscious attempts" are used for
done: whether or not mob is conscious. Renamed the bool field
accordingly.
- [x] Look into adding a way to make people choke "slowly" in softcrit
as opposed to choking at "regular speed" in crit. Make that into a param
option? Make that into a float so the speed can be finetuned?
done: `BreathingMultiplier` float field added.
<details>
1f is regular breathing, 0.25 is "quarter-breathing". Air taken is
multiplied by `BreathingMultiplier` and suffocation damage taken (that
is dealt by RespiratorSystem, not all oxy damage) is multiplied by
`1-BreathingMultiplier`.
</details>

- [x] make sure the serializer actually does its job
done: it doesn't. Removed.
- [x] Make an option to prohibit using radio headsets while in softcrit
done: Requires Incapacitated parameter to be false to be able to use
headset radio.
- [x] Make sure it at least compiles

not done:
- [ ] probably move some other stuff to Params if it makes sense. Same
thing as with `IsIncapacitated` though: I kinda don't want to, at least
for now.

---

<details><summary><h1>No media</h1></summary>
<p>

:p

</p>
</details>

---

# Changelog

🆑
- add: Soft critical state. Crawl to safety, or to your doom - whatever
is closer.

---------

Signed-off-by: RedFoxIV <38788538+RedFoxIV@users.noreply.github.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>

(cherry picked from commit 9a357c1774f1a783844a07b5414f504ca574d84c)
2025-02-15 00:12:50 +03:00

364 lines
14 KiB
C#

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<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);
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// Checks the body's physics component and any previously obtained modifiers to determine biomass cost.
/// If there is insufficient biomass, the cloning cannot start.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
private bool CheckGeneticDamage(EntityUid uid, EntityUid bodyToClone, CloningPodComponent clonePod, out float geneticDamage, float failChanceModifier = 1)
{
geneticDamage = 0;
if (clonePod.DoMetempsychosis)
return false;
if (TryComp<DamageableComponent>(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;
}
/// <summary>
/// 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.
/// </summary>
private void CauseCloningFail(EntityUid uid, CloningPodComponent component)
{
UpdateStatus(uid, CloningPodStatus.Gore, component);
component.FailedClone = true;
component.ActivelyCloning = true;
}
/// <summary>
/// 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.
/// </summary>
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<BeingClonedComponent>(entity);
_containerSystem.Remove(entity, clonePod.BodyContainer);
clonePod.CloningProgress = 0f;
clonePod.UsedBiomass = 0;
UpdateStatus(uid, CloningPodStatus.Idle, clonePod);
clonePod.ActivelyCloning = false;
}
/// <summary>
/// And now we turn it over to Chef Pod to make soup!
/// </summary>
private void EndFailedCloning(EntityUid uid, CloningPodComponent clonePod)
{
if (clonePod.BodyContainer.ContainedEntity is not null)
{
var entity = clonePod.BodyContainer.ContainedEntity.Value;
if (TryComp<PhysicsComponent>(entity, out var physics)
&& TryComp<BloodstreamComponent>(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<EmaggedComponent>(uid))
{
_audio.PlayPvs(clonePod.ScreamSound, uid);
Spawn(clonePod.MobSpawnId, Transform(uid).Coordinates);
}
if (!HasComp<EmaggedComponent>(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;
}
/// <summary>
/// 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.
/// </summary>
private void CleanupCloneComponents(EntityUid uid, EntityUid bodyToClone, bool forceOldProfile, bool doMetempsychosis)
{
if (forceOldProfile
&& TryComp<PsionicComponent>(bodyToClone, out var psionic))
{
var newPsionic = _serialization.CreateCopy(psionic, null, false, true);
AddComp(uid, newPsionic, true);
}
if (TryComp<LanguageKnowledgeComponent>(bodyToClone, out var oldKnowLangs))
{
var newKnowLangs = _serialization.CreateCopy(oldKnowLangs, null, false, true);
AddComp(uid, newKnowLangs, true);
}
if (TryComp<LanguageSpeakerComponent>(bodyToClone, out var oldSpeakLangs))
{
var newSpeakLangs = _serialization.CreateCopy(oldSpeakLangs, null, false, true);
AddComp(uid, newSpeakLangs, true);
}
if (doMetempsychosis)
EnsureComp<PsionicComponent>(uid);
EnsureComp<SpeechComponent>(uid);
EnsureComp<DamageForceSayComponent>(uid);
EnsureComp<EmotingComponent>(uid);
EnsureComp<MindContainerComponent>(uid);
EnsureComp<SSDIndicatorComponent>(uid);
RemComp<ReplacementAccentComponent>(uid);
RemComp<MonkeyAccentComponent>(uid);
RemComp<SentienceTargetComponent>(uid);
RemComp<GhostTakeoverAvailableComponent>(uid);
_tag.AddTag(uid, "DoorBumpOpener");
}
/// <summary>
/// When failing to clone, much of the failed body is dissolved into a slurry of Ammonia and Blood, which spills from the machine.
/// </summary>
/// <remarks>
/// WOE BEFALLS WHOEVER FAILS TO CLONE A LAMIA
/// </remarks>
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 _);
}
/// <summary>
/// Modify the clone's hunger and thirst values by an amount set in the cloningPod.
/// </summary>
private void UpdateHungerAndThirst(EntityUid uid, CloningPodComponent cloningPod)
{
if (cloningPod.HungerAdjustment != 0
&& TryComp<HungerComponent>(uid, out var hungerComponent))
_hunger.SetHunger(uid, cloningPod.HungerAdjustment, hungerComponent);
if (cloningPod.ThirstAdjustment != 0
&& TryComp<ThirstComponent>(uid, out var thirstComponent))
_thirst.SetThirst(uid, thirstComponent, cloningPod.ThirstAdjustment);
if (cloningPod.DrunkTimer != 0)
_drunk.TryApplyDrunkenness(uid, cloningPod.DrunkTimer);
}
/// <summary>
/// Updates the HumanoidAppearanceComponent of the clone.
/// If a species swap is occuring, this updates all relevant information as per server config.
/// </summary>
private void UpdateCloneAppearance(
EntityUid mob,
HumanoidCharacterProfile pref,
HumanoidAppearanceComponent humanoid,
List<Sex> sexes,
Gender oldGender,
bool switchingSpecies,
bool forceOldProfile,
out Gender gender)
{
gender = oldGender;
if (!TryComp<HumanoidAppearanceComponent>(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);
return;
}
_humanoidSystem.LoadProfile(mob, pref);
}
/// <summary>
/// Optionally makes sure that pronoun preferences are preserved by the clone.
/// Although handled here, the swap (if it occurs) happens during UpdateCloneAppearance.
/// </summary>
/// <param name="mob"></param>
/// <param name="gender"></param>
private void UpdateGrammar(EntityUid mob, Gender gender)
{
var grammar = EnsureComp<GrammarComponent>(mob);
grammar.ProperNoun = true;
grammar.Gender = gender;
Dirty(mob, grammar);
}
/// <summary>
/// Optionally puts the clone in crit with high Cellular damage.
/// Medbay should use Cryogenics to "Finish" clones. Doxarubixadone is perfect for this.
/// </summary>
private void UpdateCloneDamage(EntityUid mob, CloningPodComponent clonePodComp, float geneticDamage)
{
if (!clonePodComp.DoGeneticDamage
|| !HasComp<DamageableComponent>(mob)
|| !_thresholds.TryGetThresholdForState(mob, Shared.Mobs.MobState.Critical, Shared.Mobs.MobState.SoftCritical, out var threshold))
return;
DamageSpecifier damage = new();
damage.DamageDict.Add("Cellular", (int) threshold + 1 + geneticDamage);
_damageable.TryChangeDamage(mob, damage, true);
}
}