diff --git a/Content.Client/Overlays/EquipmentHudSystem.cs b/Content.Client/Overlays/EquipmentHudSystem.cs index 15b4ce1e026..9ea208af12c 100644 --- a/Content.Client/Overlays/EquipmentHudSystem.cs +++ b/Content.Client/Overlays/EquipmentHudSystem.cs @@ -14,6 +14,7 @@ public abstract class EquipmentHudSystem : EntitySystem where T : IComponent { [Dependency] private readonly IPlayerManager _player = default!; + [ViewVariables] protected bool IsActive; protected virtual SlotFlags TargetSlots => ~SlotFlags.POCKET; @@ -32,8 +33,8 @@ public override void Initialize() SubscribeLocalEvent>(OnRefreshComponentHud); SubscribeLocalEvent>>(OnRefreshEquipmentHud); -/* - SubscribeLocalEvent(OnRoundRestart); */ + + SubscribeLocalEvent(OnRoundRestart); } private void Update(RefreshEquipmentHudEvent ev) @@ -86,14 +87,17 @@ private void OnCompUnequip(Entity ent, ref GotUnequippedEvent args) RefreshOverlay(); } -/* private void OnRoundRestart(RoundRestartCleanupEvent args) + private void OnRoundRestart(RoundRestartCleanupEvent args) { Deactivate(); - } */ + } protected virtual void OnRefreshEquipmentHud(Entity ent, ref InventoryRelayedEvent> args) { - OnRefreshComponentHud(ent, ref args.Args); + // Goob edit start + args.Args.Active = true; + args.Args.Components.Add(ent); + // Goob edit end } protected virtual void OnRefreshComponentHud(Entity ent, ref RefreshEquipmentHudEvent args) diff --git a/Content.Client/_White/Overlays/BaseSwitchableOverlay.cs b/Content.Client/_White/Overlays/BaseSwitchableOverlay.cs new file mode 100644 index 00000000000..d904401e5f6 --- /dev/null +++ b/Content.Client/_White/Overlays/BaseSwitchableOverlay.cs @@ -0,0 +1,48 @@ +using System.Numerics; +using Content.Shared._White.Overlays; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; + +namespace Content.Client._White.Overlays; + +public sealed class BaseSwitchableOverlay : Overlay where TComp : SwitchableVisionOverlayComponent +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + private readonly ShaderInstance _shader; + + public TComp? Comp = null; + + public bool IsActive = true; + + public BaseSwitchableOverlay() + { + IoCManager.InjectDependencies(this); + _shader = _prototype.Index("NVHud").InstanceUnique(); + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture is null || Comp is null || !IsActive) + return; + + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _shader.SetParameter("tint", Comp.Tint); + _shader.SetParameter("luminance_threshold", Comp.Strength); + _shader.SetParameter("noise_amount", Comp.Noise); + + var worldHandle = args.WorldHandle; + + var accumulator = Math.Clamp(Comp.PulseAccumulator, 0f, Comp.PulseTime); + var alpha = Comp.PulseTime <= 0f ? 1f : float.Lerp(1f, 0f, accumulator / Comp.PulseTime); + + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.UseShader(_shader); + worldHandle.DrawRect(args.WorldBounds, Comp.Color.WithAlpha(alpha)); + worldHandle.UseShader(null); + } +} diff --git a/Content.Client/_White/Overlays/IRHudOverlay.cs b/Content.Client/_White/Overlays/IRHudOverlay.cs new file mode 100644 index 00000000000..6691452f8c7 --- /dev/null +++ b/Content.Client/_White/Overlays/IRHudOverlay.cs @@ -0,0 +1,166 @@ +using System.Linq; +using System.Numerics; +using Content.Client.Stealth; +using Content.Shared._White.Overlays; +using Content.Shared.Body.Components; +using Content.Shared.Stealth.Components; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Timing; + +namespace Content.Client._White.Overlays; + +public sealed class IRHudOverlay : Overlay +{ + [Dependency] private readonly IEntityManager _entity = default!; + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + private readonly TransformSystem _transform; + private readonly StealthSystem _stealth; + private readonly ContainerSystem _container; + private readonly SharedPointLightSystem _light; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + + private readonly List _entries = []; + + private EntityUid? _lightEntity; + + public float LightRadius; + + public IRHudComponent? Comp; + + public IRHudOverlay() + { + IoCManager.InjectDependencies(this); + + _container = _entity.System(); + _transform = _entity.System(); + _stealth = _entity.System(); + _light = _entity.System(); + + ZIndex = -1; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture is null || Comp is null) + return; + + var worldHandle = args.WorldHandle; + var eye = args.Viewport.Eye; + + if (eye == null) + return; + + var player = _player.LocalEntity; + + if (!_entity.TryGetComponent(player, out TransformComponent? playerXform)) + return; + + var accumulator = Math.Clamp(Comp.PulseAccumulator, 0f, Comp.PulseTime); + var alpha = Comp.PulseTime <= 0f ? 1f : float.Lerp(1f, 0f, accumulator / Comp.PulseTime); + + // Thermal vision grants some night vision (clientside light) + if (LightRadius > 0) + { + _lightEntity ??= _entity.SpawnAttachedTo(null, playerXform.Coordinates); + _transform.SetParent(_lightEntity.Value, player.Value); + var light = _entity.EnsureComponent(_lightEntity.Value); + _light.SetRadius(_lightEntity.Value, LightRadius, light); + _light.SetEnergy(_lightEntity.Value, alpha, light); + _light.SetColor(_lightEntity.Value, Comp.Color, light); + } + else + ResetLight(); + + var mapId = eye.Position.MapId; + var eyeRot = eye.Rotation; + + _entries.Clear(); + var entities = _entity.EntityQueryEnumerator(); + while (entities.MoveNext(out var uid, out var body, out var sprite, out var xform)) + { + if (!CanSee(uid, sprite) || !body.ThermalVisibility) + continue; + + var entity = uid; + + if (_container.TryGetOuterContainer(uid, xform, out var container)) + { + continue; // Mono + + // Mono edit, Thermals don't reveal people in lockers + /* + var owner = container.Owner; + if (_entity.TryGetComponent(owner, out var ownerSprite) + && _entity.TryGetComponent(owner, out var ownerXform)) + { + entity = owner; + sprite = ownerSprite; + xform = ownerXform; + } + */ + // Mono End + } + + if (_entries.Any(e => e.Ent.Owner == entity)) + continue; + + _entries.Add(new IRHudRenderEntry((entity, sprite, xform), mapId, eyeRot)); + } + + foreach (var entry in _entries) + { + Render(entry.Ent, entry.Map, worldHandle, entry.EyeRot, Comp.Color, alpha); + } + + worldHandle.SetTransform(Matrix3x2.Identity); + } + + private void Render(Entity ent, + MapId? map, + DrawingHandleWorld handle, + Angle eyeRot, + Color color, + float alpha) + { + var (uid, sprite, xform) = ent; + if (xform.MapID != map || !CanSee(uid, sprite)) + return; + + var position = _transform.GetWorldPosition(xform); + var rotation = _transform.GetWorldRotation(xform); + + + var originalColor = sprite.Color; + sprite.Color = color.WithAlpha(alpha); + sprite.Render(handle, eyeRot, rotation, position: position); + sprite.Color = originalColor; + } + + private bool CanSee(EntityUid uid, SpriteComponent sprite) + { + return sprite.Visible && (!_entity.TryGetComponent(uid, out StealthComponent? stealth) || + _stealth.GetVisibility(uid, stealth) > 0.5f); + } + + public void ResetLight(bool checkFirstTimePredicted = true) + { + if (_lightEntity == null || checkFirstTimePredicted && !_timing.IsFirstTimePredicted) + return; + + _entity.DeleteEntity(_lightEntity); + _lightEntity = null; + } +} + +public record struct IRHudRenderEntry( + Entity Ent, + MapId? Map, + Angle EyeRot); diff --git a/Content.Client/_White/Overlays/IRHudSystem.cs b/Content.Client/_White/Overlays/IRHudSystem.cs new file mode 100644 index 00000000000..77385c8ecaf --- /dev/null +++ b/Content.Client/_White/Overlays/IRHudSystem.cs @@ -0,0 +1,112 @@ +using Content.Client.Overlays; +using Content.Shared._White.Overlays; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Robust.Client.Graphics; + +namespace Content.Client._White.Overlays; + +public sealed class IRHudSystem : EquipmentHudSystem +{ + [Dependency] private readonly IOverlayManager _overlayMan = default!; + + private IRHudOverlay _IRHudOverlay = default!; + private BaseSwitchableOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToggle); + + _IRHudOverlay = new IRHudOverlay(); + _overlay = new BaseSwitchableOverlay(); + } + + protected override void OnRefreshComponentHud(Entity ent, + ref RefreshEquipmentHudEvent args) + { + if (!ent.Comp.IsEquipment) + base.OnRefreshComponentHud(ent, ref args); + } + + protected override void OnRefreshEquipmentHud(Entity ent, + ref InventoryRelayedEvent> args) + { + if (ent.Comp.IsEquipment) + base.OnRefreshEquipmentHud(ent, ref args); + } + + private void OnToggle(Entity ent, ref SwitchableOverlayToggledEvent args) + { + RefreshOverlay(); + } + + protected override void UpdateInternal(RefreshEquipmentHudEvent args) + { + base.UpdateInternal(args); + IRHudComponent? tvComp = null; + var lightRadius = 0f; + foreach (var comp in args.Components) + { + if (!comp.IsActive && (comp.PulseTime <= 0f || comp.PulseAccumulator >= comp.PulseTime)) + continue; + + if (tvComp == null) + tvComp = comp; + else if (!tvComp.DrawOverlay && comp.DrawOverlay) + tvComp = comp; + else if (tvComp.DrawOverlay == comp.DrawOverlay && tvComp.PulseTime > 0f && comp.PulseTime <= 0f) + tvComp = comp; + + lightRadius = MathF.Max(lightRadius, comp.LightRadius); + } + + UpdateIRHudOverlay(tvComp, lightRadius); + UpdateOverlay(tvComp); + } + + protected override void DeactivateInternal() + { + base.DeactivateInternal(); + + _IRHudOverlay.ResetLight(false); + UpdateOverlay(null); + UpdateIRHudOverlay(null, 0f); + } + + private void UpdateIRHudOverlay(IRHudComponent? comp, float lightRadius) + { + _IRHudOverlay.LightRadius = lightRadius; + _IRHudOverlay.Comp = comp; + + switch (comp) + { + case not null when !_overlayMan.HasOverlay(): + _overlayMan.AddOverlay(_IRHudOverlay); + break; + case null: + _overlayMan.RemoveOverlay(_IRHudOverlay); + _IRHudOverlay.ResetLight(); + break; + } + } + + private void UpdateOverlay(IRHudComponent? tvComp) + { + _overlay.Comp = tvComp; + + switch (tvComp) + { + case { DrawOverlay: true } when !_overlayMan.HasOverlay>(): + _overlayMan.AddOverlay(_overlay); + break; + case null or { DrawOverlay: false }: + _overlayMan.RemoveOverlay(_overlay); + break; + } + + // Night vision overlay is prioritized + _overlay.IsActive = !_overlayMan.HasOverlay>(); + } +} diff --git a/Content.Client/_White/Overlays/NVHudSystem.cs b/Content.Client/_White/Overlays/NVHudSystem.cs new file mode 100644 index 00000000000..770bfeea63d --- /dev/null +++ b/Content.Client/_White/Overlays/NVHudSystem.cs @@ -0,0 +1,105 @@ +using Content.Client.Overlays; +using Content.Shared._White.Overlays; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Robust.Client.Graphics; + +namespace Content.Client._White.Overlays; + +public sealed class NVHudSystem : EquipmentHudSystem +{ + [Dependency] private readonly IOverlayManager _overlayMan = default!; + [Dependency] private readonly ILightManager _lightManager = default!; + + private BaseSwitchableOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToggle); + + _overlay = new BaseSwitchableOverlay(); + } + + protected override void OnRefreshComponentHud(Entity ent, + ref RefreshEquipmentHudEvent args) + { + if (!ent.Comp.IsEquipment) + base.OnRefreshComponentHud(ent, ref args); + } + + protected override void OnRefreshEquipmentHud(Entity ent, + ref InventoryRelayedEvent> args) + { + if (ent.Comp.IsEquipment) + base.OnRefreshEquipmentHud(ent, ref args); + } + + private void OnToggle(Entity ent, ref SwitchableOverlayToggledEvent args) + { + RefreshOverlay(); + } + + protected override void UpdateInternal(RefreshEquipmentHudEvent args) + { + base.UpdateInternal(args); + + var active = false; + NVHudComponent? nvComp = null; + foreach (var comp in args.Components) + { + if (comp.IsActive || comp.PulseTime > 0f && comp.PulseAccumulator < comp.PulseTime) + active = true; + else + continue; + + if (comp.DrawOverlay) + { + if (nvComp == null) + nvComp = comp; + else if (nvComp.PulseTime > 0f && comp.PulseTime <= 0f) + nvComp = comp; + } + + if (active && nvComp is { PulseTime: <= 0 }) + break; + } + + UpdateNVHud(active); + UpdateOverlay(nvComp); + } + + protected override void DeactivateInternal() + { + base.DeactivateInternal(); + + UpdateNVHud(false); + UpdateOverlay(null); + } + + private void UpdateNVHud(bool active) + { + Log.Info($"NVHudSystem: Setting DrawLighting to {!active}"); + + _lightManager.DrawLighting = !active; + } + + private void UpdateOverlay(NVHudComponent? nvComp) + { + _overlay.Comp = nvComp; + + switch (nvComp) + { + case not null when !_overlayMan.HasOverlay>(): + _overlayMan.AddOverlay(_overlay); + break; + case null: + _overlayMan.RemoveOverlay(_overlay); + break; + } + + if (_overlayMan.TryGetOverlay>(out var overlay)) + overlay.IsActive = nvComp == null; + } +} diff --git a/Content.Shared/Body/Components/BodyComponent.cs b/Content.Shared/Body/Components/BodyComponent.cs index 481e22150b0..ef04c21d508 100644 --- a/Content.Shared/Body/Components/BodyComponent.cs +++ b/Content.Shared/Body/Components/BodyComponent.cs @@ -41,4 +41,9 @@ public sealed partial class BodyComponent : Component [ViewVariables] [DataField, AutoNetworkedField] public HashSet LegEntities = new(); + + // WD EDIT START + [DataField, AutoNetworkedField] + public bool ThermalVisibility = true; + // WD EDIT END } diff --git a/Content.Shared/Damage/DamageModifierSet.cs b/Content.Shared/Damage/DamageModifierSet.cs index eaa6e93da4c..ef28f7c0551 100644 --- a/Content.Shared/Damage/DamageModifierSet.cs +++ b/Content.Shared/Damage/DamageModifierSet.cs @@ -1,5 +1,6 @@ using Content.Shared.Damage.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; // goob change using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; namespace Content.Shared.Damage @@ -23,5 +24,26 @@ public partial class DamageModifierSet [DataField("flatReductions", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] public Dictionary FlatReduction = new(); + + /// + /// Goobstation. + /// Whether this modifier set will ignore incoming damage partial armor penetration, positive or negative. + /// Used mainly for species modifier sets. + /// + [DataField(customTypeSerializer: typeof(FlagSerializer))] + public int IgnoreArmorPierceFlags = (int) PartialArmorPierceFlags.None; + } + + // Goobstation start + public sealed class ArmorPierceFlags; + + [Flags, Serializable] + [FlagsFor(typeof(ArmorPierceFlags))] + public enum PartialArmorPierceFlags + { + None = 0, + Positive = 1 << 0, + Negative = 1 << 1, + All = Positive | Negative, } } diff --git a/Content.Shared/Damage/DamageSpecifier.cs b/Content.Shared/Damage/DamageSpecifier.cs index 7f505b807f7..ca125ea2943 100644 --- a/Content.Shared/Damage/DamageSpecifier.cs +++ b/Content.Shared/Damage/DamageSpecifier.cs @@ -297,6 +297,49 @@ public Dictionary GetDamagePerGroup(IPrototypeManager proto return dict; } + // Goobstation - partial AP. Returns new armor modifier set. + public static DamageModifierSet PenetrateArmor(DamageModifierSet modifierSet, float penetration) + { + if (penetration == 0f || + penetration > 0f && (modifierSet.IgnoreArmorPierceFlags & (int) PartialArmorPierceFlags.Positive) != 0 || + penetration < 0f && (modifierSet.IgnoreArmorPierceFlags & (int) PartialArmorPierceFlags.Negative) != 0) + return modifierSet; + + var result = new DamageModifierSet(); + if (penetration >= 1f) + return result; + + var inversePen = 1f - penetration; + + foreach (var (type, coef) in modifierSet.Coefficients) + { + // Negative coefficients are not modified by this, + // coefficients above 1 will actually be lowered which is not desired + if (coef is <= 0 or >= 1) + { + result.Coefficients.Add(type, coef); + continue; + } + + result.Coefficients.Add(type, MathF.Pow(coef, inversePen)); + } + + foreach (var (type, flat) in modifierSet.FlatReduction) + { + // Negative flat reductions are not modified by this + if (flat <= 0) + { + result.FlatReduction.Add(type, flat); + continue; + } + + result.FlatReduction.Add(type, flat * inversePen); + } + + return result; + } + + /// public void GetDamagePerGroup(IPrototypeManager protoManager, Dictionary dict) { diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs index 10dc6b3445a..135bff342aa 100644 --- a/Content.Shared/Damage/Systems/DamageableSystem.cs +++ b/Content.Shared/Damage/Systems/DamageableSystem.cs @@ -1,4 +1,6 @@ -using System.Linq; +using Content.Shared._Shitmed.Targeting; +// Shitmed Change +using Content.Shared.Body.Systems; using Content.Shared.CCVar; using Content.Shared.Chemistry; using Content.Shared.Damage.Prototypes; @@ -13,12 +15,10 @@ using Robust.Shared.GameStates; using Robust.Shared.Network; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - -// Shitmed Change -using Content.Shared.Body.Systems; -using Content.Shared._Shitmed.Targeting; using Robust.Shared.Random; +using Robust.Shared.Utility; +using System.Linq; +using static Content.Shared.Damage.DamageableSystem; namespace Content.Shared.Damage { @@ -28,6 +28,7 @@ public sealed class DamageableSystem : EntitySystem [Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly SharedBodySystem _body = default!; // Shitmed Change + [Dependency] private readonly IRobustRandom _random = default!; // Shitmed Change [Dependency] private readonly MobThresholdSystem _mobThreshold = default!; [Dependency] private readonly IConfigurationManager _config = default!; [Dependency] private readonly SharedChemistryGuideDataSystem _chemistryGuideData = default!; @@ -151,7 +152,7 @@ public void SetDamage(EntityUid uid, DamageableComponent damageable, DamageSpeci /// The damage changed event is used by other systems, such as damage thresholds. /// public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSpecifier? damageDelta = null, - bool interruptsDoAfters = true, EntityUid? origin = null, bool? canSever = null) // Shitmed Change + bool interruptsDoAfters = true, EntityUid? origin = null, bool? canSever = null, float armorPenetration = 0f) // Shitmed Change { component.Damage.GetDamagePerGroup(_prototypeManager, component.DamagePerGroup); component.TotalDamage = component.Damage.GetTotal(); @@ -165,6 +166,13 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp RaiseLocalEvent(uid, new DamageChangedEvent(component, damageDelta, interruptsDoAfters, origin, canSever ?? true)); // Shitmed Change } + // Mono: damage origin flags for if we can't or don't want to discern by UID + public enum DamageOriginFlag + { + Explosion, // flag set by ExplosionSystem.Processing + Barotrauma // flag set by BarotraumaSystem + } + /// /// Applies damage specified via a . /// @@ -178,9 +186,11 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp /// null if the user had no applicable components that can take damage. /// public DamageSpecifier? TryChangeDamage(EntityUid? uid, DamageSpecifier damage, bool ignoreResistances = false, - bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null, + bool interruptsDoAfters = true, DamageableComponent? damageable = null, EntityUid? origin = null, float armorPenetration = 0f, // Shitmed Change - bool? canSever = true, bool? canEvade = false, float? partMultiplier = 1.00f, TargetBodyPart? targetPart = null, EntityUid? tool = null, float armorPenetration = 0) + bool? canSever = true, bool? canEvade = false, float? partMultiplier = 1.00f, TargetBodyPart? targetPart = null, EntityUid? tool = null, + // Mono: arg to ID indirect damage sources + DamageOriginFlag? originFlag = null) { if (!uid.HasValue || !_damageableQuery.Resolve(uid.Value, ref damageable, false)) { @@ -193,7 +203,8 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp return damage; } - var before = new BeforeDamageChangedEvent(damage, origin, targetPart); // Shitmed Change + var before = new BeforeDamageChangedEvent(damage, origin, targetPart, //Shitmed Change + false, originFlag); // Mono: originFlag RaiseLocalEvent(uid.Value, ref before); if (before.Cancelled) @@ -217,7 +228,8 @@ public void DamageChanged(EntityUid uid, DamageableComponent component, DamageSp // TODO DAMAGE PERFORMANCE // use a local private field instead of creating a new dictionary here.. // TODO: We need to add a check to see if the given armor covers the targeted part (if any) to modify or not. - damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet); + damage = DamageSpecifier.ApplyModifierSet(damage, + DamageSpecifier.PenetrateArmor(modifierSet, armorPenetration)); // Goob edit } var ev = new DamageModifyEvent(damage, origin, armorPenetration, targetPart, tool); // Shitmed Change @@ -408,7 +420,7 @@ private void OnIrradiated(EntityUid uid, DamageableComponent component, OnIrradi damage.DamageDict.Add(typeId, damageValue); } - TryChangeDamage(uid, damage, interruptsDoAfters: false, origin: args.Origin); + TryChangeDamage(uid, damage, interruptsDoAfters: false); } private void OnRejuvenate(EntityUid uid, DamageableComponent component, RejuvenateEvent args) @@ -451,7 +463,8 @@ public record struct BeforeDamageChangedEvent( DamageSpecifier Damage, EntityUid? Origin = null, TargetBodyPart? TargetPart = null, // Shitmed Change - bool Cancelled = false); + bool Cancelled = false, + DamageOriginFlag? OriginFlag = null); // Mono: OriginFlag /// /// Shitmed Change: Raised on parts before damage is done so we can cancel the damage if they evade. @@ -483,8 +496,8 @@ public sealed class DamageModifyEvent : EntityEventArgs, IInventoryRelayEvent public readonly DamageSpecifier OriginalDamage; public DamageSpecifier Damage; public EntityUid? Origin; + public float ArmorPenetration; // Goobstation public readonly TargetBodyPart? TargetPart; // Shitmed Change - public readonly float ArmorPenetration = 0; // Goobstation public EntityUid? Tool; public DamageModifyEvent(DamageSpecifier damage, EntityUid? origin = null, float armorPenetration = 0, TargetBodyPart? targetPart = null, EntityUid? tool = null) // Shitmed Change diff --git a/Content.Shared/Inventory/InventoryComponent.cs b/Content.Shared/Inventory/InventoryComponent.cs index 629cf1169c4..5a1dcc289aa 100644 --- a/Content.Shared/Inventory/InventoryComponent.cs +++ b/Content.Shared/Inventory/InventoryComponent.cs @@ -7,10 +7,12 @@ namespace Content.Shared.Inventory; [RegisterComponent, NetworkedComponent] [Access(typeof(InventorySystem))] +[AutoGenerateComponentState(true)] public sealed partial class InventoryComponent : Component { [DataField("templateId", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string TemplateId { get; private set; } = "human"; + [AutoNetworkedField] + public string TemplateId { get; set; } = "human"; [DataField("speciesId")] public string? SpeciesId { get; set; } @@ -31,4 +33,16 @@ public sealed partial class InventoryComponent : Component /// [DataField] public Dictionary MaleDisplacements = new(); + + /// + /// Mono - blocks slots with those names from receiving InventoryRelayEvent-s. + /// + [DataField] + public List RelayBlockedSlots = new(); } + +/// +/// Raised if the of an inventory changed. +/// +[ByRefEvent] +public struct InventoryTemplateUpdated; diff --git a/Content.Shared/Inventory/InventorySystem.Helpers.cs b/Content.Shared/Inventory/InventorySystem.Helpers.cs index 746342e2f15..3d62515a8e9 100644 --- a/Content.Shared/Inventory/InventorySystem.Helpers.cs +++ b/Content.Shared/Inventory/InventorySystem.Helpers.cs @@ -139,4 +139,17 @@ public void SpawnItemOnEntity(EntityUid entity, EntProtoId item) //Try insert into hands, or drop on the floor _handsSystem.PickupOrDrop(entity, itemToSpawn, false); } + + // Goobstation + public bool TryGetContainingEntity(Entity entity, [NotNullWhen(true)] out EntityUid? containingEntity) + { + if (!_containerSystem.TryGetContainingContainer(entity, out var container) || !HasComp(container.Owner)) + { + containingEntity = null; + return false; + } + + containingEntity = container.Owner; + return true; + } } diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index c82ec43d8ce..b104ed25bb4 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -1,3 +1,5 @@ +using Content.Shared._Goobstation.Flashbang; +using Content.Shared._White.Overlays; using Content.Shared.Armor; using Content.Shared.Atmos; using Content.Shared.Chat; @@ -36,11 +38,13 @@ public void InitializeRelay() SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); - SubscribeLocalEvent(RelayInventoryEvent); - SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEventAlways); // Mono + SubscribeLocalEvent(RelayInventoryEventAlways); // Mono SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); - SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEventAlways); // Mono + SubscribeLocalEvent(RelayInventoryEvent); // goob edit + SubscribeLocalEvent(RelayInventoryEvent); // goob edit SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); @@ -78,8 +82,11 @@ public void InitializeRelay() SubscribeLocalEvent>(RefRelayInventoryEvent); SubscribeLocalEvent>(RefRelayInventoryEvent); + SubscribeLocalEvent>(RefRelayInventoryEvent); // Goobstation + SubscribeLocalEvent>(RefRelayInventoryEvent); // Goobstation + SubscribeLocalEvent>(RefRelayInventoryEvent); + SubscribeLocalEvent>(OnGetEquipmentVerbs); - SubscribeLocalEvent>(OnGetInnateVerbs); } @@ -93,7 +100,19 @@ protected void RelayInventoryEvent(EntityUid uid, InventoryComponent componen RelayEvent((uid, component), args); } - public void RelayEvent(Entity inventory, ref T args) where T : IInventoryRelayEvent + // Mono + protected void RefRelayInventoryEventAlways(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent + { + RelayEvent((uid, component), ref args, true); + } + + // Mono + protected void RelayInventoryEventAlways(EntityUid uid, InventoryComponent component, T args) where T : IInventoryRelayEvent + { + RelayEvent((uid, component), args, true); + } + + public void RelayEvent(Entity inventory, ref T args, bool ignoreBlocked = false) where T : IInventoryRelayEvent { if (args.TargetSlots == SlotFlags.NONE) return; @@ -101,25 +120,27 @@ public void RelayEvent(Entity inventory, ref T args) wher // this copies the by-ref event if it is a struct var ev = new InventoryRelayedEvent(args); var enumerator = new InventorySlotEnumerator(inventory, args.TargetSlots); - while (enumerator.NextItem(out var item)) + while (enumerator.NextItem(out var item, out var slot)) { - RaiseLocalEvent(item, ev); + if (!ignoreBlocked && !inventory.Comp.RelayBlockedSlots.Contains(slot.Name)) // Mono + RaiseLocalEvent(item, ev); } // and now we copy it back args = ev.Args; } - public void RelayEvent(Entity inventory, T args) where T : IInventoryRelayEvent + public void RelayEvent(Entity inventory, T args, bool ignoreBlocked = false) where T : IInventoryRelayEvent { if (args.TargetSlots == SlotFlags.NONE) return; var ev = new InventoryRelayedEvent(args); var enumerator = new InventorySlotEnumerator(inventory, args.TargetSlots); - while (enumerator.NextItem(out var item)) + while (enumerator.NextItem(out var item, out var slot)) { - RaiseLocalEvent(item, ev); + if (!ignoreBlocked && !inventory.Comp.RelayBlockedSlots.Contains(slot.Name)) // Mono + RaiseLocalEvent(item, ev); } } @@ -135,17 +156,6 @@ private void OnGetEquipmentVerbs(EntityUid uid, InventoryComponent component, Ge } } - private void OnGetInnateVerbs(EntityUid uid, InventoryComponent component, GetVerbsEvent args) - { - // Automatically relay stripping related verbs to all equipped clothing. - var ev = new InventoryRelayedEvent>(args); - var enumerator = new InventorySlotEnumerator(component, SlotFlags.WITHOUT_POCKET); - while (enumerator.NextItem(out var item)) - { - RaiseLocalEvent(item, ev); - } - } - } /// diff --git a/Content.Shared/Overlays/ThermalSightComponent.cs b/Content.Shared/Overlays/ThermalSightComponent.cs new file mode 100644 index 00000000000..df199728dcb --- /dev/null +++ b/Content.Shared/Overlays/ThermalSightComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Overlays; + +/// +/// Makes the entity see air temperature. +/// When added to a clothing item it will also grant the wearer the same overlay. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class ThermalSightComponent : Component; diff --git a/Content.Shared/_Goobstation/Flashbang/FlashEvents.cs b/Content.Shared/_Goobstation/Flashbang/FlashEvents.cs new file mode 100644 index 00000000000..3788558bcb7 --- /dev/null +++ b/Content.Shared/_Goobstation/Flashbang/FlashEvents.cs @@ -0,0 +1,25 @@ +using Content.Shared.Inventory; + +namespace Content.Shared._Goobstation.Flashbang; + +public sealed class GetFlashbangedEvent(float range) : EntityEventArgs, IInventoryRelayEvent +{ + public float ProtectionRange = range; + + public SlotFlags TargetSlots => SlotFlags.EARS | SlotFlags.HEAD; +} +public sealed class AreaFlashEvent(float range, float distance, EntityUid target) : EntityEventArgs +{ + public float Range = range; + + public float Distance = distance; + + public EntityUid Target = target; +} + +public sealed class FlashDurationMultiplierEvent : EntityEventArgs, IInventoryRelayEvent +{ + public float Multiplier = 1f; + + public SlotFlags TargetSlots => SlotFlags.EYES | SlotFlags.HEAD | SlotFlags.MASK; +} diff --git a/Content.Shared/_Mono/ArmorPlate/ArmorPlateHolderComponent.cs b/Content.Shared/_Mono/ArmorPlate/ArmorPlateHolderComponent.cs new file mode 100644 index 00000000000..17cd98e8bbf --- /dev/null +++ b/Content.Shared/_Mono/ArmorPlate/ArmorPlateHolderComponent.cs @@ -0,0 +1,57 @@ +using Content.Shared.Containers.ItemSlots; // HardLight +using Robust.Shared.GameStates; + +namespace Content.Shared._Mono.ArmorPlate; + +/// +/// Component for clothes that can hold an armor plate in a dedicated slot. // HardLight +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class ArmorPlateHolderComponent : Component +{ + // HardLight start: Moved plate storage to a dedicated item slot to simplify logic and allow for better interactions with container systems. + public const string PlateSlotId = "armor_plate"; + + /// + /// The item slot used to hold the installed armor plate. + /// + [DataField("plateSlot")] + public ItemSlot PlateSlot = new(); + // HardLight end + + /// + /// Reference to the currently active armor plate entity. + /// + [DataField] + [AutoNetworkedField] + public EntityUid? ActivePlate; + + /// + /// Whether to show a popup notification when the active plate is destroyed. + /// + [DataField] + public bool ShowBreakPopup = true; + + /// + /// Walk speed modifier from the currently active plate. + /// + [DataField] + [AutoNetworkedField] + public float WalkSpeedModifier = 1.0f; + + /// + /// Sprint speed modifier from the currently active plate. + /// + [DataField] + [AutoNetworkedField] + public float SprintSpeedModifier = 1.0f; + + /// + /// Stamina damage multiplier from the currently active plate. + /// + [DataField] + public float StaminaDamageMultiplier = 1.0f; + +} + diff --git a/Content.Shared/_Mono/ArmorPlate/ArmorPlateItemComponent.cs b/Content.Shared/_Mono/ArmorPlate/ArmorPlateItemComponent.cs new file mode 100644 index 00000000000..d94c94a4b13 --- /dev/null +++ b/Content.Shared/_Mono/ArmorPlate/ArmorPlateItemComponent.cs @@ -0,0 +1,53 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Mono.ArmorPlate; + +/// +/// Component for armor plates that can be inserted into compatible clothing. +/// +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class ArmorPlateItemComponent : Component +{ + /// + /// Maximum durability of this plate before destruction. Should match the destruction threshold in DestructibleComponent. + /// + [DataField] + [AutoNetworkedField] + public int MaxDurability = 100; + + /// + /// Walk speed modifier applied when this plate is active in worn clothing. + /// + [DataField] + [AutoNetworkedField] + public float WalkSpeedModifier = 1.0f; + + /// + /// Sprint speed modifier applied when this plate is active in worn clothing. + /// + [DataField] + [AutoNetworkedField] + public float SprintSpeedModifier = 1.0f; + + /// + /// Multiplier applied when converting absorbed damage to stamina damage. + /// + [DataField] + public float StaminaDamageMultiplier = 1.0f; + + /// + /// How much damage dealt to the plate is multiplied, by damagetype + /// + [DataField("damageMultipliers")] + public Dictionary DamageMultipliers = new(); + + /// + /// Absorption effect of the plate, by damagetype. + /// Can go negative which INCREASES damage taken. Negative values will still decrement armor durability. + /// + [DataField("absorptionRatios")] + public Dictionary AbsorptionRatios = new(); + +} + diff --git a/Content.Shared/_Mono/ArmorPlate/ArmorPlateProtectedComponent.cs b/Content.Shared/_Mono/ArmorPlate/ArmorPlateProtectedComponent.cs new file mode 100644 index 00000000000..734b3acb15c --- /dev/null +++ b/Content.Shared/_Mono/ArmorPlate/ArmorPlateProtectedComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.Timing; + +namespace Content.Shared.Armor; + +[RegisterComponent] + +/// +/// Added or removed on plate insertion/removal or equip/unequip of any equipment with ArmorPlateHolderComponent. +/// Tying subscription of OnBeforeDamageChanged to this component for plates prevents constant spam from this system from passive regeneration and breathing from unarmored players. +/// +public sealed partial class ArmorPlateProtectedComponent : Component { } diff --git a/Content.Shared/_Mono/ArmorPlate/SharedArmorPlateSystem.cs b/Content.Shared/_Mono/ArmorPlate/SharedArmorPlateSystem.cs new file mode 100644 index 00000000000..d9f27be48d5 --- /dev/null +++ b/Content.Shared/_Mono/ArmorPlate/SharedArmorPlateSystem.cs @@ -0,0 +1,493 @@ +using Content.Shared.Armor; +using Content.Shared.Damage; +using Content.Shared.Damage.Systems; +using Content.Shared.Examine; +using Content.Shared.FixedPoint; +using Content.Shared.Containers.ItemSlots; // HardLight +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Movement.Systems; +using Content.Shared.Popups; +// using Content.Shared.Storage; // HardLight +using Content.Shared.Verbs; +using Robust.Shared.Containers; +// using Robust.Shared.Timing; // HardLight +using Robust.Shared.Utility; + +namespace Content.Shared._Mono.ArmorPlate; + +/// +/// Handles all armor plate behavior +/// +public sealed class SharedArmorPlateSystem : EntitySystem +{ + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly ExamineSystemShared _examine = default!; + // [Dependency] private readonly IGameTiming _timing = default!; // HardLight + [Dependency] private readonly StaminaSystem _stamina = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly ItemSlotsSystem _itemSlots = default!; // HardLight + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHolderInit); // HardLight + SubscribeLocalEvent(OnHolderRemove); // HardLight + SubscribeLocalEvent(OnPlateInsertAttempt); // HardLight + SubscribeLocalEvent(OnPlateInserted); + SubscribeLocalEvent(OnPlateRemoved); + SubscribeLocalEvent(OnEquippedArmor); + SubscribeLocalEvent(OnUnequippedArmor); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent>(OnRefreshMoveSpeed); + SubscribeLocalEvent>(OnPlateVerbExamine); + SubscribeLocalEvent(OnPlateDestroyed); + SubscribeLocalEvent(OnBeforeDamageChanged); + } + + // HardLight start: Added item slot management for the armor plate slot and validation on insertion to ensure only valid plates can be inserted. + private void OnHolderInit(EntityUid uid, ArmorPlateHolderComponent component, ComponentInit args) + { + _itemSlots.AddItemSlot(uid, ArmorPlateHolderComponent.PlateSlotId, component.PlateSlot); + } + + private void OnHolderRemove(EntityUid uid, ArmorPlateHolderComponent component, ComponentRemove args) + { + _itemSlots.RemoveItemSlot(uid, component.PlateSlot); + } + + private void OnPlateInsertAttempt(Entity ent, ref ItemSlotInsertAttemptEvent args) + { + if (!HasComp(args.Item)) + args.Cancelled = true; + } + // HardLight end + + public void OnBeforeDamageChanged(Entity ent, ref BeforeDamageChangedEvent args) + { + if (args.Cancelled || !args.Damage.AnyPositive()) + return; + + if (!TryComp(ent.Owner, out var inv)) + return; + + if (!_inventory.TryGetSlots(ent, out var slots)) + return; + + if (args.Origin == null && args.OriginFlag != DamageableSystem.DamageOriginFlag.Explosion) + return; + + foreach (var slot in slots) + { + if (!_inventory.TryGetSlotEntity(ent, slot.Name, out var equipped, inv)) + continue; + + if (!TryComp(equipped, out var holder)) + continue; + + if (!TryGetActivePlate((equipped.Value, holder), out var plate)) + continue; + + // Calculate damages owed to plate and holder + CalcPlateDamages(args.Damage, plate.Comp, out var remainder, out var absorbed, out var plateDamage); + + // Damage to plate, stamina damage to holder + AbsorbDamage(ent, plate, absorbed, plateDamage); // HardLight: Removed equipped.Value and holder + + // Full absorption, done + if (remainder.Empty) + { + args.Cancelled = true; + return; + } + + // Replace raw damage with remaining damage post-absorption + args.Damage.DamageDict.Clear(); + foreach (var (type, amt) in remainder.DamageDict) + args.Damage.DamageDict.Add(type, amt); + } + } + + private void AbsorbDamage( + EntityUid wearer, + // EntityUid armorUid, // HardLight + // ArmorPlateHolderComponent holder, // HardLight + Entity plate, + FixedPoint2 absorbed, + FixedPoint2 plateDamage) + + { + var damageSpec = new DamageSpecifier(); + damageSpec.DamageDict.Add("Blunt", plateDamage); + + _damageable.TryChangeDamage(plate.Owner, damageSpec, ignoreResistances: true); + + var staminaDamage = absorbed.Float() * plate.Comp.StaminaDamageMultiplier; + _stamina.TakeStaminaDamage(wearer, staminaDamage); + } + + private void OnPlateInserted(Entity ent, ref EntInsertedIntoContainerMessage args) + { + if (args.Container.ID != ent.Comp.PlateSlot.ID) // HardLight: StorageComponent.ContainerId(insertedEntity, out var plateComp)) + return; + + var holder = ent.Comp; + + if (holder.ActivePlate == null) + { + SetActivePlate(ent, insertedEntity, plateComp, holder); + } + } + + private void OnPlateRemoved(Entity ent, ref EntRemovedFromContainerMessage args) + { + if (args.Container.ID != ent.Comp.PlateSlot.ID) // HardLight: StorageComponent.ContainerId(ent, out var storage)) + // { + // foreach (var item in storage.Container.ContainedEntities) + // { + // if (TryComp(item, out var plateComp)) + // { + // SetActivePlate(ent, item, plateComp, holder); + // break; + // } + // } + // } + // HardLight end + } + + private void OnExamined(Entity ent, ref ExaminedEvent args) + { + var holder = ent.Comp; + + if (!_itemSlots.TryGetSlot(ent, ArmorPlateHolderComponent.PlateSlotId, out _)) // HardLight + { + args.PushMarkup(Loc.GetString("armor-plate-examine-no-slot")); // HardLight: -storage<-slot + return; + } + + if (holder.ActivePlate == null) + { + args.PushMarkup(Loc.GetString("armor-plate-examine-no-plate")); + return; + } + + var plateName = MetaData(holder.ActivePlate.Value).EntityName; + + if (!TryComp(holder.ActivePlate.Value, out var plateItem)) + { + args.PushMarkup(Loc.GetString("armor-plate-examine-with-plate-simple", ("plateName", plateName))); + return; + } + + if (TryComp(holder.ActivePlate.Value, out var damageable)) + { + var totalDamage = damageable.TotalDamage.Int(); + var maxDurability = plateItem.MaxDurability; + + var durabilityPercent = ((maxDurability - totalDamage) / (float)maxDurability) * 100f; + durabilityPercent = Math.Clamp(durabilityPercent, 0f, 100f); + + var durabilityColor = durabilityPercent switch + { + > 66f => "green", + >= 33f => "yellow", + _ => "red", + }; + + args.PushMarkup(Loc.GetString("armor-plate-examine-with-plate", + ("plateName", plateName), + ("percent", (int)durabilityPercent), + ("durabilityColor", durabilityColor))); + } + else + { + args.PushMarkup(Loc.GetString("armor-plate-examine-with-plate-simple", ("plateName", plateName))); + } + } + + private void OnRefreshMoveSpeed(EntityUid uid, ArmorPlateHolderComponent component, InventoryRelayedEvent args) + { + args.Args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier); + } + + /// + /// Sets the active plate and updates speed modifiers. + /// + private void SetActivePlate(EntityUid holderUid, EntityUid plateUid, ArmorPlateItemComponent plateComp, ArmorPlateHolderComponent holder) + { + holder.ActivePlate = plateUid; + holder.WalkSpeedModifier = plateComp.WalkSpeedModifier; + holder.SprintSpeedModifier = plateComp.SprintSpeedModifier; + holder.StaminaDamageMultiplier = plateComp.StaminaDamageMultiplier; + + Dirty(holderUid, holder); + RefreshMovementSpeed(holderUid); + RefreshPlateProtection(holderUid); + } + + /// + /// Clears the active plate and resets speed modifiers. + /// + private void ClearActivePlate(EntityUid holderUid, ArmorPlateHolderComponent holder) + { + holder.ActivePlate = null; + holder.WalkSpeedModifier = 1.0f; + holder.SprintSpeedModifier = 1.0f; + holder.StaminaDamageMultiplier = 1.0f; + + Dirty(holderUid, holder); + RefreshMovementSpeed(holderUid); + RefreshPlateProtection(holderUid); + } + + /// + /// Refreshes movement speed for the entity wearing this armor. + /// + private void RefreshMovementSpeed(EntityUid armorUid) + { + if (_inventory.TryGetContainingEntity(armorUid, out var wearer)) + { + _movementSpeed.RefreshMovementSpeedModifiers(wearer.Value); + } + } + + /// + /// Tries to get the active plate from an armor holder. + /// + public bool TryGetActivePlate(Entity holder, out Entity plate) + { + plate = default; + + if (!Resolve(holder, ref holder.Comp, logMissing: false)) + return false; + + if (holder.Comp.ActivePlate == null) + return false; + + if (!TryComp(holder.Comp.ActivePlate.Value, out var plateComp)) + return false; + + plate = (holder.Comp.ActivePlate.Value, plateComp); + return true; + } + + /// + /// Calculate numbers used for damaging plate and player + /// + public void CalcPlateDamages(DamageSpecifier incoming, ArmorPlateItemComponent plate, out DamageSpecifier remainder, out FixedPoint2 absorbedTotal, out FixedPoint2 plateDamageTotal) + { + remainder = new DamageSpecifier(); + absorbedTotal = FixedPoint2.Zero; + plateDamageTotal = FixedPoint2.Zero; + + foreach (var (type, amount) in incoming.DamageDict) + { + if (amount <= FixedPoint2.Zero) + continue; + + var multiplier = plate.DamageMultipliers.GetValueOrDefault(type, 1.0f); + var ratio = plate.AbsorptionRatios.GetValueOrDefault(type, 0f); + + FixedPoint2 absorbed = FixedPoint2.Zero; + FixedPoint2 remainderAmt = amount; + + if (ratio > 0f) + { + absorbed = amount * ratio; + remainderAmt = amount - absorbed; + } + else if (ratio < 0f) + { + remainderAmt = amount * (1f + Math.Abs(ratio)); + } + + var plateDamage = amount * Math.Abs(ratio) * multiplier; + + absorbedTotal = absorbedTotal + absorbed; + plateDamageTotal = plateDamageTotal + plateDamage; + + if (remainderAmt > FixedPoint2.Zero) + remainder.DamageDict.Add(type, remainderAmt); + } + } + + /// + /// Examine tooltip handler + /// + private void OnPlateVerbExamine(EntityUid uid, ArmorPlateItemComponent component, GetVerbsEvent args) + { + if (!args.CanInteract || !args.CanAccess) + return; + + var examineMarkup = GetPlateExamine(component); + + var ev = new ArmorExamineEvent(examineMarkup); + RaiseLocalEvent(uid, ref ev); + + _examine.AddDetailedExamineVerb(args, component, examineMarkup, + Loc.GetString("armor-plate-examinable-verb-text"), + "/Textures/Interface/VerbIcons/dot.svg.192dpi.png", + Loc.GetString("armor-plate-examinable-verb-message")); + } + + // Used to tell the .ftl if it's a positive or negative value + private static int CalcDirection(float ratio) => ratio < 0 ? 1 : ratio > 0 ? -1 : 0; + //Speed tooltip generating method + private void AddSpeedDisplay(FormattedMessage msg, string gaitType, float speedCalc) + { + var deltaSign = CalcDirection(speedCalc); + + msg.PushNewline(); + msg.AddMarkupOrThrow(Loc.GetString("armor-plate-speed-display", + ("gait", gaitType), + ("deltasign", deltaSign), + ("speedPercent", Math.Abs(speedCalc)) + )); + } + + private FormattedMessage GetPlateExamine(ArmorPlateItemComponent plate) + { + var msg = new FormattedMessage(); + msg.AddMarkupOrThrow(Loc.GetString("armor-plate-attributes-examine")); + + msg.PushNewline(); + + msg.AddMarkupOrThrow(Loc.GetString("armor-plate-initial-durability", + ("durability", plate.MaxDurability) + )); + + var walkModifierCalc = MathF.Round((plate.WalkSpeedModifier - 1.0f) * 100f, 1); + var sprintModifierCalc = MathF.Round((plate.SprintSpeedModifier - 1.0f) * 100f, 1); + + if (!(walkModifierCalc == 0.0f && sprintModifierCalc == 0.0f)) + { + if (MathHelper.CloseTo(walkModifierCalc, sprintModifierCalc, 0.5f)) + { + AddSpeedDisplay(msg, Loc.GetString("armor-plate-gait-speed"), walkModifierCalc); + } + else + { + AddSpeedDisplay(msg, Loc.GetString("armor-plate-gait-sprint"), sprintModifierCalc); + AddSpeedDisplay(msg, Loc.GetString("armor-plate-gait-walk"), walkModifierCalc); + } + } + + foreach (var kv in plate.AbsorptionRatios) + { + msg.PushNewline(); + + var dmgType = Loc.GetString("armor-damage-type-" + kv.Key.ToLower()); + var ratioPercent = MathF.Round(kv.Value * 100, 1); + + var multiplier = plate.DamageMultipliers.GetValueOrDefault(kv.Key, 1.0f); + var multiplierStr = multiplier.ToString("0.##"); + var deltaSign = CalcDirection(kv.Value); + + msg.AddMarkupOrThrow(Loc.GetString("armor-plate-ratios-display", + ("deltasign", deltaSign), + ("dmgType", dmgType), + ("ratioPercent", Math.Abs(ratioPercent)), + ("multiplier", multiplierStr) + )); + } + + msg.PushNewline(); + var staminaPercent = MathF.Round(plate.StaminaDamageMultiplier * 100f, 1); + msg.AddMarkupOrThrow(Loc.GetString("armor-plate-stamina-value", + ("multiplier", staminaPercent))); + + return msg; + } + + private void OnPlateDestroyed(Entity ent, ref EntityTerminatingEvent args) + { + if (!_container.TryGetContainingContainer(ent.Owner, out var container)) + return; + + var holderUid = container.Owner; + if (!TryComp(holderUid, out var holder)) + return; + + if (container.ID != holder.PlateSlot.ID) // HardLight + return; + + if (holder.ActivePlate != ent.Owner) + return; + + if (holder.ShowBreakPopup) + { + if (_inventory.TryGetContainingEntity(holderUid, out var wearer)) + { + var plateName = MetaData(ent).EntityName; + _popup.PopupEntity( + Loc.GetString("armor-plate-break", ("plateName", plateName)), + wearer.Value, + wearer.Value, + PopupType.MediumCaution + ); + } + } + } + + /// + /// Starts listening to damage instances for plate evaluation on equip of a plate-bearing item. + /// + private void OnEquippedArmor(Entity armor, ref GotEquippedEvent args) + { + if (TryGetActivePlate((armor.Owner, armor.Comp), out _)) + { + EnsureComp(args.Equipee); + } + } + + /// + /// Stops listening to damage instances for plate evaluation on unequip. + /// + private void OnUnequippedArmor(Entity armor, ref GotUnequippedEvent args) + { + if (TryGetActivePlate((armor.Owner, armor.Comp), out _)) + { + RemComp(args.Equipee); + } + } + + /// + /// Re-evaluates plate holder status. + /// + private void RefreshPlateProtection(EntityUid armorUid) + { + if (!_inventory.TryGetContainingEntity(armorUid, out var wearer)) + return; + + var wearerUid = wearer.Value; + + if (!TryComp(armorUid, out var holder)) + return; + + if (TryGetActivePlate((armorUid, holder), out _)) + EnsureComp(wearerUid); + else + RemComp(wearerUid); + } +} diff --git a/Content.Shared/_White/Overlays/BaseVisionOverlayComponent.cs b/Content.Shared/_White/Overlays/BaseVisionOverlayComponent.cs new file mode 100644 index 00000000000..89072c7e5ff --- /dev/null +++ b/Content.Shared/_White/Overlays/BaseVisionOverlayComponent.cs @@ -0,0 +1,18 @@ +using System.Numerics; + +namespace Content.Shared._White.Overlays; + +public abstract partial class BaseVisionOverlayComponent : Component +{ + [DataField, ViewVariables(VVAccess.ReadOnly)] + public virtual Vector3 Tint { get; set; } = new(0.3f, 0.3f, 0.3f); + + [DataField, ViewVariables(VVAccess.ReadOnly)] + public virtual float Strength { get; set; } = 2f; + + [DataField, ViewVariables(VVAccess.ReadOnly)] + public virtual float Noise { get; set; } = 0.5f; + + [DataField, ViewVariables(VVAccess.ReadOnly)] + public virtual Color Color { get; set; } = Color.White; +} diff --git a/Content.Shared/_White/Overlays/IRHudComponent.cs b/Content.Shared/_White/Overlays/IRHudComponent.cs new file mode 100644 index 00000000000..26c99019437 --- /dev/null +++ b/Content.Shared/_White/Overlays/IRHudComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Actions; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._White.Overlays; + +[RegisterComponent, NetworkedComponent] +public sealed partial class IRHudComponent : SwitchableVisionOverlayComponent +{ + public override EntProtoId? ToggleAction { get; set; } = "ToggleIRHud"; + + public override Color Color { get; set; } = Color.FromHex("#d06764"); + + [DataField] + public float LightRadius = 2f; +} + +public sealed partial class ToggleIRHudEvent : InstantActionEvent; diff --git a/Content.Shared/_White/Overlays/NVHudComponent.cs b/Content.Shared/_White/Overlays/NVHudComponent.cs new file mode 100644 index 00000000000..fbb5bc97f6e --- /dev/null +++ b/Content.Shared/_White/Overlays/NVHudComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared.Actions; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._White.Overlays; + +[RegisterComponent, NetworkedComponent] +public sealed partial class NVHudComponent : SwitchableVisionOverlayComponent +{ + public override EntProtoId? ToggleAction { get; set; } = "ToggleNVHud"; + + public override Color Color { get; set; } = Color.FromHex("#d4d4d4"); // Mono +} + +public sealed partial class ToggleNVHudEvent : InstantActionEvent; diff --git a/Content.Shared/_White/Overlays/SharedIRHudSystem.cs b/Content.Shared/_White/Overlays/SharedIRHudSystem.cs new file mode 100644 index 00000000000..a1dbb26610a --- /dev/null +++ b/Content.Shared/_White/Overlays/SharedIRHudSystem.cs @@ -0,0 +1,3 @@ +namespace Content.Shared._White.Overlays; + +public sealed class SharedIRHudSystem : SwitchableOverlaySystem; diff --git a/Content.Shared/_White/Overlays/SharedNVHudSystem.cs b/Content.Shared/_White/Overlays/SharedNVHudSystem.cs new file mode 100644 index 00000000000..41de140e3f5 --- /dev/null +++ b/Content.Shared/_White/Overlays/SharedNVHudSystem.cs @@ -0,0 +1,3 @@ +namespace Content.Shared._White.Overlays; + +public sealed class SharedNVHudSystem : SwitchableOverlaySystem; diff --git a/Content.Shared/_White/Overlays/SwitchableOverlaySystem.cs b/Content.Shared/_White/Overlays/SwitchableOverlaySystem.cs new file mode 100644 index 00000000000..3b7efebd47d --- /dev/null +++ b/Content.Shared/_White/Overlays/SwitchableOverlaySystem.cs @@ -0,0 +1,194 @@ +using Content.Shared._Goobstation.Flashbang; +using Content.Shared.Actions; +using Content.Shared.Inventory; +using Robust.Shared.Audio.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +namespace Content.Shared._White.Overlays; + +public abstract class SwitchableOverlaySystem : EntitySystem // this should get move to a white module if we ever do anything with forks.. + where TComp : SwitchableVisionOverlayComponent + where TEvent : InstantActionEvent +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + + public override void Initialize() + { + SubscribeLocalEvent(OnToggle); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnGetItemActions); + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnGetFlashMultiplier); + SubscribeLocalEvent>(OnGetInventoryFlashMultiplier); + } + + private void OnGetFlashMultiplier(Entity ent, ref FlashDurationMultiplierEvent args) + { + if (!ent.Comp.IsEquipment) + args.Multiplier *= GetFlashMultiplier(ent); + } + + private void OnGetInventoryFlashMultiplier(Entity ent, + ref InventoryRelayedEvent args) + { + if (ent.Comp.IsEquipment) + args.Args.Multiplier *= GetFlashMultiplier(ent); + } + + private float GetFlashMultiplier(TComp comp) + { + if (!comp.IsActive && (comp.PulseTime <= 0f || comp.PulseAccumulator >= comp.PulseTime)) + return 1f; + + return comp.FlashDurationMultiplier; + } + + public override void FrameUpdate(float frameTime) + { + base.FrameUpdate(frameTime); + + if (_net.IsClient) + ActiveTick(frameTime); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (_net.IsServer) + ActiveTick(frameTime); + } + + private void ActiveTick(float frameTime) + { + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.PulseTime <= 0f || comp.PulseAccumulator >= comp.PulseTime) + continue; + + comp.PulseAccumulator += frameTime; + + if (comp.PulseAccumulator < comp.PulseTime) + continue; + + Toggle(uid, comp, false, false); + RaiseSwitchableOverlayToggledEvent(uid, uid, comp.IsActive); + RaiseSwitchableOverlayToggledEvent(uid, Transform(uid).ParentUid, comp.IsActive); + } + } + + private void OnGetState(EntityUid uid, TComp component, ref ComponentGetState args) + { + args.State = new SwitchableVisionOverlayComponentState + { + Color = component.Color, + IsActive = component.IsActive, + FlashDurationMultiplier = component.FlashDurationMultiplier, + ActivateSound = component.ActivateSound, + DeactivateSound = component.DeactivateSound, + ToggleAction = component.ToggleAction, + LightRadius = component is IRHudComponent thermal ? thermal.LightRadius : 0f, + }; + } + + private void OnHandleState(EntityUid uid, TComp component, ref ComponentHandleState args) + { + if (args.Current is not SwitchableVisionOverlayComponentState state) + return; + + component.Color = state.Color; + component.FlashDurationMultiplier = state.FlashDurationMultiplier; + component.ActivateSound = state.ActivateSound; + component.DeactivateSound = state.DeactivateSound; + + if (component.ToggleAction != state.ToggleAction) + { + _actions.RemoveAction(uid, component.ToggleActionEntity); + component.ToggleAction = state.ToggleAction; + if (component.ToggleAction != null) + _actions.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction); + } + + if (component is IRHudComponent thermal) + thermal.LightRadius = state.LightRadius; + + if (component.IsActive == state.IsActive) + return; + + component.IsActive = state.IsActive; + + RaiseSwitchableOverlayToggledEvent(uid, + component.IsEquipment ? Transform(uid).ParentUid : uid, + component.IsActive); + } + + private void OnGetItemActions(Entity ent, ref GetItemActionsEvent args) + { + if (ent.Comp.IsEquipment && ent.Comp.ToggleAction != null && args.SlotFlags is not SlotFlags.POCKET and not null) + args.AddAction(ref ent.Comp.ToggleActionEntity, ent.Comp.ToggleAction); + } + + private void OnShutdown(EntityUid uid, TComp component, ComponentShutdown args) + { + if (!component.IsEquipment) + _actions.RemoveAction(uid, component.ToggleActionEntity); + } + + private void OnInit(EntityUid uid, TComp component, ComponentInit args) + { + component.PulseAccumulator = component.PulseTime; + } + + private void OnMapInit(EntityUid uid, TComp component, MapInitEvent args) + { + if (component is { IsEquipment: false, ToggleActionEntity: null, ToggleAction: not null }) + _actions.AddAction(uid, ref component.ToggleActionEntity, component.ToggleAction); + } + + private void OnToggle(EntityUid uid, TComp component, TEvent args) + { + Toggle(uid, component, !component.IsActive); + RaiseSwitchableOverlayToggledEvent(uid, args.Performer, component.IsActive); + args.Handled = true; + } + + private void Toggle(EntityUid uid, TComp component, bool activate, bool playSound = true) + { + if (playSound && _net.IsClient && _timing.IsFirstTimePredicted) + { + _audio.PlayEntity(activate ? component.ActivateSound : component.DeactivateSound, + Filter.Local(), + uid, + false); + } + + if (component.PulseTime > 0f) + { + component.PulseAccumulator = activate ? 0f : component.PulseTime; + return; + } + + component.IsActive = activate; + Dirty(uid, component); + } + + private void RaiseSwitchableOverlayToggledEvent(EntityUid uid, EntityUid user, bool activated) + { + var ev = new SwitchableOverlayToggledEvent(user, activated); + RaiseLocalEvent(uid, ref ev); + } +} + +[ByRefEvent] +public record struct SwitchableOverlayToggledEvent(EntityUid User, bool Activated); diff --git a/Content.Shared/_White/Overlays/SwitchableVisionOverlayComponent.cs b/Content.Shared/_White/Overlays/SwitchableVisionOverlayComponent.cs new file mode 100644 index 00000000000..ba63d687205 --- /dev/null +++ b/Content.Shared/_White/Overlays/SwitchableVisionOverlayComponent.cs @@ -0,0 +1,56 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._White.Overlays; + +public abstract partial class SwitchableVisionOverlayComponent : BaseVisionOverlayComponent +{ + [DataField] + public bool IsActive; + + [DataField] + public bool DrawOverlay = true; + + /// + /// Whether it should grant equipment enhanced vision or is it mob vision + /// + [DataField] + public bool IsEquipment; + + /// + /// If it is greater than 0, overlay isn't toggled but pulsed instead + /// + [DataField] + public float PulseTime; + + [ViewVariables(VVAccess.ReadOnly)] + public float PulseAccumulator; + + [DataField] + public float FlashDurationMultiplier = 1f; + + [DataField] + public SoundSpecifier? ActivateSound = new SoundPathSpecifier("/Audio/_White/Items/Goggles/activate.ogg"); + + [DataField] + public SoundSpecifier? DeactivateSound = new SoundPathSpecifier("/Audio/_White/Items/Goggles/deactivate.ogg"); + + [DataField] + public virtual EntProtoId? ToggleAction { get; set; } + + [ViewVariables] + public EntityUid? ToggleActionEntity; +} + +[Serializable, NetSerializable] +public sealed class SwitchableVisionOverlayComponentState : IComponentState +{ + public Color Color; + public bool IsActive; + public float FlashDurationMultiplier; + public SoundSpecifier? ActivateSound; + public SoundSpecifier? DeactivateSound; + public EntProtoId? ToggleAction; + public float LightRadius; +} diff --git a/Resources/Audio/_White/Items/Goggles/activate.ogg b/Resources/Audio/_White/Items/Goggles/activate.ogg new file mode 100644 index 00000000000..96cdb288fe0 Binary files /dev/null and b/Resources/Audio/_White/Items/Goggles/activate.ogg differ diff --git a/Resources/Audio/_White/Items/Goggles/attributions.yml b/Resources/Audio/_White/Items/Goggles/attributions.yml new file mode 100644 index 00000000000..7b1121f5423 --- /dev/null +++ b/Resources/Audio/_White/Items/Goggles/attributions.yml @@ -0,0 +1,9 @@ +- files: ["activate.ogg"] + license: "CC-BY-NC-SA-3.0" + copyright: "Taken from TGstation" + source: "https://github.com/tgstation/tgstation" + +- files: ["deactivate.ogg"] + license: "CC-BY-NC-SA-3.0" + copyright: "Taken from TGstation" + source: "https://github.com/tgstation/tgstation" \ No newline at end of file diff --git a/Resources/Audio/_White/Items/Goggles/deactivate.ogg b/Resources/Audio/_White/Items/Goggles/deactivate.ogg new file mode 100644 index 00000000000..e1e8f4fd82f Binary files /dev/null and b/Resources/Audio/_White/Items/Goggles/deactivate.ogg differ diff --git a/Resources/Locale/en-US/_Mono/armor-plate.ftl b/Resources/Locale/en-US/_Mono/armor-plate.ftl new file mode 100644 index 00000000000..286ed907bcc --- /dev/null +++ b/Resources/Locale/en-US/_Mono/armor-plate.ftl @@ -0,0 +1,34 @@ +armor-plate-break = Your {$plateName} has shattered! +armor-plate-examine-with-plate = Has a [color=yellow]{$plateName}[/color] installed. Durability: [color={$durabilityColor}]{$percent}%[/color] +armor-plate-examine-with-plate-simple = Has a [color=yellow]{$plateName}[/color] installed. +armor-plate-examine-no-plate = No armor plate installed. +armor-plate-examine-no-slot = No armor plate slot is available. + +armor-plate-examinable-verb-text = Plate attributes +armor-plate-examinable-verb-message = Examine protection and durability characteristics. + +armor-plate-attributes-examine = This armor plate: +armor-plate-initial-durability = Is rated for [color=yellow]{ $durability }[/color] standard units of damage. + +armor-plate-item-durability = Durability: [color={$durabilityColor}]{$percent}%[/color] + +armor-plate-gait-speed = speed +armor-plate-gait-walk = walking speed +armor-plate-gait-sprint = running speed + +armor-plate-speed-display = + { $deltasign -> + [-1] Increases your {$gait} by [color=yellow]{$speedPercent}%[/color]. + [0] Doesn't affect your speed. + [1] Decreases your {$gait} by [color=yellow]{$speedPercent}%[/color]. + *[other] Shouldn't be have this speed value! + } + +armor-plate-ratios-display = + { $deltasign -> + [-1] [color=cyan]Absorbs[/color] [color=yellow]{$ratioPercent}%[/color] of [color=yellow]{$dmgType}[/color] and takes it as [color=yellow]x{$multiplier}[/color] durability damage. + [0] Is unaffected by {$dmgType} + [1] [color=fuchsia]Amplifies[/color] [color=yellow]{$dmgType}[/color] by [color=yellow]{$ratioPercent}%[/color] and takes the added damage as [color=yellow]x{$multiplier}[/color] durability damage. + *[other] {$dmgType} shouldn't be have this absorption value! + } +armor-plate-stamina-value = Inflicts [color=yellow]{$multiplier}%[/color] of absorbed damage as stamina damage. diff --git a/Resources/Locale/en-US/_White/research/techologies.ftl b/Resources/Locale/en-US/_White/research/techologies.ftl new file mode 100644 index 00000000000..34d35e6e9f6 --- /dev/null +++ b/Resources/Locale/en-US/_White/research/techologies.ftl @@ -0,0 +1,2 @@ +research-technology-night-vision = Night Vision Technology +research-technology-thermal-vision = Thermal Vision Technology diff --git a/Resources/Locale/en-US/_White/store/uplink-catalog.ftl b/Resources/Locale/en-US/_White/store/uplink-catalog.ftl new file mode 100644 index 00000000000..3e4b40d90d6 --- /dev/null +++ b/Resources/Locale/en-US/_White/store/uplink-catalog.ftl @@ -0,0 +1,11 @@ +uplink-night-vision-name = Night Vision Goggles +uplink-night-vision-desc = They allow you to see in the dark, while looking like normal sunglasses! + +uplink-thermal-vision-name = Thermal Vision Goggles +uplink-thermal-vision-desc = They allow you to see living creatures regardless of obstacles, while looking like normal sunglasses! + +uplink-betrayal-knife-name = Betrayal Knife +uplink-betrayal-knife-desc = + Betrayal knife allows the user to blink a short distance, knocking down people in a small radius around blink position. + Deals significant damage when target is lying down or facing away from you. + Use it in your hand to toggle blink mode. diff --git a/Resources/Prototypes/_HL/Entities/Clothing/OuterClothing/armor.yml b/Resources/Prototypes/_HL/Entities/Clothing/OuterClothing/armor.yml index 31aeecc18cd..ab0f5e38e3f 100644 --- a/Resources/Prototypes/_HL/Entities/Clothing/OuterClothing/armor.yml +++ b/Resources/Prototypes/_HL/Entities/Clothing/OuterClothing/armor.yml @@ -84,3 +84,25 @@ sprite: _HL/Clothing/OuterClothing/pmc_armored_jacket_olive.rsi - type: Clothing sprite: _HL/Clothing/OuterClothing/pmc_armored_jacket_olive.rsi + +# Base item for testing purposes. + +- type: entity + parent: [ClothingOuterStorageBase, AllowSuitStorageClothing, BaseC3SyndicateContraband, ContrabandClothing, ClothingArmorPlateLight] # Frontier: BaseSyndicateContraband 1.0) { + grey = 1.0; + } + } + + // apply night vision color tint + color.rgb = mix(color.rgb, tint, grey); + + // add some noise for realism + lowp float noise = rand(FRAGCOORD.xy + TIME) * noise_amount / 10.0; + color.rgb += noise; + + COLOR = color; +}