Files
wwdpublic/Content.Server/Zombies/ZombieSystem.Transform.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

292 lines
13 KiB
C#

using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
using Content.Server.Chat;
using Content.Server.Chat.Managers;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
using Content.Server.Inventory;
using Content.Server.Mind;
using Content.Server.Mind.Commands;
using Content.Server.NPC;
using Content.Server.NPC.Components;
using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Server.Roles;
using Content.Server.Speech.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Abilities.Psionics;
using Content.Shared.CombatMode;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Damage;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid;
using Content.Shared.Interaction.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.NPC.Components;
using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.AnimalHusbandry;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Weapons.Melee;
using Content.Shared.Zombies;
using Content.Shared.Prying.Components;
using Robust.Shared.Audio.Systems;
using Content.Shared.Traits.Assorted.Components;
using Content.Server.Abilities.Psionics;
namespace Content.Server.Zombies
{
/// <summary>
/// Handles zombie propagation and inherent zombie traits
/// </summary>
/// <remarks>
/// Don't Shitcode Open Inside
/// </remarks>
public sealed partial class ZombieSystem
{
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly ServerInventorySystem _inventory = default!;
[Dependency] private readonly NpcFactionSystem _faction = default!;
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly PsionicAbilitiesSystem _psionic = default!;
/// <summary>
/// Handles an entity turning into a zombie when they die or go into crit
/// </summary>
private void OnDamageChanged(EntityUid uid, ZombifyOnDeathComponent component, MobStateChangedEvent args)
{
if (args.IsDead())
{
ZombifyEntity(uid, args.Component);
}
}
/// <summary>
/// This is the general purpose function to call if you want to zombify an entity.
/// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
/// </summary>
/// <param name="target">the entity being zombified</param>
/// <param name="mobState"></param>
/// <remarks>
/// ALRIGHT BIG BOYS, GIRLS AND ANYONE ELSE. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING.
/// This function is the god function for zombie stuff, and it is cursed. I have
/// attempted to label everything thouroughly for your sanity. I have attempted to
/// rewrite this, but this is how it shall lie eternal. Turn back now.
/// -emo
/// </remarks>
public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
{
//Don't zombfiy zombies
if (HasComp<ZombieComponent>(target) || HasComp<ZombieImmuneComponent>(target))
return;
if (!Resolve(target, ref mobState, logMissing: false))
return;
//you're a real zombie now, son.
var zombiecomp = AddComp<ZombieComponent>(target);
//we need to basically remove all of these because zombies shouldn't
//get diseases, breath, be thirst, be hungry, die in space, have offspring or be paraplegic.
RemComp<RespiratorComponent>(target);
RemComp<BarotraumaComponent>(target);
RemComp<HungerComponent>(target);
RemComp<ThirstComponent>(target);
RemComp<ReproductiveComponent>(target);
RemComp<ReproductivePartnerComponent>(target);
RemComp<LegsParalyzedComponent>(target);
RemComp<ComplexInteractionComponent>(target);
if (HasComp<PsionicComponent>(target)) // Prevent psionic zombies
{
_psionic.RemoveAllPsionicPowers(target, true);
}
//funny voice
var accentType = "zombie";
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
accentType = accent.Accent;
EnsureComp<ReplacementAccentComponent>(target).Accent = accentType;
//This is needed for stupid entities that fuck up combat mode component
//in an attempt to make an entity not attack. This is the easiest way to do it.
var combat = EnsureComp<CombatModeComponent>(target);
RemComp<PacifiedComponent>(target);
_combat.SetCanDisarm(target, false, combat);
_combat.SetInCombatMode(target, true, combat);
//This is the actual damage of the zombie. We assign the visual appearance
//and range here because of stuff we'll find out later
var melee = EnsureComp<MeleeWeaponComponent>(target);
melee.Animation = zombiecomp.AttackAnimation;
melee.WideAnimation = zombiecomp.AttackAnimation;
melee.AltDisarm = false;
melee.Range = 1.2f;
melee.Angle = 0.0f;
melee.SoundHit = zombiecomp.BiteSound;
if (mobState.CurrentState.IsAlive())
{
// Groaning when damaged
EnsureComp<EmoteOnDamageComponent>(target);
_emoteOnDamage.AddEmote(target, "Scream");
// Random groaning
EnsureComp<AutoEmoteComponent>(target);
_autoEmote.AddEmote(target, "ZombieGroan");
}
//We have specific stuff for humanoid zombies because they matter more
if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
{
//store some values before changing them in case the humanoid get cloned later
zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
if (TryComp<BloodstreamComponent>(target, out var stream))
zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
// Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right
huApComp.EyeColor = zombiecomp.EyeColor;
// this might not resync on clone?
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
//This is done here because non-humanoids shouldn't get baller damage
//lord forgive me for the hardcoded damage
DamageSpecifier dspec = new()
{
DamageDict = new()
{
{ "Slash", 13 },
{ "Piercing", 7 },
{ "Structural", 10 }
}
};
melee.Damage = dspec;
// humanoid zombies get to pry open doors and shit
var pryComp = EnsureComp<PryingComponent>(target);
pryComp.SpeedModifier = 0.75f;
pryComp.PryPowered = true;
pryComp.Force = true;
Dirty(target, pryComp);
}
Dirty(target, melee);
//The zombie gets the assigned damage weaknesses and strengths
_damageable.SetDamageModifierSetId(target, "Zombie");
//This makes it so the zombie doesn't take bloodloss damage.
//NOTE: they are supposed to bleed, just not take damage
_bloodstream.SetBloodLossThreshold(target, 0f);
//Give them zombie blood
_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
_inventory.TryUnequip(target, "gloves", true, true);
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
_inventory.TryUnequip(target, "ears", true, true);
//popup
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
//Make it sentient if it's an animal or something
MakeSentientCommand.MakeSentient(target, EntityManager);
//Make the zombie not die in the cold. Good for space zombies
if (TryComp<TemperatureComponent>(target, out var tempComp))
tempComp.ColdDamage.ClampMax(0);
//Heals the zombie from all the damage it took while human
if (TryComp<DamageableComponent>(target, out var damageablecomp))
_damageable.SetAllDamage(target, damageablecomp, 0);
_mobState.ChangeMobState(target, MobState.Alive);
_faction.ClearFactions(target, dirty: false);
_faction.AddFaction(target, "Zombie");
//gives it the funny "Zombie ___" name.
_nameMod.RefreshNameModifiers(target);
_identity.QueueIdentityUpdate(target);
var htn = EnsureComp<HTNComponent>(target);
htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" };
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
_npc.SleepNPC(target, htn);
//He's gotta have a mind
var hasMind = _mind.TryGetMind(target, out var mindId, out _);
if (hasMind && _mind.TryGetSession(mindId, out var session))
{
//Zombie role for player manifest
_roles.MindAddRole(mindId, new ZombieRoleComponent { PrototypeId = zombiecomp.ZombieRoleId });
//Greeting message for new bebe zombers
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
// Notificate player about new role assignment
_audio.PlayGlobal(zombiecomp.GreetSoundNotification, session);
}
else
{
_npc.WakeNPC(target, htn);
}
if (!HasComp<GhostRoleMobSpawnerComponent>(target) && !hasMind) //this specific component gives build test trouble so pop off, ig
{
//yet more hardcoding. Visit zombie.ftl for more information.
var ghostRole = EnsureComp<GhostRoleComponent>(target);
EnsureComp<GhostTakeoverAvailableComponent>(target);
ghostRole.RoleName = Loc.GetString("zombie-generic");
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
}
if (TryComp<HandsComponent>(target, out var handsComp))
{
_hands.RemoveHands(target);
RemComp(target, handsComp);
}
// Sloth: What the fuck?
// How long until compregistry lmao.
RemComp<PullerComponent>(target);
// No longer waiting to become a zombie:
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
RemCompDeferred<PendingZombieComponent>(target);
//zombie gamemode stuff
var ev = new EntityZombifiedEvent(target);
RaiseLocalEvent(target, ref ev, true);
//zombies get slowdown once they convert
_movementSpeedModifier.RefreshMovementSpeedModifiers(target);
}
}
}