diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
index f53711d4c3..9d244b0c34 100644
--- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
+++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
@@ -97,12 +97,12 @@
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
index b0fd603001..a8d9f32125 100644
--- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
@@ -196,19 +196,19 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt
#region Height
- CHeight.OnTextChanged += args =>
- {
- if (!float.TryParse(args.Text, out var newHeight))
- return;
-
- CHeightLabel.Text = MathF.Round(newHeight, 1).ToString("G");
- SetHeight(newHeight);
- };
-
- CHeightReset.OnPressed += _ =>
- {
- CHeight.Text = _defaultHeight.ToString(CultureInfo.InvariantCulture);
- };
+ // CHeight.OnTextChanged += args =>
+ // {
+ // if (!float.TryParse(args.Text, out var newHeight))
+ // return;
+ //
+ // CHeightLabel.Text = MathF.Round(newHeight, 1).ToString("G");
+ // SetHeight(newHeight);
+ // };
+ //
+ // CHeightReset.OnPressed += _ =>
+ // {
+ // CHeight.Text = _defaultHeight.ToString(CultureInfo.InvariantCulture);
+ // };
#endregion Height
diff --git a/Content.Server/HealthExaminable/HealthExaminableSystem.cs b/Content.Server/HealthExaminable/HealthExaminableSystem.cs
index ed69a1c096..fc0db25ab0 100644
--- a/Content.Server/HealthExaminable/HealthExaminableSystem.cs
+++ b/Content.Server/HealthExaminable/HealthExaminableSystem.cs
@@ -1,4 +1,5 @@
-using Content.Shared.Damage;
+using Content.Shared._FTL.Wounds;
+using Content.Shared.Damage;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.IdentityManagement;
@@ -22,14 +23,15 @@ private void OnGetExamineVerbs(EntityUid uid, HealthExaminableComponent componen
{
if (!TryComp(uid, out var damage))
return;
+ TryComp(uid, out var wounds);
- var detailsRange = _examineSystem.IsInDetailsRange(args.User, uid);
+ var detailsRange = _examineSystem.IsInDetailsRange(args.User, uid);
var verb = new ExamineVerb()
{
Act = () =>
{
- var markup = CreateMarkup(uid, component, damage);
+ var markup = CreateMarkup(uid, component, damage, wounds);
_examineSystem.SendExamineTooltip(args.User, uid, markup, false, false);
},
Text = Loc.GetString("health-examinable-verb-text"),
@@ -42,7 +44,7 @@ private void OnGetExamineVerbs(EntityUid uid, HealthExaminableComponent componen
args.Verbs.Add(verb);
}
- private FormattedMessage CreateMarkup(EntityUid uid, HealthExaminableComponent component, DamageableComponent damage)
+ private FormattedMessage CreateMarkup(EntityUid uid, HealthExaminableComponent component, DamageableComponent damage, WoundsHolderComponent? wounds)
{
var msg = new FormattedMessage();
@@ -57,7 +59,7 @@ private FormattedMessage CreateMarkup(EntityUid uid, HealthExaminableComponent c
FixedPoint2 closest = FixedPoint2.Zero;
- string chosenLocStr = string.Empty;
+ var chosenLocStr = string.Empty;
foreach (var threshold in component.Thresholds)
{
var str = $"health-examinable-{component.LocPrefix}-{type}-{threshold}";
@@ -88,6 +90,22 @@ private FormattedMessage CreateMarkup(EntityUid uid, HealthExaminableComponent c
msg.AddMarkup(chosenLocStr);
}
+ if (wounds != null)
+ {
+ foreach (var wound in wounds.Wounds.ContainedEntities)
+ {
+ if (!TryComp(wound, out var wComp))
+ continue;
+
+ if (!first)
+ msg.PushNewline();
+ else
+ first = false;
+
+ msg.AddMarkup(Loc.GetString(wComp.WoundExamineMessage, ("target", Identity.Entity(uid, EntityManager))));
+ }
+ }
+
if (msg.IsEmpty)
{
msg.AddMarkup(Loc.GetString($"health-examinable-{component.LocPrefix}-none"));
diff --git a/Content.Server/_FTL/Wounds/WoundThresholdComponent.cs b/Content.Server/_FTL/Wounds/WoundThresholdComponent.cs
new file mode 100644
index 0000000000..131a7473ea
--- /dev/null
+++ b/Content.Server/_FTL/Wounds/WoundThresholdComponent.cs
@@ -0,0 +1,19 @@
+namespace Content.Server._FTL.Wounds;
+
+[RegisterComponent]
+public sealed partial class WoundThresholdComponent : Component
+{
+ [DataField("thresholds")]
+ public List Thresholds = new();
+
+ public float TimeSinceLastUpdate = 0;
+}
+
+[DataDefinition]
+public partial record struct WoundThreshold
+{
+ [DataField, ViewVariables] public string Wound;
+ [DataField, ViewVariables] public string DamageType;
+ [DataField, ViewVariables] public float Probability;
+ [DataField, ViewVariables] public float Threshold;
+}
diff --git a/Content.Server/_FTL/Wounds/WoundTreatmentSystem.cs b/Content.Server/_FTL/Wounds/WoundTreatmentSystem.cs
new file mode 100644
index 0000000000..b59eef0252
--- /dev/null
+++ b/Content.Server/_FTL/Wounds/WoundTreatmentSystem.cs
@@ -0,0 +1,223 @@
+using Content.Server.DoAfter;
+using Content.Server.Popups;
+using Content.Server.Tools;
+using Content.Shared._FTL.Wounds;
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using Content.Shared.Popups;
+using Content.Shared.Tools;
+using Content.Shared.Tools.Components;
+using Content.Shared.Verbs;
+using Robust.Server.GameObjects;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Serialization;
+
+namespace Content.Server._FTL.Wounds;
+
+///
+/// This handles the treating of wounds.
+///
+///
+/// This is a shitty work around because putting this code in shared doesn't let me delete the entities.
+///
+public sealed class WoundTreatmentSystem : EntitySystem
+{
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly SharedWoundsSystem _woundsSystem = default!;
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly AudioSystem _audioSystem = default!;
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly IEntityManager _entMan = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent>(AddTreatVerb);
+ SubscribeLocalEvent(OnDoAfter);
+ }
+
+ private void OnDoAfter(EntityUid uid, WoundsHolderComponent component, WoundTreatmentDoAfterEvent args)
+ {
+ if (args.Cancelled)
+ return;
+
+ var currentWoundEntity = _entMan.GetEntity(args.Entity);
+ var user = args.Args.User;
+ var woundHolder = _entMan.GetEntity(args.WoundHolder);
+
+ if (args.Handled)
+ return;
+
+ if (!TryComp(woundHolder, out var damageable))
+ return;
+
+ var currentWound = EnsureComp(currentWoundEntity);
+
+ // If the current treatment path is more than the treatment paths available
+ // We know the last treatment path is performed, so remove the wound
+ if (currentWound.CurrentTreatmentPath < currentWound.TreatmentPaths.Count)
+ {
+ var currentPath = currentWound.TreatmentPaths[currentWound.CurrentTreatmentPath];
+ if ((currentWound.CurrentTreatmentPath - 1) > 0)
+ {
+ var prevPath = currentWound.TreatmentPaths[currentWound.CurrentTreatmentPath - 1];
+ prevPath.OnTreatmentEnd(_entMan);
+ }
+
+ var endedMsgUser = Loc.GetString(currentPath.EndedMessage);
+ var endedMsgOther = Loc.GetString(currentPath.EndedMessage + "-other", ("target", woundHolder));
+
+ // Actually increment it here since we're finished
+ currentWound.CurrentTreatmentPath += 1;
+ Dirty(currentWoundEntity, currentWound);
+
+ if (currentWound.CurrentTreatmentPath < currentWound.TreatmentPaths.Count)
+ {
+ _popupSystem.PopupEntity(endedMsgUser, woundHolder, user);
+ _popupSystem.PopupEntity(endedMsgOther, woundHolder, Filter.PvsExcept(user), true);
+ return;
+ }
+ }
+
+ _popupSystem.PopupEntity(Loc.GetString("popup-wound-cured", ("target", uid), ("woundName", MetaData(currentWoundEntity).EntityName)), uid);
+ QueueDel(currentWoundEntity);
+ _damageableSystem.DamageChanged(uid, damageable);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var entity, out var thresholdComponent, out var woundsHolderComponent, out var damageableComponent))
+ {
+ thresholdComponent.TimeSinceLastUpdate += frameTime;
+
+ if (thresholdComponent.TimeSinceLastUpdate < 5)
+ continue; // run this every 5 seconds
+
+ thresholdComponent.TimeSinceLastUpdate = 0;
+
+ foreach (var threshold in thresholdComponent.Thresholds)
+ {
+ if (!damageableComponent.Damage.DamageDict.TryGetValue(threshold.DamageType, out var damageAmount))
+ continue;
+
+ if (damageAmount < threshold.Threshold)
+ continue;
+
+ if (!_random.Prob(threshold.Probability))
+ continue;
+
+ _woundsSystem.TryAddWound(threshold.Wound, entity, woundsHolderComponent);
+ }
+ }
+ }
+
+ private void AddTreatVerb(EntityUid uid, WoundsHolderComponent component, GetVerbsEvent args)
+ {
+ if (!args.CanInteract)
+ return;
+
+ if (component.Wounds.ContainedEntities.Count <= 0)
+ return; // why show a treat menu when theres nothing to treat
+
+ for (var i = 0; i < component.Wounds.ContainedEntities.Count; i++)
+ {
+ var wound = component.Wounds.ContainedEntities[i];
+
+ if (!TryComp(wound, out _))
+ return;
+
+ var meta = MetaData(wound);
+
+ var i1 = i;
+ args.Verbs.Add(new AlternativeVerb
+ {
+ Text = $"{meta.EntityName}",
+ Act = () =>
+ {
+ component.CurrentWoundTreating = i1;
+ },
+ Disabled = i == component.CurrentWoundTreating,
+ Category = VerbCategory.SelectWound
+ });
+ }
+
+ // Hamlet? From Don't Starve Hamlet (DLC?)
+ // gardeners are RUINING ss14
+ // i do not want to hear it... EVER
+ // -flare
+
+ // Get the currently selected wound
+ var currentWoundEntity = component.Wounds.ContainedEntities[component.CurrentWoundTreating];
+ if (!TryComp(currentWoundEntity, out var currentWound))
+ return; // If it doesnt have a wound comp wtf is it doing here???
+
+ // Get the current treatment path
+ var currentPath = currentWound.TreatmentPaths[currentWound.CurrentTreatmentPath];
+
+ if (args.Hands == null)
+ {
+ _popupSystem.PopupClient(Loc.GetString("popup-wound-need-hand"), uid, args.User);
+ return; // you need hands
+ }
+
+ if (args.Hands.ActiveHand == null)
+ {
+ _popupSystem.PopupClient(Loc.GetString("popup-wound-need-hand"), uid, args.User);
+ return; // you need A hand at least
+ }
+
+ var quality = _prototypeManager.Index(currentPath.ToolQuality.Id);
+
+ args.Verbs.Add(new AlternativeVerb
+ {
+ Text = currentPath.GetVerbText(currentWound),
+ Act = () =>
+ {
+ var currentlyHeld = args.Hands?.ActiveHand?.HeldEntity;
+ if (!currentlyHeld.HasValue)
+ return;
+
+ if (!currentPath.TreatmentCheck(_entMan, _entMan.GetNetEntity(currentlyHeld.Value)))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("popup-wound-need-item", ("item", Loc.GetString(quality.Name))), uid);
+ return;
+ }
+
+
+ var tool = EnsureComp(currentlyHeld.Value);
+
+ _audioSystem.PlayPvs(currentWound.TreatmentPaths[currentWound.CurrentTreatmentPath].TreatmentSound, uid);
+
+ var startedMsgUser = Loc.GetString(currentPath.BeganMessage);
+ var startedMsgOther = Loc.GetString(currentPath.BeganMessage + "-other", ("target", args.User));
+
+ _popupSystem.PopupEntity(startedMsgUser, uid, args.User);
+ _popupSystem.PopupEntity(startedMsgOther, uid, Filter.PvsExcept(args.User), true);
+
+ var ev = new WoundTreatmentDoAfterEvent(
+ _entMan.GetNetEntity(currentWoundEntity),
+ _entMan.GetNetEntity(uid)
+ );
+
+ _doAfterSystem.TryStartDoAfter(new DoAfterArgs(_entMan, args.User, TimeSpan.FromSeconds(currentPath.TreatmentLength * tool.SpeedModifier), ev, uid)
+ {
+ BreakOnHandChange = true,
+ BreakOnDamage = true,
+ BreakOnWeightlessMove = true,
+ //BreakOnTargetMove = true, // idk why upstream doesnt allow this
+ BreakOnUserMove = true,
+ NeedHand = true
+ });
+ }
+ });
+ }
+}
diff --git a/Content.Shared/Damage/DamageModifierSet.cs b/Content.Shared/Damage/DamageModifierSet.cs
index bd074ec30f..f39d815a5b 100644
--- a/Content.Shared/Damage/DamageModifierSet.cs
+++ b/Content.Shared/Damage/DamageModifierSet.cs
@@ -24,5 +24,8 @@ public partial class DamageModifierSet
[DataField("flatReductions", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
public Dictionary FlatReduction = new();
+
+ [DataField("woundReduction")] // TODO: Flat reductions
+ public float WoundCoefficient = 0;
}
}
diff --git a/Content.Shared/Damage/DamageSpecifier.cs b/Content.Shared/Damage/DamageSpecifier.cs
index b7181e297f..d2140d8abe 100644
--- a/Content.Shared/Damage/DamageSpecifier.cs
+++ b/Content.Shared/Damage/DamageSpecifier.cs
@@ -37,6 +37,14 @@ public sealed partial class DamageSpecifier : IEquatable
[IncludeDataField(customTypeSerializer: typeof(DamageSpecifierDictionarySerializer), readOnly: true)]
public Dictionary DamageDict { get; set; } = new();
+ ///
+ /// What wounds should be (and their probability of being) applied?
+ ///
+ [JsonIgnore]
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("wounds", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))]
+ public Dictionary? Wounds;
+
[JsonIgnore]
[Obsolete("Use GetTotal()")]
public FixedPoint2 Total => GetTotal();
@@ -159,6 +167,16 @@ public static DamageSpecifier ApplyModifierSet(DamageSpecifier damageSpec, Damag
newDamage.DamageDict[key] = FixedPoint2.New(newValue);
}
+ if (damageSpec.Wounds == null)
+ return newDamage;
+
+ newDamage.Wounds = new Dictionary();
+
+ foreach (var (key, value) in damageSpec.Wounds)
+ {
+ newDamage.Wounds[key] = value * modifierSet.WoundCoefficient;
+ }
+
return newDamage;
}
diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs
index 8f6ccc20e6..2f22743273 100644
--- a/Content.Shared/Damage/Systems/DamageableSystem.cs
+++ b/Content.Shared/Damage/Systems/DamageableSystem.cs
@@ -1,4 +1,5 @@
using System.Linq;
+using Content.Shared._FTL.Wounds;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
@@ -9,6 +10,7 @@
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Shared.Damage
@@ -19,6 +21,8 @@ public sealed class DamageableSystem : EntitySystem
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly MobThresholdSystem _mobThreshold = default!;
+ [Dependency] private readonly SharedWoundsSystem _sharedWoundsSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
private EntityQuery _appearanceQuery;
private EntityQuery _damageableQuery;
@@ -42,7 +46,7 @@ private void DamageableInit(EntityUid uid, DamageableComponent component, Compon
{
if (component.DamageContainerID != null &&
_prototypeManager.TryIndex(component.DamageContainerID,
- out var damageContainerPrototype))
+ out var damageContainerPrototype))
{
// Initialize damage dictionary, using the types and groups from the damage
// container prototype
@@ -96,8 +100,17 @@ public void SetDamage(EntityUid uid, DamageableComponent damageable, DamageSpeci
public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null,
bool interruptsDoAfters = true, EntityUid? origin = null)
{
- component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup);
- component.TotalDamage = component.Damage.GetTotal();
+ DamageSpecifier dmg = new (component.Damage);
+ if (TryComp(uid, out var wounds))
+ {
+ if (_sharedWoundsSystem.TryGetDamageFromWounds(uid, wounds, out var spec))
+ {
+ dmg = spec + dmg;
+ }
+ }
+
+ dmg.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup);
+ component.TotalDamage = dmg.GetTotal();
Dirty(uid, component);
if (_appearanceQuery.TryGetComponent(uid, out var appearance) && damageDelta != null)
@@ -185,6 +198,19 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp
if (delta.DamageDict.Count > 0)
DamageChanged(uid.Value, damageable, delta, interruptsDoAfters, origin);
+ if (damage.Wounds == null || !TryComp(uid, out var woundsHolderComponent))
+ return delta;
+
+ // ...Maybe *don't* add wounds based on pure random chance?
+ foreach (var wound in damage.Wounds.Keys)
+ {
+ var prob = damage.Wounds[wound];
+ if (_random.Prob(prob))
+ {
+ _sharedWoundsSystem.TryAddWound(wound, uid.Value);
+ }
+ }
+
return delta;
}
diff --git a/Content.Shared/Verbs/VerbCategory.cs b/Content.Shared/Verbs/VerbCategory.cs
index d22041396f..a97b85a973 100644
--- a/Content.Shared/Verbs/VerbCategory.cs
+++ b/Content.Shared/Verbs/VerbCategory.cs
@@ -83,5 +83,7 @@ public VerbCategory(string text, string? icon, bool iconsOnly = false)
public static readonly VerbCategory Lever = new("verb-categories-lever", null);
public static readonly VerbCategory SelectType = new("verb-categories-select-type", null);
+
+ public static readonly VerbCategory SelectWound = new("verb-categories-select-wound", null);
}
}
diff --git a/Content.Shared/_FTL/Wounds/BaseTreatmentPath.cs b/Content.Shared/_FTL/Wounds/BaseTreatmentPath.cs
new file mode 100644
index 0000000000..34773110fa
--- /dev/null
+++ b/Content.Shared/_FTL/Wounds/BaseTreatmentPath.cs
@@ -0,0 +1,49 @@
+using Content.Shared.Tools;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._FTL.Wounds;
+
+[DataDefinition, Serializable, NetSerializable]
+public abstract partial class BaseTreatmentPath
+{
+ ///
+ /// The quality required to start treatment.
+ ///
+ [ViewVariables, DataField("quality")]
+ public ProtoId ToolQuality = "Prying";
+
+ ///
+ /// How long does it take to perform this (seconds)?
+ ///
+ [ViewVariables, DataField("length")]
+ public float TreatmentLength = 3;
+
+ ///
+ /// The sound played when treatment is began
+ ///
+ [DataField("grabSound")]
+ public SoundSpecifier TreatmentSound = new SoundPathSpecifier("/Audio/Effects/chop.ogg");
+
+ [DataField("beginMessage"), ViewVariables]
+ public LocId BeganMessage = "popup-wound-generic-began";
+
+ [DataField("endMessage"), ViewVariables]
+ public LocId EndedMessage = "popup-wound-generic-ended";
+
+ public virtual bool TreatmentCheck(IEntityManager entMan, NetEntity activeHand)
+ {
+ return true;
+ }
+
+ public virtual void OnTreatmentEnd(IEntityManager entityManager)
+ {
+ // noop
+ }
+
+ public virtual string GetVerbText(WoundComponent currentWound)
+ {
+ return "Treat current wound";
+ }
+}
diff --git a/Content.Shared/_FTL/Wounds/SharedWoundsSystem.cs b/Content.Shared/_FTL/Wounds/SharedWoundsSystem.cs
new file mode 100644
index 0000000000..2713ebf87b
--- /dev/null
+++ b/Content.Shared/_FTL/Wounds/SharedWoundsSystem.cs
@@ -0,0 +1,121 @@
+using Content.Shared.Damage;
+using Content.Shared.DoAfter;
+using JetBrains.Annotations;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared._FTL.Wounds;
+
+[Serializable, NetSerializable]
+public sealed partial class WoundTreatmentDoAfterEvent : DoAfterEvent
+{
+ public NetEntity Entity;
+ public NetEntity WoundHolder;
+
+ public WoundTreatmentDoAfterEvent(NetEntity entity, NetEntity woundHolder)
+ {
+ Entity = entity;
+ WoundHolder = woundHolder;
+ }
+
+ public override DoAfterEvent Clone() => this;
+}
+
+///
+/// Handles the adding and managing of wounds.
+///
+public sealed class SharedWoundsSystem : EntitySystem
+{
+ [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ public const string ContainerName = "wounds";
+
+ // TODO: Integrate ALL of this with events
+
+ ///
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnComponentInit);
+ }
+
+ private void OnComponentInit(EntityUid uid, WoundsHolderComponent component, ComponentInit args)
+ {
+ component.Wounds = _containerSystem.EnsureContainer(uid, ContainerName);
+ }
+
+ ///
+ /// Attempts to add a wound to an entity.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public bool TryAddWound(EntProtoId woundPrototype, EntityUid uid, WoundsHolderComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return false;
+
+ var entityId = woundPrototype.Id;
+ if (component.Wounds.ContainedEntities.FirstOrNull(e => MetaData(e).EntityPrototype?.ID == entityId) != null)
+ return false; // more than one...??!??g
+
+ var wound = Spawn(woundPrototype);
+ component.Wounds.Insert(wound);
+
+ if (!TryComp(wound, out var woundComponent))
+ {
+ Log.Error($"Expected wound prototype ${woundPrototype} to have WoundComponent.");
+ }
+ else if (TryComp(uid, out var damageableComponent))
+ {
+ _damageableSystem.DamageChanged(uid, damageableComponent, woundComponent.Damage, true, null);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Gets the total damage of all wounds from a WHC.
+ ///
+ ///
+ ///
+ [PublicAPI]
+ public DamageSpecifier GetDamageFromWounds(WoundsHolderComponent component)
+ {
+ var damage = new DamageSpecifier();
+
+ foreach (var entity in component.Wounds.ContainedEntities)
+ {
+ var wound = EnsureComp(entity);
+ if (wound.Damage != null)
+ damage = wound.Damage + damage;
+ }
+
+ return damage;
+ }
+
+ ///
+ /// Attempts to get damage from wounds given an entity.
+ ///
+ ///
+ ///
+ ///
+ ///
+ [PublicAPI]
+ public bool TryGetDamageFromWounds(EntityUid uid, WoundsHolderComponent? component, out DamageSpecifier spec)
+ {
+ if (!Resolve(uid, ref component, false))
+ {
+ spec = new DamageSpecifier();
+ return false;
+ }
+
+ spec = GetDamageFromWounds(component);
+
+ return true;
+ }
+}
diff --git a/Content.Shared/_FTL/Wounds/ToolTreatmentPath.cs b/Content.Shared/_FTL/Wounds/ToolTreatmentPath.cs
new file mode 100644
index 0000000000..fef787c580
--- /dev/null
+++ b/Content.Shared/_FTL/Wounds/ToolTreatmentPath.cs
@@ -0,0 +1,24 @@
+using Content.Shared.Tools;
+using Content.Shared.Tools.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._FTL.Wounds;
+
+[DataDefinition]
+public sealed partial class ToolTreatmentPath : BaseTreatmentPath
+{
+ public override bool TreatmentCheck(IEntityManager entMan, NetEntity activeHand)
+ {
+ var currentlyHeld = entMan.GetEntity(activeHand);
+ var toolSystem = entMan.System();
+ var quality = IoCManager.Resolve().Index(ToolQuality.Id);
+
+ return toolSystem.HasQuality(currentlyHeld, quality.ID);
+ }
+
+ public override string GetVerbText(WoundComponent currentWound)
+ {
+ return $"Treat current wound ({currentWound.CurrentTreatmentPath}/{currentWound.TreatmentPaths.Count})";
+ }
+}
diff --git a/Content.Shared/_FTL/Wounds/WoundComponent.cs b/Content.Shared/_FTL/Wounds/WoundComponent.cs
new file mode 100644
index 0000000000..b9cbcb268a
--- /dev/null
+++ b/Content.Shared/_FTL/Wounds/WoundComponent.cs
@@ -0,0 +1,40 @@
+using Content.Shared.Damage;
+using Content.Shared.Tools;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._FTL.Wounds;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Serializable]
+public sealed partial class WoundComponent : Component
+{
+ ///
+ /// How much damage does this wound do permanently?
+ ///
+ [DataField, ViewVariables] public DamageSpecifier? Damage;
+
+ ///
+ /// How severe is this wound?
+ ///
+ [DataField, ViewVariables] public float Severity = 1f;
+
+ ///
+ /// What does it say when this wound is examined?
+ ///
+ [DataField, ViewVariables] public LocId WoundExamineMessage;
+
+ ///
+ /// What current treatment path are we on?
+ ///
+ [DataField, ViewVariables, AutoNetworkedField]
+ public int CurrentTreatmentPath;
+
+ ///
+ /// A list of treatment paths.
+ ///
+ [ViewVariables(VVAccess.ReadWrite), DataField("paths", required: true)]
+ public List TreatmentPaths = new();
+}
diff --git a/Content.Shared/_FTL/Wounds/WoundsHolderComponent.cs b/Content.Shared/_FTL/Wounds/WoundsHolderComponent.cs
new file mode 100644
index 0000000000..1b078993be
--- /dev/null
+++ b/Content.Shared/_FTL/Wounds/WoundsHolderComponent.cs
@@ -0,0 +1,12 @@
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared._FTL.Wounds;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class WoundsHolderComponent : Component
+{
+ [ViewVariables] public Container Wounds = default!;
+ [ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+ public int CurrentWoundTreating = 0;
+}
diff --git a/Resources/Locale/en-US/_ftl/wounds.ftl b/Resources/Locale/en-US/_ftl/wounds.ftl
new file mode 100644
index 0000000000..0675ed2a15
--- /dev/null
+++ b/Resources/Locale/en-US/_ftl/wounds.ftl
@@ -0,0 +1,49 @@
+health-examinable-debug-wound = [color=green]{ CAPITALIZE(SUBJECT($target)) } { CONJUGATE-HAVE($target) } ones and zeros coming out of { POSS-ADJ($target) } body![/color]
+
+health-examinable-laceration-wound = { CAPITALIZE(SUBJECT($target)) } { CONJUGATE-HAVE($target) } a really bad looking [color=red]laceration[/color] on { POSS-ADJ($target) } body![/color]
+health-examinable-abrasion-wound = { CAPITALIZE(SUBJECT($target)) } { CONJUGATE-HAVE($target) } a [color=red]large scrape[/color] across { POSS-ADJ($target) } body ![/color]
+health-examinable-puncture-wound = { CAPITALIZE(SUBJECT($target)) } { CONJUGATE-HAVE($target) } [color=red]holes[/color] all across { POSS-ADJ($target) } body![/color]
+health-examinable-gunshot-wound = { CAPITALIZE(SUBJECT($target)) } { CONJUGATE-HAVE($target) } a [color=red]bullet[/color] lodged in { POSS-ADJ($target) } body![/color]
+health-examinable-fracture-wound = { CAPITALIZE(SUBJECT($target)) } shouldn't have { POSS-ADJ($target) } [color=red]bones[/color] looking like that...[/color]
+health-examinable-sprain-wound = { CAPITALIZE(SUBJECT($target)) } { CONJUGATE-HAVE($target) } { POSS-ADJ($target) } [color=red]foot bending[/color] really weirdly...[/color]
+health-examinable-burns-wound = { CAPITALIZE(SUBJECT($target)) } { CONJUGATE-HAVE($target) } really bad looking [color=orange]burns[color] all across { POSS-ADJ($target) } body!
+
+popup-wound-cured = { CAPITALIZE(POSS-ADJ($target)) } {$woundName} is healed.
+popup-wound-need-item = You need to hold an item that can perform {$item} in your hand!
+popup-wound-need-hand = You need a hand!
+verb-categories-select-wound = Select Wound
+
+popup-wound-clamping-began = You begin to clamp the wound...
+popup-wound-clamping-began-other = { CAPITALIZE(SUBJECT($target)) } begins to clamps the wound...
+
+popup-wound-incision-began = You begin to create an incision...
+popup-wound-incision-began-other = { CAPITALIZE(SUBJECT($target)) } creates an incision...
+
+popup-wound-sow-began = You begin to sow the wound together...
+popup-wound-sow-began-other = { CAPITALIZE(SUBJECT($target)) } sows the wound together...
+
+popup-wound-seal-began = You begin to seal the wound...
+popup-wound-seal-began-other = { CAPITALIZE(SUBJECT($target)) } seals the wound...
+
+popup-wound-bone-cutting-began = You begin cutting through the bone...
+popup-wound-bone-cutting-began-other = { CAPITALIZE(SUBJECT($target)) } cuts through the bone...
+
+popup-wound-retractor-began = You begin to retract the skin...
+popup-wound-retractor-began-other = { CAPITALIZE(SUBJECT($target)) } retracts the skin...
+
+popup-wound-bullet-removal-began = You begin to remove the bullet...
+popup-wound-bullet-removal-began-other = { CAPITALIZE(SUBJECT($target)) } removes the bullet...
+
+popup-wound-bone-set-began = You begin setting the bone...
+popup-wound-bone-set-began-other = { CAPITALIZE(SUBJECT($target)) } sets the bone...
+
+popup-wound-seal-began = You begin to seal the wound...
+popup-wound-seal-began-other = { CAPITALIZE(SUBJECT($target)) } seals the wound...
+
+popup-wound-sow-attempt-began = You try to connect the skin together...
+popup-wound-sow-attempt-began-other = { CAPITALIZE(SUBJECT($target)) } does their best to connect the skin...
+
+popup-wound-generic-began = You begin treating the wound...
+popup-wound-generic-began-other = { CAPITALIZE(SUBJECT($target)) } begins treating the wound...
+popup-wound-generic-ended = You finish treating the wound.
+popup-wound-generic-ended-other = { CAPITALIZE(SUBJECT($target)) } finishes treating the wound.
diff --git a/Resources/Prototypes/Entities/Mobs/base.yml b/Resources/Prototypes/Entities/Mobs/base.yml
index 2b4057eadf..aba9a4223b 100644
--- a/Resources/Prototypes/Entities/Mobs/base.yml
+++ b/Resources/Prototypes/Entities/Mobs/base.yml
@@ -52,12 +52,31 @@
components:
- type: Damageable
damageContainer: Biological
+ - type: WoundsHolder
+ - type: WoundThreshold
+ thresholds:
+ - wound: WoundLaceration
+ damageType: Slash
+ probability: 1
+ threshold: 35
+ - wound: WoundAbrasion
+ damageType: Blunt
+ probability: 1
+ threshold: 25
+ - wound: WoundFracture
+ damageType: Blunt
+ probability: 1
+ threshold: 65
+ - wound: WoundPuncture
+ damageType: Pierce
+ probability: 1
+ threshold: 50
- type: ConstantDamage
probability: .5
checkFrequency: 60
damage:
types:
- VeilIndividualExposure: 3
+ VeilIndividualExposure: 1
- type: Destructible
thresholds:
- trigger:
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml
index 354cee2f99..46a5233623 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/surgery.yml
@@ -15,7 +15,7 @@
name: cautery
id: Cautery
parent: BaseToolSurgery
- description: A surgical tool used to cauterize open wounds.
+ description: A surgical tool used to cauterize open wounds. Welding under surgery.
components:
- type: Sprite
sprite: Objects/Specific/Medical/Surgery/cautery.rsi
@@ -23,6 +23,10 @@
- type: Item
sprite: Objects/Specific/Medical/Surgery/cautery.rsi
- type: ItemCooldown
+ - type: Tool
+ speed: .04
+ qualities:
+ - Welding
- type: MeleeWeapon
damage:
types:
@@ -36,7 +40,7 @@
name: drill
id: Drill
parent: BaseToolSurgery
- description: A surgical drill for making holes into hard material.
+ description: A surgical drill for making holes into hard material. Cutting under surgery.
components:
- type: Sprite
sprite: Objects/Specific/Medical/Surgery/drill.rsi
@@ -44,6 +48,10 @@
- type: Item
sprite: Objects/Specific/Medical/Surgery/drill.rsi
- type: ItemCooldown
+ - type: Tool
+ speed: .04
+ qualities:
+ - Cutting
- type: MeleeWeapon
damage:
types:
@@ -57,13 +65,17 @@
name: scalpel
id: Scalpel
parent: BaseToolSurgery
- description: A surgical tool used to make incisions into flesh.
+ description: A surgical tool used to make incisions into flesh. Slicing under surgery.
components:
- type: Sharp
butcherDelayModifier: 1.5 # Butchering with a scalpel, regardless of the type, will take 50% longer
- type: Utensil
types:
- Knife
+ - type: Tool
+ speed: .04
+ qualities:
+ - Slicing
- type: Sprite
sprite: Objects/Specific/Medical/Surgery/scalpel.rsi
state: scalpel
@@ -82,18 +94,20 @@
name: shiv
id: ScalpelShiv
parent: Scalpel
- description: A pointy piece of glass, abraded to an edge and wrapped in tape for a handle. # Could become a decent tool or weapon with right tool mods.
+ description: A pointy piece of glass, abraded to an edge and wrapped in tape for a handle. Slicing under surgery. # Could become a decent tool or weapon with right tool mods.
components:
- type: Sprite
state: shiv
- type: Item
heldPrefix: shiv
+ - type: Tool
+ speed: 0.5
- type: entity
name: advanced scalpel
id: ScalpelAdvanced
parent: Scalpel
- description: Made of more expensive materials, sharper and generally more reliable.
+ description: Made of more expensive materials, sharper and generally more reliable. Slicing under surgery.
components:
- type: Sprite
state: advanced
@@ -103,17 +117,21 @@
damage:
types:
Slash: 12
+ - type: Tool
+ speed: .06
- type: entity
name: laser scalpel
id: ScalpelLaser
parent: Scalpel
- description: A scalpel which uses a directed laser to slice instead of a blade, for more precise surgery while also cauterizing as it cuts.
+ description: A scalpel which uses a directed laser to slice instead of a blade, for more precise surgery while also cauterizing as it cuts. Slicing under surgery.
components:
- type: Sprite
state: laser
- type: Item
heldPrefix: laser
+ - type: Tool
+ speed: .08
# Scissors
@@ -121,7 +139,7 @@
name: retractor
id: Retractor
parent: BaseToolSurgery
- description: A surgical tool used to hold open incisions.
+ description: A surgical tool used to hold open incisions. Cutting under surgery.
components:
- type: Sprite
sprite: Objects/Specific/Medical/Surgery/scissors.rsi
@@ -129,6 +147,10 @@
- type: Item
sprite: Objects/Specific/Medical/Surgery/scissors.rsi
- type: ItemCooldown
+ - type: Tool
+ speed: .04
+ qualities:
+ - Cutting
- type: entity
name: hemostat
@@ -153,7 +175,7 @@
name: metal saw
id: Saw
parent: BaseToolSurgery
- description: For cutting wood and other objects to pieces. Or sawing bones, in case of emergency.
+ description: For cutting wood and other objects to pieces. Or sawing bones, in case of emergency. Sawing under surgery.
components:
- type: Sharp
- type: Utensil
@@ -168,14 +190,14 @@
- type: Tool
qualities:
- Sawing
- speed: 1.0
+ speed: .1
# No melee for regular saw because have you ever seen someone use a band saw as a weapon? It's dumb.
- type: entity
name: choppa
id: SawImprov
parent: Saw
- description: A wicked serrated blade made of whatever nasty sharp things you could find. # It would make a pretty decent weapon, given there are more space for some tool mods too.
+ description: A wicked serrated blade made of whatever nasty sharp things you could find. Sawing under surgery. # It would make a pretty decent weapon, given there are more space for some tool mods too.
components:
- type: Sprite
state: improv
@@ -190,13 +212,13 @@
- type: Tool
qualities:
- Sawing
- speed: 0.5
+ speed: .50
- type: entity
name: circular saw
id: SawElectric
parent: Saw
- description: For heavy duty cutting.
+ description: For heavy duty cutting. Sawing under surgery.
components:
- type: Sprite
state: electric
@@ -211,13 +233,13 @@
- type: Tool
qualities:
- Sawing
- speed: 1.5
+ speed: .54
- type: entity
name: advanced circular saw
id: SawAdvanced
parent: Saw
- description: You think you can cut anything with it.
+ description: You think you can cut anything with it. Sawing under surgery.
components:
- type: Sprite
state: advanced
@@ -233,4 +255,4 @@
- type: Tool
qualities:
- Sawing
- speed: 2.0
+ speed: .07
diff --git a/Resources/Prototypes/_FTL/Entities/Objects/Weapons/Guns/Projectiles/ap.yml b/Resources/Prototypes/_FTL/Entities/Objects/Weapons/Guns/Projectiles/ap.yml
index 80938caa60..4d5b2e427b 100644
--- a/Resources/Prototypes/_FTL/Entities/Objects/Weapons/Guns/Projectiles/ap.yml
+++ b/Resources/Prototypes/_FTL/Entities/Objects/Weapons/Guns/Projectiles/ap.yml
@@ -13,7 +13,9 @@
types:
Structural: 1000
groups:
- Brute: 1000
+ Brute: 75
+ wounds:
+ WoundGunshot: 1
- type: entity
id: Bullet53mmAP
@@ -30,7 +32,7 @@
types:
Structural: 1500
groups:
- Brute: 2500
+ Brute: 250
- type: entity
id: Bullet80mmAP
@@ -47,7 +49,7 @@
types:
Structural: 2000
groups:
- Brute: 5000
+ Brute: 500
- type: entity
id: Bullet105mmAP
@@ -64,7 +66,7 @@
types:
Structural: 2500
groups:
- Brute: 8000
+ Brute: 800
- type: entity
id: Bullet120mmAP
@@ -81,7 +83,7 @@
types:
Structural: 10000
groups:
- Brute: 9000
+ Brute: 900
- type: entity
id: Bullet140mmAP
@@ -98,4 +100,4 @@
types:
Structural: 5000
groups:
- Brute: 10000
+ Brute: 1000
diff --git a/Resources/Prototypes/_FTL/Entities/Wounds/debug.yml b/Resources/Prototypes/_FTL/Entities/Wounds/debug.yml
new file mode 100644
index 0000000000..953aa8f949
--- /dev/null
+++ b/Resources/Prototypes/_FTL/Entities/Wounds/debug.yml
@@ -0,0 +1,14 @@
+- type: entity
+ id: WoundDebug
+ name: debug wound
+ description: It's a wound that glows green with symbols that look mesmerizing, like 1s and 0s.
+ components:
+ - type: Wound
+ woundExamineMessage: health-examinable-debug-wound
+ paths:
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ length: 6
+ damage:
+ types:
+ Blunt: 30
diff --git a/Resources/Prototypes/_FTL/Entities/Wounds/wounds.yml b/Resources/Prototypes/_FTL/Entities/Wounds/wounds.yml
new file mode 100644
index 0000000000..03b4ddb666
--- /dev/null
+++ b/Resources/Prototypes/_FTL/Entities/Wounds/wounds.yml
@@ -0,0 +1,135 @@
+- type: entity
+ id: WoundLaceration
+ name: laceration
+ components:
+ - type: Wound
+ woundExamineMessage: health-examinable-laceration-wound
+ paths:
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-clamping-began
+ length: 12
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-sow-began
+ length: 20
+ damage:
+ types:
+ Slash: 35
+
+- type: entity
+ id: WoundAbrasion
+ name: abrasion
+ components:
+ - type: Wound
+ woundExamineMessage: health-examinable-abrasion-wound
+ paths:
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-clamping-began
+ length: 12
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-sow-attempt-began
+ length: 20
+ damage:
+ types:
+ Blunt: 25
+
+- type: entity
+ id: WoundPuncture
+ name: puncture
+ components:
+ - type: Wound
+ woundExamineMessage: health-examinable-puncture-wound
+ paths:
+ - !type:ToolTreatmentPath
+ quality: Slicing
+ beginMessage: popup-wound-incision-began
+ length: 8
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-retractor-began
+ length: 4
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-clamping-began
+ length: 4
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-sow-began
+ length: 7
+ - !type:ToolTreatmentPath
+ quality: Welding
+ beginMessage: popup-wound-seal-began
+ length: 5
+ damage:
+ types:
+ Pierce: 35
+
+- type: entity
+ id: WoundGunshot
+ name: gunshot
+ components:
+ - type: Wound
+ woundExamineMessage: health-examinable-gunshot-wound
+ paths:
+ # Process: Incision -> Retract skin -> Saw -> Clamp bleeders -> Remove bullet -> Seal
+ - !type:ToolTreatmentPath
+ quality: Slicing
+ beginMessage: popup-wound-incision-began
+ length: 8
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-retractor-began
+ length: 4
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-clamping-began
+ length: 4
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-bullet-removal-began
+ length: 16
+ - !type:ToolTreatmentPath
+ quality: Welding
+ beginMessage: popup-wound-seal-began
+ length: 8
+ damage:
+ types:
+ Pierce: 45
+
+- type: entity
+ id: WoundFracture
+ name: fracture
+ components:
+ - type: Wound
+ woundExamineMessage: health-examinable-fracture-wound
+ paths:
+ - !type:ToolTreatmentPath
+ quality: Slicing
+ beginMessage: popup-wound-incision-began
+ length: 8
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-retractor-began
+ length: 4
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-clamping-began
+ length: 4
+ - !type:ToolTreatmentPath
+ quality: Sawing
+ beginMessage: popup-wound-bone-cutting-began
+ length: 7
+ - !type:ToolTreatmentPath
+ quality: Cutting
+ beginMessage: popup-wound-bone-set-began
+ length: 16
+ - !type:ToolTreatmentPath
+ quality: Welding
+ beginMessage: popup-wound-seal-began
+ length: 8
+ damage:
+ types:
+ Pierce: 45