mirror of
https://github.com/WWhiteDreamProject/wwdpublic.git
synced 2026-04-19 06:28:40 +03:00
Makes an assortment of changes related to chems. Morphine has been made more accessible at roundstart, adding a pain pen to the prefilled advanced medical belt and swapping a pain pen for a morphine bottle in the prefilled syringe case. Morphine also now blocks pain for longer, hopefully alleviating an issue where the effect would wear off during heart transplants. Also fixes an error which required the patient to have *both* ForcedSleep and NoScream to block the surgery pain moodlet. Additionally adds a morphine autoinjector cartridge. Adds enunciase, which removes stuttering statuses and temporarily disables all accents, and formic acid, which can react to make water or enunciase. Haloperidol also now removes less severe stuttering and ousiana dust now removes psionic insulation and power blocking statuses. Salicylic acid now works on corpses to reflect the description. Additionally fixes a missing locale from the Goob chem port and tweaks the text for the proto medical multitool. --- <details><summary><h1>Media</h1></summary> <p>        (dylovene to the left for comparison)    </p> </details> --- 🆑 - add: Added formic acid - add: Added enunciase - add: Added morphine autoinjector cartridge - tweak: Haloperidol now counteracts stuttering from chems - tweak: Ousiana dust now counteracts psionic insulation/blocking statuses - tweak: Morphine blocks pain for longer - tweak: Morphine and artiplates are more visually distinct - tweak: Increased distribution of morphine roundstart - tweak: Changed wording of stamina change effect - tweak: Salicylic acid works on corpses - fix: Fixed missing locale text for some stamina-affecting chems - fix: Fixed guidebook falsely saying effects accumulate - fix: Fixed capitalisation of proto medical multitool - fix: Fixed morphine not blocking surgery pain moodlet --------- Signed-off-by: GNUtopia <93669372+GNUtopia@users.noreply.github.com> - CartridgePain # Morphine Tweaks#
932 lines
36 KiB
C#
932 lines
36 KiB
C#
using Content.Shared.Humanoid;
|
|
using Content.Shared.Humanoid.Markings;
|
|
using Content.Shared.Bed.Sleep;
|
|
using Content.Shared.Body.Part;
|
|
using Content.Shared.Body.Organ;
|
|
using Content.Shared.Body.Events;
|
|
using Content.Shared._Shitmed.BodyEffects;
|
|
using Content.Shared._Shitmed.Body.Events;
|
|
using Content.Shared.Buckle.Components;
|
|
using Content.Shared.Containers.ItemSlots;
|
|
using Content.Shared.Damage;
|
|
using Content.Shared.Damage.Prototypes;
|
|
using Content.Shared.DoAfter;
|
|
using Content.Shared.IdentityManagement;
|
|
using Content.Shared._Shitmed.Medical.Surgery.Conditions;
|
|
using Content.Shared._Shitmed.Medical.Surgery.Effects.Step;
|
|
using Content.Shared._Shitmed.Medical.Surgery.Steps;
|
|
using Content.Shared._Shitmed.Medical.Surgery.Steps.Parts;
|
|
using Content.Shared._Shitmed.Medical.Surgery.Tools;
|
|
using Content.Shared.Mood;
|
|
using Content.Shared.Inventory;
|
|
using Content.Shared.Item;
|
|
using Content.Shared._Shitmed.Body.Organ;
|
|
using Content.Shared._Shitmed.Body.Part;
|
|
using Content.Shared.Popups;
|
|
using Robust.Shared.Prototypes;
|
|
using Robust.Shared.Toolshed.TypeParsers;
|
|
using System.Linq;
|
|
using Content.Shared.Inventory.VirtualItem;
|
|
|
|
|
|
namespace Content.Shared._Shitmed.Medical.Surgery;
|
|
|
|
public abstract partial class SharedSurgerySystem
|
|
{
|
|
private static readonly string[] BruteDamageTypes = { "Slash", "Blunt", "Piercing" };
|
|
private static readonly string[] BurnDamageTypes = { "Heat", "Shock", "Cold", "Caustic" };
|
|
private void InitializeSteps()
|
|
{
|
|
SubscribeLocalEvent<SurgeryStepComponent, SurgeryStepEvent>(OnToolStep);
|
|
SubscribeLocalEvent<SurgeryStepComponent, SurgeryStepCompleteCheckEvent>(OnToolCheck);
|
|
SubscribeLocalEvent<SurgeryStepComponent, SurgeryCanPerformStepEvent>(OnToolCanPerform);
|
|
|
|
//SubSurgery<SurgeryCutLarvaRootsStepComponent>(OnCutLarvaRootsStep, OnCutLarvaRootsCheck);
|
|
|
|
/* Abandon all hope ye who enter here. Now I am become shitcoder, the bloater of files.
|
|
On a serious note, I really hate how much bloat this pattern of subscribing to a StepEvent and a CheckEvent
|
|
creates in terms of readability. And while Check DOES only run on the server side, it's still annoying to parse through.*/
|
|
|
|
SubSurgery<SurgeryTendWoundsEffectComponent>(OnTendWoundsStep, OnTendWoundsCheck);
|
|
SubSurgery<SurgeryStepCavityEffectComponent>(OnCavityStep, OnCavityCheck);
|
|
SubSurgery<SurgeryAddPartStepComponent>(OnAddPartStep, OnAddPartCheck);
|
|
SubSurgery<SurgeryAffixPartStepComponent>(OnAffixPartStep, OnAffixPartCheck);
|
|
SubSurgery<SurgeryRemovePartStepComponent>(OnRemovePartStep, OnRemovePartCheck);
|
|
SubSurgery<SurgeryAddOrganStepComponent>(OnAddOrganStep, OnAddOrganCheck);
|
|
SubSurgery<SurgeryRemoveOrganStepComponent>(OnRemoveOrganStep, OnRemoveOrganCheck);
|
|
SubSurgery<SurgeryAffixOrganStepComponent>(OnAffixOrganStep, OnAffixOrganCheck);
|
|
SubSurgery<SurgeryAddMarkingStepComponent>(OnAddMarkingStep, OnAddMarkingCheck);
|
|
SubSurgery<SurgeryRemoveMarkingStepComponent>(OnRemoveMarkingStep, OnRemoveMarkingCheck);
|
|
Subs.BuiEvents<SurgeryTargetComponent>(SurgeryUIKey.Key, subs =>
|
|
{
|
|
subs.Event<SurgeryStepChosenBuiMsg>(OnSurgeryTargetStepChosen);
|
|
});
|
|
}
|
|
|
|
private void SubSurgery<TComp>(EntityEventRefHandler<TComp, SurgeryStepEvent> onStep,
|
|
EntityEventRefHandler<TComp, SurgeryStepCompleteCheckEvent> onComplete) where TComp : IComponent
|
|
{
|
|
SubscribeLocalEvent(onStep);
|
|
SubscribeLocalEvent(onComplete);
|
|
}
|
|
|
|
private void OnToolStep(Entity<SurgeryStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (ent.Comp.Tool != null)
|
|
{
|
|
foreach (var reg in ent.Comp.Tool.Values)
|
|
{
|
|
if (!AnyHaveComp(args.Tools, reg.Component, out var tool, out _))
|
|
return;
|
|
|
|
if (_net.IsServer &&
|
|
TryComp(tool, out SurgeryToolComponent? toolComp) &&
|
|
toolComp.EndSound != null)
|
|
{
|
|
_audio.PlayPvs(toolComp.EndSound, tool);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.Add != null)
|
|
{
|
|
foreach (var reg in ent.Comp.Add.Values)
|
|
{
|
|
var compType = reg.Component.GetType();
|
|
if (HasComp(args.Part, compType))
|
|
continue;
|
|
AddComp(args.Part, _compFactory.GetComponent(compType));
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.Remove != null)
|
|
{
|
|
foreach (var reg in ent.Comp.Remove.Values)
|
|
{
|
|
RemComp(args.Part, reg.Component.GetType());
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.BodyAdd != null)
|
|
{
|
|
foreach (var reg in ent.Comp.BodyAdd.Values)
|
|
{
|
|
var compType = reg.Component.GetType();
|
|
if (HasComp(args.Body, compType))
|
|
continue;
|
|
AddComp(args.Body, _compFactory.GetComponent(compType));
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.BodyRemove != null)
|
|
{
|
|
foreach (var reg in ent.Comp.BodyRemove.Values)
|
|
{
|
|
RemComp(args.Body, reg.Component.GetType());
|
|
}
|
|
}
|
|
|
|
// Dude this fucking function is so bloated now what the fuck.
|
|
if (ent.Comp.AddOrganOnAdd != null)
|
|
{
|
|
var organSlotIdToOrgan = _body.GetPartOrgans(args.Part).ToDictionary(o => o.Item2.SlotId, o => o);
|
|
|
|
foreach (var (organSlotId, compsToAdd) in ent.Comp.AddOrganOnAdd)
|
|
{
|
|
if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organValue))
|
|
continue;
|
|
var (organId, organ) = organValue;
|
|
|
|
organ.OnAdd ??= new();
|
|
|
|
foreach (var (key, compToAdd) in compsToAdd)
|
|
organ.OnAdd[key] = compToAdd;
|
|
|
|
EnsureComp<OrganEffectComponent>(organId);
|
|
RaiseLocalEvent(organId, new OrganComponentsModifyEvent(args.Body, true));
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.RemoveOrganOnAdd != null)
|
|
{
|
|
var organSlotIdToOrgan = _body.GetPartOrgans(args.Part).ToDictionary(o => o.Item2.SlotId, o => o);
|
|
|
|
foreach (var (organSlotId, compsToRemove) in ent.Comp.RemoveOrganOnAdd)
|
|
{
|
|
if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organValue) ||
|
|
organValue.Item2.OnAdd == null)
|
|
continue;
|
|
var (organId, organ) = organValue;
|
|
|
|
// Need to raise this event first before removing the component entries so
|
|
// OrganEffectSystem still knows which components on the body to remove
|
|
RaiseLocalEvent(organId, new OrganComponentsModifyEvent(args.Body, false));
|
|
foreach (var key in compsToRemove.Keys)
|
|
organ.OnAdd.Remove(key);
|
|
}
|
|
}
|
|
|
|
|
|
if (!HasComp<ForcedSleepingComponent>(args.Body) && !HasComp<NoScreamComponent>(args.Body))
|
|
RaiseLocalEvent(args.Body, new MoodEffectEvent("SurgeryPain"));
|
|
// Morphine - reenable this :)
|
|
if (!_inventory.TryGetSlotEntity(args.User, "gloves", out var _)
|
|
|| !_inventory.TryGetSlotEntity(args.User, "mask", out var _))
|
|
{
|
|
if (!HasComp<SanitizedComponent>(args.User))
|
|
{
|
|
// Zeta - Xelthia Glove Jacket Check
|
|
_inventory.TryGetSlotEntity(args.User, "outerClothing", out var glovejacket);
|
|
if (!HasComp<GloveJacketComponent>(glovejacket) || !_inventory.TryGetSlotEntity(args.User, "mask", out var _))
|
|
{
|
|
var sepsis = new DamageSpecifier(_prototypes.Index<DamageTypePrototype>("Poison"), 5);
|
|
var ev = new SurgeryStepDamageEvent(args.User, args.Body, args.Part, args.Surgery, sepsis, 0.5f);
|
|
RaiseLocalEvent(args.Body, ref ev);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnToolCheck(Entity<SurgeryStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
// Lord this function is fucking bloated now. Need to clean it up so its less spammy.
|
|
if (ent.Comp.Add != null)
|
|
{
|
|
foreach (var reg in ent.Comp.Add.Values)
|
|
{
|
|
if (!HasComp(args.Part, reg.Component.GetType()))
|
|
{
|
|
args.Cancelled = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.Remove != null)
|
|
{
|
|
foreach (var reg in ent.Comp.Remove.Values)
|
|
{
|
|
if (HasComp(args.Part, reg.Component.GetType()))
|
|
{
|
|
args.Cancelled = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.BodyAdd != null)
|
|
{
|
|
foreach (var reg in ent.Comp.BodyAdd.Values)
|
|
{
|
|
if (!HasComp(args.Body, reg.Component.GetType()))
|
|
{
|
|
args.Cancelled = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.BodyRemove != null)
|
|
{
|
|
foreach (var reg in ent.Comp.BodyRemove.Values)
|
|
{
|
|
if (HasComp(args.Body, reg.Component.GetType()))
|
|
{
|
|
args.Cancelled = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.AddOrganOnAdd != null)
|
|
{
|
|
var organSlotIdToOrgan = _body.GetPartOrgans(args.Part).ToDictionary(o => o.Item2.SlotId, o => o.Item2);
|
|
foreach (var (organSlotId, compsToAdd) in ent.Comp.AddOrganOnAdd)
|
|
{
|
|
if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organ))
|
|
continue;
|
|
|
|
if (organ.OnAdd == null || compsToAdd.Keys.Any(key => !organ.OnAdd.ContainsKey(key)))
|
|
{
|
|
args.Cancelled = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ent.Comp.RemoveOrganOnAdd != null)
|
|
{
|
|
var organSlotIdToOrgan = _body.GetPartOrgans(args.Part).ToDictionary(o => o.Item2.SlotId, o => o.Item2);
|
|
foreach (var (organSlotId, compsToRemove) in ent.Comp.RemoveOrganOnAdd)
|
|
{
|
|
if (!organSlotIdToOrgan.TryGetValue(organSlotId, out var organ) || organ.OnAdd == null)
|
|
continue;
|
|
|
|
if (compsToRemove.Keys.Any(key => organ.OnAdd.ContainsKey(key)))
|
|
{
|
|
args.Cancelled = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnToolCanPerform(Entity<SurgeryStepComponent> ent, ref SurgeryCanPerformStepEvent args)
|
|
{
|
|
if (HasComp<SurgeryOperatingTableConditionComponent>(ent))
|
|
{
|
|
if (!TryComp(args.Body, out BuckleComponent? buckle) ||
|
|
!HasComp<OperatingTableComponent>(buckle.BuckledTo))
|
|
{
|
|
args.Invalid = StepInvalidReason.NeedsOperatingTable;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (_inventory.TryGetContainerSlotEnumerator(args.Body, out var containerSlotEnumerator, args.TargetSlots))
|
|
{
|
|
if (HasComp<SurgeryIgnoreClothingComponent>(args.User))
|
|
return;
|
|
|
|
while (containerSlotEnumerator.MoveNext(out var containerSlot))
|
|
{
|
|
if (!containerSlot.ContainedEntity.HasValue)
|
|
continue;
|
|
|
|
args.Invalid = StepInvalidReason.Armor;
|
|
args.Popup = Loc.GetString("surgery-ui-window-steps-error-armor");
|
|
return;
|
|
}
|
|
}
|
|
|
|
RaiseLocalEvent(args.Body, ref args);
|
|
|
|
if (args.Invalid != StepInvalidReason.None)
|
|
return;
|
|
|
|
if (ent.Comp.Tool != null)
|
|
{
|
|
args.ValidTools ??= new Dictionary<EntityUid, float>();
|
|
|
|
foreach (var reg in ent.Comp.Tool.Values)
|
|
{
|
|
if (!AnyHaveComp(args.Tools, reg.Component, out var tool, out var speed))
|
|
{
|
|
args.Invalid = StepInvalidReason.MissingTool;
|
|
|
|
if (reg.Component is ISurgeryToolComponent required)
|
|
args.Popup = $"You need {required.ToolName} to perform this step!";
|
|
|
|
return;
|
|
}
|
|
|
|
args.ValidTools[tool] = speed;
|
|
}
|
|
}
|
|
}
|
|
|
|
private EntProtoId? GetProtoId(EntityUid entityUid)
|
|
{
|
|
if (!TryComp<MetaDataComponent>(entityUid, out var metaData))
|
|
return null;
|
|
|
|
return metaData.EntityPrototype?.ID;
|
|
}
|
|
|
|
// I wonder if theres not a function that can do this already.
|
|
private bool HasDamageGroup(EntityUid entity, string[] group, out DamageableComponent? damageable)
|
|
{
|
|
if (!TryComp<DamageableComponent>(entity, out var damageableComp))
|
|
{
|
|
damageable = null;
|
|
return false;
|
|
}
|
|
|
|
damageable = damageableComp;
|
|
return group.Any(damageType => damageableComp.Damage.DamageDict.TryGetValue(damageType, out var value) && value > 0);
|
|
|
|
}
|
|
|
|
private void OnTendWoundsStep(Entity<SurgeryTendWoundsEffectComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
var group = ent.Comp.MainGroup == "Brute" ? BruteDamageTypes : BurnDamageTypes;
|
|
|
|
if (!HasDamageGroup(args.Body, group, out var damageable)
|
|
&& !HasDamageGroup(args.Part, group, out var _)
|
|
|| damageable == null) // This shouldnt be possible but the compiler doesn't shut up.
|
|
return;
|
|
|
|
|
|
// Right now the bonus is based off the body's total damage, maybe we could make it based off each part in the future.
|
|
var bonus = ent.Comp.HealMultiplier * damageable.DamagePerGroup[ent.Comp.MainGroup];
|
|
if (_mobState.IsDead(args.Body))
|
|
bonus *= 0.2;
|
|
|
|
var adjustedDamage = new DamageSpecifier(ent.Comp.Damage);
|
|
|
|
foreach (var type in group)
|
|
adjustedDamage.DamageDict[type] -= bonus;
|
|
|
|
var ev = new SurgeryStepDamageEvent(args.User, args.Body, args.Part, args.Surgery, adjustedDamage, 0.5f);
|
|
RaiseLocalEvent(args.Body, ref ev);
|
|
}
|
|
|
|
private void OnTendWoundsCheck(Entity<SurgeryTendWoundsEffectComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
var group = ent.Comp.MainGroup == "Brute" ? BruteDamageTypes : BurnDamageTypes;
|
|
|
|
if (HasDamageGroup(args.Body, group, out var _)
|
|
|| HasDamageGroup(args.Part, group, out var _))
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
/*private void OnCutLarvaRootsStep(Entity<SurgeryCutLarvaRootsStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (TryComp(args.Body, out VictimInfectedComponent? infected) &&
|
|
infected.BurstAt > _timing.CurTime &&
|
|
infected.SpawnedLarva == null)
|
|
{
|
|
infected.RootsCut = true;
|
|
}
|
|
}
|
|
|
|
private void OnCutLarvaRootsCheck(Entity<SurgeryCutLarvaRootsStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
if (!TryComp(args.Body, out VictimInfectedComponent? infected) || !infected.RootsCut)
|
|
args.Cancelled = true;
|
|
|
|
// The larva has fully developed and surgery is now impossible
|
|
// TODO: Surgery should still be possible, but the fully developed larva should escape while also saving the hosts life
|
|
if (infected != null && infected.SpawnedLarva != null)
|
|
args.Cancelled = true;
|
|
}*/
|
|
|
|
private void OnCavityStep(Entity<SurgeryStepCavityEffectComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (!TryComp(args.Part, out BodyPartComponent? partComp) || partComp.PartType != BodyPartType.Torso)
|
|
return;
|
|
|
|
_itemSlotsSystem.SetLock(args.Part, partComp.ItemInsertionSlot, false); // WWDP prevent inserting items into torsos without surgery
|
|
|
|
var activeHandEntity = _hands.EnumerateHeld(args.User).FirstOrDefault();
|
|
if (activeHandEntity != default
|
|
&& ent.Comp.Action == "Insert"
|
|
&& !HasComp<VirtualItemComponent>(activeHandEntity) // WWDP prevent trying to insert virtual items
|
|
&& TryComp(activeHandEntity, out ItemComponent? itemComp)
|
|
&& (itemComp.Size.Id == "Tiny"
|
|
|| itemComp.Size.Id == "Small"))
|
|
_itemSlotsSystem.TryInsert(ent, partComp.ItemInsertionSlot, activeHandEntity, args.User);
|
|
else if (ent.Comp.Action == "Remove")
|
|
_itemSlotsSystem.TryEjectToHands(ent, partComp.ItemInsertionSlot, args.User);
|
|
|
|
_itemSlotsSystem.SetLock(args.Part, partComp.ItemInsertionSlot, true); // WWDP prevent inserting items into torsos without surgery
|
|
}
|
|
|
|
private void OnCavityCheck(Entity<SurgeryStepCavityEffectComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
// Normally this check would simply be partComp.ItemInsertionSlot.HasItem, but as mentioned before,
|
|
// For whatever reason it's not instantiating the field on the clientside after the wizmerge.
|
|
if (!TryComp(args.Part, out BodyPartComponent? partComp)
|
|
|| !TryComp(args.Part, out ItemSlotsComponent? itemComp)
|
|
|| ent.Comp.Action == "Insert"
|
|
&& !itemComp.Slots[partComp.ContainerName].HasItem
|
|
|| ent.Comp.Action == "Remove"
|
|
&& itemComp.Slots[partComp.ContainerName].HasItem)
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
private void OnAddPartStep(Entity<SurgeryAddPartStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp))
|
|
return;
|
|
|
|
foreach (var tool in args.Tools)
|
|
{
|
|
if (TryComp(tool, out BodyPartComponent? partComp)
|
|
&& partComp.PartType == removedComp.Part
|
|
&& (removedComp.Symmetry == null || partComp.Symmetry == removedComp.Symmetry))
|
|
{
|
|
var slotName = removedComp.Symmetry != null
|
|
? $"{removedComp.Symmetry?.ToString().ToLower()} {removedComp.Part.ToString().ToLower()}"
|
|
: removedComp.Part.ToString().ToLower();
|
|
_body.TryCreatePartSlot(args.Part, slotName, partComp.PartType, out var _);
|
|
_body.AttachPart(args.Part, slotName, tool);
|
|
EnsureComp<BodyPartReattachedComponent>(tool);
|
|
var ev = new BodyPartAttachedEvent((tool, partComp));
|
|
RaiseLocalEvent(args.Body, ref ev);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnAffixPartStep(Entity<SurgeryAffixPartStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp))
|
|
return;
|
|
|
|
var targetPart = _body.GetBodyChildrenOfType(args.Body, removedComp.Part, symmetry: removedComp.Symmetry).FirstOrDefault();
|
|
|
|
if (targetPart != default)
|
|
{
|
|
// We reward players for properly affixing the parts by healing a little bit of damage, and enabling the part temporarily.
|
|
var ev = new BodyPartEnableChangedEvent(true);
|
|
RaiseLocalEvent(targetPart.Id, ref ev);
|
|
_damageable.TryChangeDamage(args.Body,
|
|
_body.GetHealingSpecifier(targetPart.Component) * 2,
|
|
canSever: false, // Just in case we heal a brute damage specifier and the logic gets fucky lol
|
|
targetPart: _body.GetTargetBodyPart(targetPart.Component.PartType, targetPart.Component.Symmetry));
|
|
RemComp<BodyPartReattachedComponent>(targetPart.Id);
|
|
}
|
|
}
|
|
|
|
private void OnAffixPartCheck(Entity<SurgeryAffixPartStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp))
|
|
return;
|
|
|
|
var targetPart = _body.GetBodyChildrenOfType(args.Body, removedComp.Part, symmetry: removedComp.Symmetry).FirstOrDefault();
|
|
|
|
if (targetPart != default
|
|
&& HasComp<BodyPartReattachedComponent>(targetPart.Id))
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
private void OnAddPartCheck(Entity<SurgeryAddPartStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
if (!TryComp(args.Surgery, out SurgeryPartRemovedConditionComponent? removedComp)
|
|
|| !_body.GetBodyChildrenOfType(args.Body, removedComp.Part, symmetry: removedComp.Symmetry).Any())
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
private void OnRemovePartStep(Entity<SurgeryRemovePartStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (!TryComp(args.Part, out BodyPartComponent? partComp)
|
|
|| partComp.Body != args.Body)
|
|
return;
|
|
|
|
var ev = new AmputateAttemptEvent(args.Part);
|
|
RaiseLocalEvent(args.Part, ref ev);
|
|
_hands.TryPickupAnyHand(args.User, args.Part);
|
|
}
|
|
|
|
private void OnRemovePartCheck(Entity<SurgeryRemovePartStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
if (!TryComp(args.Part, out BodyPartComponent? partComp)
|
|
|| partComp.Body == args.Body)
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
private void OnAddOrganStep(Entity<SurgeryAddOrganStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (!TryComp(args.Part, out BodyPartComponent? partComp)
|
|
|| partComp.Body != args.Body
|
|
|| !TryComp(args.Surgery, out SurgeryOrganConditionComponent? organComp)
|
|
|| organComp.Organ == null)
|
|
return;
|
|
|
|
// Adding organs is generally done for a single one at a time, so we only need to check for the first.
|
|
var firstOrgan = organComp.Organ.Values.FirstOrDefault();
|
|
if (firstOrgan == default)
|
|
return;
|
|
|
|
foreach (var tool in args.Tools)
|
|
{
|
|
if (HasComp(tool, firstOrgan.Component.GetType())
|
|
&& TryComp<OrganComponent>(tool, out var insertedOrgan)
|
|
&& _body.InsertOrgan(args.Part, tool, insertedOrgan.SlotId, partComp, insertedOrgan))
|
|
{
|
|
EnsureComp<OrganReattachedComponent>(tool);
|
|
if (_body.TrySetOrganUsed(tool, true, insertedOrgan)
|
|
&& insertedOrgan.OriginalBody != args.Body)
|
|
{
|
|
var ev = new SurgeryStepDamageChangeEvent(args.User, args.Body, args.Part, ent);
|
|
RaiseLocalEvent(ent, ref ev);
|
|
args.Complete = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnAddOrganCheck(Entity<SurgeryAddOrganStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
if (!TryComp<SurgeryOrganConditionComponent>(args.Surgery, out var organComp)
|
|
|| organComp.Organ is null
|
|
|| !TryComp(args.Part, out BodyPartComponent? partComp)
|
|
|| partComp.Body != args.Body)
|
|
return;
|
|
|
|
// For now we naively assume that every entity will only have one of each organ type.
|
|
// that we do surgery on, but in the future we'll need to reference their prototype somehow
|
|
// to know if they need 2 hearts, 2 lungs, etc.
|
|
foreach (var reg in organComp.Organ.Values)
|
|
{
|
|
if (!_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var _))
|
|
{
|
|
args.Cancelled = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnAffixOrganStep(Entity<SurgeryAffixOrganStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (!TryComp(args.Surgery, out SurgeryOrganConditionComponent? removedOrganComp)
|
|
|| removedOrganComp.Organ == null
|
|
|| !removedOrganComp.Reattaching)
|
|
return;
|
|
|
|
foreach (var reg in removedOrganComp.Organ.Values)
|
|
{
|
|
_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs);
|
|
if (organs != null && organs.Count > 0)
|
|
RemComp<OrganReattachedComponent>(organs[0].Id);
|
|
}
|
|
|
|
}
|
|
|
|
private void OnAffixOrganCheck(Entity<SurgeryAffixOrganStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
if (!TryComp(args.Surgery, out SurgeryOrganConditionComponent? removedOrganComp)
|
|
|| removedOrganComp.Organ == null
|
|
|| !removedOrganComp.Reattaching)
|
|
return;
|
|
|
|
foreach (var reg in removedOrganComp.Organ.Values)
|
|
{
|
|
_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs);
|
|
if (organs != null
|
|
&& organs.Count > 0
|
|
&& organs.Any(organ => HasComp<OrganReattachedComponent>(organ.Id)))
|
|
args.Cancelled = true;
|
|
}
|
|
}
|
|
|
|
private void OnRemoveOrganStep(Entity<SurgeryRemoveOrganStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (!TryComp<SurgeryOrganConditionComponent>(args.Surgery, out var organComp)
|
|
|| organComp.Organ == null)
|
|
return;
|
|
|
|
foreach (var reg in organComp.Organ.Values)
|
|
{
|
|
_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs);
|
|
if (organs != null && organs.Count > 0)
|
|
{
|
|
_body.RemoveOrgan(organs[0].Id, organs[0].Organ);
|
|
_hands.TryPickupAnyHand(args.User, organs[0].Id);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void OnRemoveOrganCheck(Entity<SurgeryRemoveOrganStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
if (!TryComp<SurgeryOrganConditionComponent>(args.Surgery, out var organComp)
|
|
|| organComp.Organ == null
|
|
|| !TryComp(args.Part, out BodyPartComponent? partComp)
|
|
|| partComp.Body != args.Body)
|
|
return;
|
|
|
|
foreach (var reg in organComp.Organ.Values)
|
|
{
|
|
if (_body.TryGetBodyPartOrgans(args.Part, reg.Component.GetType(), out var organs)
|
|
&& organs != null
|
|
&& organs.Count > 0)
|
|
{
|
|
args.Cancelled = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Refactor bodies to include ears as a prototype instead of doing whatever the hell this is.
|
|
private void OnAddMarkingStep(Entity<SurgeryAddMarkingStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
if (!TryComp(args.Body, out HumanoidAppearanceComponent? bodyAppearance)
|
|
|| ent.Comp.Organ == null)
|
|
return;
|
|
|
|
var organType = ent.Comp.Organ.Values.FirstOrDefault();
|
|
if (organType == default)
|
|
return;
|
|
|
|
var markingCategory = MarkingCategoriesConversion.FromHumanoidVisualLayers(ent.Comp.MarkingCategory);
|
|
foreach (var tool in args.Tools)
|
|
{
|
|
if (TryComp(tool, out MarkingContainerComponent? markingComp)
|
|
&& HasComp(tool, organType.Component.GetType()))
|
|
{
|
|
if (!bodyAppearance.MarkingSet.Markings.TryGetValue(markingCategory, out var markingList)
|
|
|| !markingList.Any(marking => marking.MarkingId.Contains(ent.Comp.MatchString)))
|
|
{
|
|
EnsureComp<BodyPartAppearanceComponent>(args.Part);
|
|
_body.ModifyMarkings(args.Body, args.Part, bodyAppearance, ent.Comp.MarkingCategory, markingComp.Marking);
|
|
|
|
if (ent.Comp.Accent != null
|
|
&& ent.Comp.Accent.Values.FirstOrDefault() is { } accent)
|
|
{
|
|
var compType = accent.Component.GetType();
|
|
if (!HasComp(args.Body, compType))
|
|
AddComp(args.Body, _compFactory.GetComponent(compType));
|
|
}
|
|
|
|
QueueDel(tool); // Again since this isnt actually being inserted we just delete it lol.
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private void OnAddMarkingCheck(Entity<SurgeryAddMarkingStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
var markingCategory = MarkingCategoriesConversion.FromHumanoidVisualLayers(ent.Comp.MarkingCategory);
|
|
|
|
if (!TryComp(args.Body, out HumanoidAppearanceComponent? bodyAppearance)
|
|
|| !bodyAppearance.MarkingSet.Markings.TryGetValue(markingCategory, out var markingList)
|
|
|| !markingList.Any(marking => marking.MarkingId.Contains(ent.Comp.MatchString)))
|
|
args.Cancelled = true;
|
|
}
|
|
|
|
private void OnRemoveMarkingStep(Entity<SurgeryRemoveMarkingStepComponent> ent, ref SurgeryStepEvent args)
|
|
{
|
|
|
|
}
|
|
|
|
private void OnRemoveMarkingCheck(Entity<SurgeryRemoveMarkingStepComponent> ent, ref SurgeryStepCompleteCheckEvent args)
|
|
{
|
|
|
|
}
|
|
|
|
private void OnSurgeryTargetStepChosen(Entity<SurgeryTargetComponent> ent, ref SurgeryStepChosenBuiMsg args)
|
|
{
|
|
var user = args.Actor;
|
|
if (GetEntity(args.Entity) is { } body &&
|
|
GetEntity(args.Part) is { } targetPart)
|
|
{
|
|
TryDoSurgeryStep(body, targetPart, user, args.Surgery, args.Step);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Do a surgery step on a part, if it can be done.
|
|
/// Returns true if it succeeded.
|
|
/// </summary>
|
|
public bool TryDoSurgeryStep(EntityUid body, EntityUid targetPart, EntityUid user, EntProtoId surgeryId, EntProtoId stepId)
|
|
{
|
|
if (!IsSurgeryValid(body, targetPart, surgeryId, stepId, user, out var surgery, out var part, out var step))
|
|
return false;
|
|
|
|
if (!PreviousStepsComplete(body, part, surgery, stepId) ||
|
|
IsStepComplete(body, part, stepId, surgery))
|
|
return false;
|
|
|
|
if (!CanPerformStep(user, body, part, step, true, out _, out _, out var validTools))
|
|
return false;
|
|
|
|
var speed = 1f;
|
|
var usedEv = new SurgeryToolUsedEvent(user, body);
|
|
// We need to check for nullability because of surgeries that dont require a tool, like Cavity Implants
|
|
if (validTools?.Count > 0)
|
|
{
|
|
foreach (var (tool, toolSpeed) in validTools)
|
|
{
|
|
RaiseLocalEvent(tool, ref usedEv);
|
|
if (usedEv.Cancelled)
|
|
return false;
|
|
|
|
speed *= toolSpeed;
|
|
}
|
|
|
|
if (_net.IsServer)
|
|
{
|
|
foreach (var tool in validTools.Keys)
|
|
{
|
|
if (TryComp(tool, out SurgeryToolComponent? toolComp) &&
|
|
toolComp.StartSound != null)
|
|
{
|
|
_audio.PlayPvs(toolComp.StartSound, tool);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (TryComp(body, out TransformComponent? xform))
|
|
_rotateToFace.TryFaceCoordinates(user, _transform.GetMapCoordinates(body, xform).Position);
|
|
|
|
var ev = new SurgeryDoAfterEvent(surgeryId, stepId);
|
|
var duration = GetSurgeryDuration(step, user, body, speed);
|
|
|
|
/* WWDP fix - SurgerySpeedModifier is already counted in GetSurgeryDuration
|
|
if (TryComp(user, out SurgerySpeedModifierComponent? surgerySpeedMod)
|
|
&& surgerySpeedMod is not null)
|
|
duration = duration / surgerySpeedMod.SpeedModifier;
|
|
*/
|
|
|
|
var doAfter = new DoAfterArgs(EntityManager, user, TimeSpan.FromSeconds(duration), ev, body, part)
|
|
{
|
|
BreakOnMove = true,
|
|
CancelDuplicate = true,
|
|
DuplicateCondition = DuplicateConditions.SameEvent,
|
|
NeedHand = true,
|
|
BreakOnHandChange = true,
|
|
};
|
|
|
|
if (!_doAfter.TryStartDoAfter(doAfter))
|
|
return false;
|
|
|
|
var userName = Identity.Entity(user, EntityManager);
|
|
var targetName = Identity.Entity(body, EntityManager);
|
|
|
|
var locName = $"surgery-popup-procedure-{surgeryId}-step-{stepId}";
|
|
var locResult = Loc.GetString(locName,
|
|
("user", userName), ("target", targetName), ("part", part));
|
|
|
|
if (locResult == locName)
|
|
locResult = Loc.GetString($"surgery-popup-step-{stepId}",
|
|
("user", userName), ("target", targetName), ("part", part));
|
|
|
|
_popup.PopupEntity(locResult, user);
|
|
return true;
|
|
}
|
|
|
|
private float GetSurgeryDuration(EntityUid surgeryStep, EntityUid user, EntityUid target, float toolSpeed)
|
|
{
|
|
if (!TryComp(surgeryStep, out SurgeryStepComponent? stepComp))
|
|
return 2f; // Shouldnt really happen but just a failsafe.
|
|
|
|
var speed = toolSpeed;
|
|
|
|
if (TryComp(user, out SurgerySpeedModifierComponent? surgerySpeedMod))
|
|
speed *= surgerySpeedMod.SpeedModifier;
|
|
|
|
return stepComp.Duration / speed;
|
|
}
|
|
private (Entity<SurgeryComponent> Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, Entity<SurgeryComponent?> surgery, List<EntityUid> requirements)
|
|
{
|
|
if (!Resolve(surgery, ref surgery.Comp))
|
|
return null;
|
|
|
|
if (requirements.Contains(surgery))
|
|
throw new ArgumentException($"Surgery {surgery} has a requirement loop: {string.Join(", ", requirements)}");
|
|
|
|
requirements.Add(surgery);
|
|
|
|
if (surgery.Comp.Requirement is { } requirementId &&
|
|
GetSingleton(requirementId) is { } requirement &&
|
|
GetNextStep(body, part, requirement, requirements) is { } requiredNext)
|
|
{
|
|
return requiredNext;
|
|
}
|
|
|
|
for (var i = 0; i < surgery.Comp.Steps.Count; i++)
|
|
{
|
|
var surgeryStep = surgery.Comp.Steps[i];
|
|
if (!IsStepComplete(body, part, surgeryStep, surgery))
|
|
return ((surgery, surgery.Comp), i);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public (Entity<SurgeryComponent> Surgery, int Step)? GetNextStep(EntityUid body, EntityUid part, EntityUid surgery)
|
|
{
|
|
return GetNextStep(body, part, surgery, new List<EntityUid>());
|
|
}
|
|
|
|
public bool PreviousStepsComplete(EntityUid body, EntityUid part, Entity<SurgeryComponent> surgery, EntProtoId step)
|
|
{
|
|
// TODO RMC14 use index instead of the prototype id
|
|
if (surgery.Comp.Requirement is { } requirement)
|
|
{
|
|
if (GetSingleton(requirement) is not { } requiredEnt ||
|
|
!TryComp(requiredEnt, out SurgeryComponent? requiredComp) ||
|
|
!PreviousStepsComplete(body, part, (requiredEnt, requiredComp), step))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
foreach (var surgeryStep in surgery.Comp.Steps)
|
|
{
|
|
if (surgeryStep == step)
|
|
break;
|
|
|
|
if (!IsStepComplete(body, part, surgeryStep, surgery))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public bool CanPerformStep(EntityUid user, EntityUid body, EntityUid part,
|
|
EntityUid step, bool doPopup, out string? popup, out StepInvalidReason reason,
|
|
out Dictionary<EntityUid, float>? validTools)
|
|
{
|
|
var type = BodyPartType.Other;
|
|
if (TryComp(part, out BodyPartComponent? partComp))
|
|
{
|
|
type = partComp.PartType;
|
|
}
|
|
|
|
var slot = type switch
|
|
{
|
|
BodyPartType.Head => SlotFlags.HEAD,
|
|
BodyPartType.Torso => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING,
|
|
BodyPartType.Arm => SlotFlags.OUTERCLOTHING | SlotFlags.INNERCLOTHING,
|
|
BodyPartType.Hand => SlotFlags.GLOVES,
|
|
BodyPartType.Leg => SlotFlags.OUTERCLOTHING | SlotFlags.LEGS,
|
|
BodyPartType.Foot => SlotFlags.FEET,
|
|
BodyPartType.Tail => SlotFlags.NONE,
|
|
BodyPartType.Other => SlotFlags.NONE,
|
|
_ => SlotFlags.NONE
|
|
};
|
|
|
|
var check = new SurgeryCanPerformStepEvent(user, body, GetTools(user), slot);
|
|
RaiseLocalEvent(step, ref check);
|
|
popup = check.Popup;
|
|
validTools = check.ValidTools;
|
|
|
|
if (check.Invalid != StepInvalidReason.None)
|
|
{
|
|
if (doPopup && check.Popup != null)
|
|
_popup.PopupEntity(check.Popup, user, user, PopupType.SmallCaution);
|
|
|
|
reason = check.Invalid;
|
|
return false;
|
|
}
|
|
|
|
reason = default;
|
|
return true;
|
|
}
|
|
|
|
public bool CanPerformStep(EntityUid user, EntityUid body, EntityUid part, EntityUid step, bool doPopup)
|
|
{
|
|
return CanPerformStep(user, body, part, step, doPopup, out _, out _, out _);
|
|
}
|
|
|
|
public bool IsStepComplete(EntityUid body, EntityUid part, EntProtoId step, EntityUid surgery)
|
|
{
|
|
if (GetSingleton(step) is not { } stepEnt)
|
|
return false;
|
|
|
|
var ev = new SurgeryStepCompleteCheckEvent(body, part, surgery);
|
|
RaiseLocalEvent(stepEnt, ref ev);
|
|
return !ev.Cancelled;
|
|
}
|
|
|
|
private bool AnyHaveComp(List<EntityUid> tools, IComponent component, out EntityUid withComp, out float speed)
|
|
{
|
|
foreach (var tool in tools)
|
|
{
|
|
if (EntityManager.TryGetComponent(tool, component.GetType(), out var found) && found is ISurgeryToolComponent toolComp)
|
|
{
|
|
withComp = tool;
|
|
speed = toolComp.Speed;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
withComp = EntityUid.Invalid;
|
|
speed = 1f;
|
|
return false;
|
|
}
|
|
}
|