diff --git a/Content.Client/Examine/ExamineSystem.cs b/Content.Client/Examine/ExamineSystem.cs index 37483e135f..6164df123e 100644 --- a/Content.Client/Examine/ExamineSystem.cs +++ b/Content.Client/Examine/ExamineSystem.cs @@ -1,4 +1,5 @@ using Content.Client.Verbs; +using Content.Shared.Eye.Blinding; using Content.Shared.Examine; using Content.Shared.IdentityManagement; using Content.Shared.Input; @@ -116,7 +117,7 @@ namespace Content.Client.Examine // Tooltips coming in from the server generally prioritize // opening at the old tooltip rather than the cursor/another entity, // since there's probably one open already if it's coming in from the server. - OpenTooltip(player.Value, ev.EntityUid, ev.CenterAtCursor, ev.OpenAtOldTooltip); + OpenTooltip(player.Value, ev.EntityUid, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget); UpdateTooltipInfo(player.Value, ev.EntityUid, ev.Message, ev.Verbs); } @@ -131,7 +132,7 @@ namespace Content.Client.Examine /// not fill it with information. This is done when the server sends examine info/verbs, /// or immediately if it's entirely clientside. /// - public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCursor=true, bool openAtOldTooltip=true) + public void OpenTooltip(EntityUid player, EntityUid target, bool centeredOnCursor=true, bool openAtOldTooltip=true, bool knowTarget = true) { // Close any examine tooltip that might already be opened // Before we do that, save its position. We'll prioritize opening any new popups there if @@ -191,11 +192,22 @@ namespace Content.Client.Examine }); } - hBox.AddChild(new Label + if (knowTarget) { - Text = Identity.Name(target, EntityManager, player), - HorizontalExpand = true, - }); + hBox.AddChild(new Label + { + Text = Identity.Name(target, EntityManager, player), + HorizontalExpand = true, + }); + } + else + { + hBox.AddChild(new Label + { + Text = "???", + HorizontalExpand = true, + }); + } panel.Measure(Vector2.Infinity); var size = Vector2.ComponentMax((minWidth, 0), panel.DesiredSize); @@ -294,7 +306,13 @@ namespace Content.Client.Examine return; FormattedMessage message; - OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false); + + // Basically this just predicts that we can't make out the entity if we have poor vision. + var canSeeClearly = true; + if (HasComp(playerEnt)) + canSeeClearly = false; + + OpenTooltip(playerEnt.Value, entity, centeredOnCursor, false, knowTarget: canSeeClearly); if (entity.IsClientSide()) { message = GetExamineText(entity, playerEnt); diff --git a/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs b/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs new file mode 100644 index 0000000000..4f957ae692 --- /dev/null +++ b/Content.Client/Eye/Blinding/BlurryVisionOverlay.cs @@ -0,0 +1,68 @@ +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; +using Content.Shared.Eye.Blinding; + +namespace Content.Client.Eye.Blinding +{ + public sealed class BlurryVisionOverlay : Overlay + { + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + private readonly ShaderInstance _blurryVisionXShader; + private readonly ShaderInstance _blurryVisionYShader; + private BlurryVisionComponent _blurryVisionComponent = default!; + + public BlurryVisionOverlay() + { + IoCManager.InjectDependencies(this); + _blurryVisionXShader = _prototypeManager.Index("BlurryVisionX").InstanceUnique(); + _blurryVisionYShader = _prototypeManager.Index("BlurryVisionY").InstanceUnique(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + var playerEntity = _playerManager.LocalPlayer?.ControlledEntity; + + if (playerEntity == null) + return false; + + if (!_entityManager.TryGetComponent(playerEntity, out var blurComp)) + return false; + + if (!blurComp.Active) + return false; + + if (_entityManager.TryGetComponent(playerEntity, out var blindComp) + && blindComp.Sources > 0) + return false; + + _blurryVisionComponent = blurComp; + return true; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return; + + _blurryVisionXShader?.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _blurryVisionXShader?.SetParameter("BLUR_AMOUNT", (_blurryVisionComponent.Magnitude / 10)); + _blurryVisionYShader?.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _blurryVisionYShader?.SetParameter("BLUR_AMOUNT", (_blurryVisionComponent.Magnitude / 10)); + + var worldHandle = args.WorldHandle; + var viewport = args.WorldBounds; + worldHandle.SetTransform(Matrix3.Identity); + worldHandle.UseShader(_blurryVisionXShader); + worldHandle.DrawRect(viewport, Color.White); + worldHandle.UseShader(_blurryVisionYShader); + worldHandle.DrawRect(viewport, Color.White); + } + } +} diff --git a/Content.Client/Eye/Blinding/BlurryVisionSystem.cs b/Content.Client/Eye/Blinding/BlurryVisionSystem.cs new file mode 100644 index 0000000000..3e8afd9e4d --- /dev/null +++ b/Content.Client/Eye/Blinding/BlurryVisionSystem.cs @@ -0,0 +1,61 @@ +using Content.Shared.Eye.Blinding; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.GameStates; + +namespace Content.Client.Eye.Blinding; + +public sealed class BlurryVisionSystem : EntitySystem +{ + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IOverlayManager _overlayMan = default!; + private BlurryVisionOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnBlurryInit); + SubscribeLocalEvent(OnBlurryShutdown); + + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + + SubscribeLocalEvent(OnHandleState); + + _overlay = new(); + } + + private void OnPlayerAttached(EntityUid uid, BlurryVisionComponent component, PlayerAttachedEvent args) + { + _overlayMan.AddOverlay(_overlay); + } + + private void OnPlayerDetached(EntityUid uid, BlurryVisionComponent component, PlayerDetachedEvent args) + { + _overlayMan.RemoveOverlay(_overlay); + } + + private void OnBlurryInit(EntityUid uid, BlurryVisionComponent component, ComponentInit args) + { + if (_player.LocalPlayer?.ControlledEntity == uid) + _overlayMan.AddOverlay(_overlay); + } + + private void OnBlurryShutdown(EntityUid uid, BlurryVisionComponent component, ComponentShutdown args) + { + if (_player.LocalPlayer?.ControlledEntity == uid) + { + _overlayMan.RemoveOverlay(_overlay); + } + } + + private void OnHandleState(EntityUid uid, BlurryVisionComponent component, ref ComponentHandleState args) + { + if (args.Current is not BlurryVisionComponentState state) + return; + + component.Magnitude = state.Magnitude; + } +} diff --git a/Content.Server/Chemistry/ReagentEffects/ChemHealEyeDamage.cs b/Content.Server/Chemistry/ReagentEffects/ChemHealEyeDamage.cs new file mode 100644 index 0000000000..622d6c41d3 --- /dev/null +++ b/Content.Server/Chemistry/ReagentEffects/ChemHealEyeDamage.cs @@ -0,0 +1,23 @@ +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Eye.Blinding; +using JetBrains.Annotations; + +namespace Content.Server.Chemistry.ReagentEffects +{ + /// + /// Heal eye damage (or deal) + /// + [UsedImplicitly] + public sealed class ChemHealEyeDamage : ReagentEffect + { + /// + /// Add or remove eye damage? + [DataField("add")] + public bool Add = false; + + public override void Effect(ReagentEffectArgs args) + { + EntitySystem.Get().AdjustEyeDamage(args.SolutionEntity, Add); + } + } +} diff --git a/Content.Server/Examine/ExamineSystem.cs b/Content.Server/Examine/ExamineSystem.cs index 9730bb337b..1229f94ce3 100644 --- a/Content.Server/Examine/ExamineSystem.cs +++ b/Content.Server/Examine/ExamineSystem.cs @@ -16,10 +16,14 @@ namespace Content.Server.Examine private static readonly FormattedMessage _entityNotFoundMessage; + private static readonly FormattedMessage _entityOutOfRangeMessage; + static ExamineSystem() { _entityNotFoundMessage = new FormattedMessage(); _entityNotFoundMessage.AddText(Loc.GetString("examine-system-entity-does-not-exist")); + _entityOutOfRangeMessage = new FormattedMessage(); + _entityOutOfRangeMessage.AddText(Loc.GetString("examine-system-cant-see-entity")); } public override void Initialize() @@ -54,14 +58,20 @@ namespace Content.Server.Examine var channel = player.ConnectedClient; if (session.AttachedEntity is not {Valid: true} playerEnt - || !EntityManager.EntityExists(request.EntityUid) - || !CanExamine(playerEnt, request.EntityUid)) + || !EntityManager.EntityExists(request.EntityUid)) { RaiseNetworkEvent(new ExamineSystemMessages.ExamineInfoResponseMessage( request.EntityUid, _entityNotFoundMessage), channel); return; } + if (!CanExamine(playerEnt, request.EntityUid)) + { + RaiseNetworkEvent(new ExamineSystemMessages.ExamineInfoResponseMessage( + request.EntityUid, _entityOutOfRangeMessage, knowTarget: false), channel); + return; + } + SortedSet? verbs = null; if (request.GetVerbs) verbs = _verbSystem.GetLocalVerbs(request.EntityUid, playerEnt, typeof(ExamineVerb)); diff --git a/Content.Server/Eye/Blinding/EyeProtection/EyeProtectionSystem.cs b/Content.Server/Eye/Blinding/EyeProtection/EyeProtectionSystem.cs new file mode 100644 index 0000000000..18b439e6a1 --- /dev/null +++ b/Content.Server/Eye/Blinding/EyeProtection/EyeProtectionSystem.cs @@ -0,0 +1,77 @@ +using Content.Shared.Eye.Blinding.EyeProtection; // why aren't tools predicted 🙂 +using Content.Shared.Eye.Blinding; +using Content.Shared.StatusEffect; +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Server.Tools; + +namespace Content.Server.Eye.Blinding.EyeProtection +{ + public sealed class EyeProtectionSystem : EntitySystem + { + [Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!; + [Dependency] private readonly SharedBlindingSystem _blindingSystem = default!; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnUseAttempt); + SubscribeLocalEvent(OnWelderToggled); + + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnUnequipped); + } + + private void OnUseAttempt(EntityUid uid, RequiresEyeProtectionComponent component, ToolUseAttemptEvent args) + { + if (!component.Toggled) + return; + + if (!TryComp(args.User, out var status) || !TryComp(args.User, out var blindable)) + return; + + if (blindable.Sources > 0) + return; + + float statusTime = (float) component.StatusEffectTime.TotalSeconds - blindable.BlindResistance; + + if (statusTime <= 0) + return; + + var statusTimeSpan = TimeSpan.FromSeconds(statusTime * (blindable.EyeDamage + 1)); + // Add permanent eye damage if they had zero protection, also scale their temporary blindness by how much they already accumulated. + if (_statusEffectsSystem.TryAddStatusEffect(args.User, SharedBlindingSystem.BlindingStatusEffect, statusTimeSpan, false, "TemporaryBlindness") && blindable.BlindResistance <= 0) + _blindingSystem.AdjustEyeDamage(args.User, true, blindable); + } + private void OnWelderToggled(EntityUid uid, RequiresEyeProtectionComponent component, WelderToggledEvent args) + { + component.Toggled = args.WelderOn; + } + + private void OnEquipped(EntityUid uid, EyeProtectionComponent component, GotEquippedEvent args) + { + if (!TryComp(uid, out var clothing) || clothing.Slots == SlotFlags.PREVENTEQUIP) + return; + + if (!clothing.Slots.HasFlag(args.SlotFlags)) + return; + + component.IsActive = true; + if (!TryComp(args.Equipee, out var blindComp)) + return; + + blindComp.BlindResistance += (float) component.ProtectionTime.TotalSeconds; + } + + private void OnUnequipped(EntityUid uid, EyeProtectionComponent component, GotUnequippedEvent args) + { + if (!component.IsActive) + return; + component.IsActive = false; + if (!TryComp(args.Equipee, out var blindComp)) + return; + + blindComp.BlindResistance -= (float) component.ProtectionTime.TotalSeconds; + } + } +} diff --git a/Content.Server/Tools/ToolSystem.Welder.cs b/Content.Server/Tools/ToolSystem.Welder.cs index 1a26904f31..31f3d9957b 100644 --- a/Content.Server/Tools/ToolSystem.Welder.cs +++ b/Content.Server/Tools/ToolSystem.Welder.cs @@ -104,6 +104,9 @@ namespace Content.Server.Tools welder.Lit = true; + var ev = new WelderToggledEvent(true); + RaiseLocalEvent(welder.Owner, ev, false); + if(item != null) _itemSystem.SetHeldPrefix(uid, "on", item); @@ -140,6 +143,9 @@ namespace Content.Server.Tools welder.Lit = false; + var ev = new WelderToggledEvent(false); + RaiseLocalEvent(welder.Owner, ev, false); + // TODO: Make all this use visualizers. if (item != null) _itemSystem.SetHeldPrefix(uid, "off", item); @@ -332,4 +338,14 @@ namespace Content.Server.Tools _welderTimer -= WelderUpdateTimer; } } + + public sealed class WelderToggledEvent : EntityEventArgs + { + public bool WelderOn; + + public WelderToggledEvent(bool welderOn) + { + WelderOn = welderOn; + } + } } diff --git a/Content.Shared/Examine/ExamineSystemMessages.cs b/Content.Shared/Examine/ExamineSystemMessages.cs index 628fedfd8c..01f410ae81 100644 --- a/Content.Shared/Examine/ExamineSystemMessages.cs +++ b/Content.Shared/Examine/ExamineSystemMessages.cs @@ -31,14 +31,17 @@ namespace Content.Shared.Examine public readonly bool CenterAtCursor; public readonly bool OpenAtOldTooltip; + public readonly bool KnowTarget; + public ExamineInfoResponseMessage(EntityUid entityUid, FormattedMessage message, List? verbs=null, - bool centerAtCursor=true, bool openAtOldTooltip=true) + bool centerAtCursor=true, bool openAtOldTooltip=true, bool knowTarget = true) { EntityUid = entityUid; Message = message; Verbs = verbs; CenterAtCursor = centerAtCursor; OpenAtOldTooltip = openAtOldTooltip; + KnowTarget = knowTarget; } } } diff --git a/Content.Shared/Examine/ExamineSystemShared.cs b/Content.Shared/Examine/ExamineSystemShared.cs index 48f98da212..2541be7cfa 100644 --- a/Content.Shared/Examine/ExamineSystemShared.cs +++ b/Content.Shared/Examine/ExamineSystemShared.cs @@ -101,6 +101,12 @@ namespace Content.Shared.Examine return DeadExamineRange; else if (MobStateSystem.IsCritical(examiner, mobState) || (TryComp(examiner, out var blind) && blind.Sources > 0)) return CritExamineRange; + + else if (TryComp(examiner, out var blurry) && blurry.Magnitude != 0) + { + float range = ExamineRange - (2 * (8 - blurry.Magnitude)); + return Math.Clamp(range, 2, 16); + } } return ExamineRange; } diff --git a/Content.Shared/Eye/Blinding/BlindableComponent.cs b/Content.Shared/Eye/Blinding/BlindableComponent.cs index 01a5dc64bd..d17a1ca37c 100644 --- a/Content.Shared/Eye/Blinding/BlindableComponent.cs +++ b/Content.Shared/Eye/Blinding/BlindableComponent.cs @@ -12,6 +12,24 @@ namespace Content.Shared.Eye.Blinding [DataField("sources")] public int Sources = 0; + /// + /// How many seconds will be subtracted from each attempt to add blindness to us? + /// + [DataField("blindResistance")] + public float BlindResistance = 0; + + /// + /// Replace with actual eye damage after bobby I guess + /// + [ViewVariables] + public int EyeDamage = 0; + + /// + /// Whether eye damage has accumulated enough to blind them. + /// + [ViewVariables] + public bool EyeTooDamaged = false; + /// /// Used to ensure that this doesn't break with sandbox or admin tools. /// This is not "enabled/disabled". diff --git a/Content.Shared/Eye/Blinding/BlurryVisionComponent.cs b/Content.Shared/Eye/Blinding/BlurryVisionComponent.cs new file mode 100644 index 0000000000..bbf518b2b4 --- /dev/null +++ b/Content.Shared/Eye/Blinding/BlurryVisionComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Eye.Blinding +{ + [RegisterComponent] + [NetworkedComponent] + public sealed class BlurryVisionComponent : Component + { + [DataField("mangitude")] + public float Magnitude = 1f; + + public bool Active => Magnitude < 10f; + } +} diff --git a/Content.Shared/Eye/Blinding/EyeProtection/EyeProtectionComponent.cs b/Content.Shared/Eye/Blinding/EyeProtection/EyeProtectionComponent.cs new file mode 100644 index 0000000000..0eb360b41b --- /dev/null +++ b/Content.Shared/Eye/Blinding/EyeProtection/EyeProtectionComponent.cs @@ -0,0 +1,18 @@ +namespace Content.Shared.Eye.Blinding.EyeProtection +{ + /// + /// For welding masks, sunglasses, etc. + /// + [RegisterComponent] + public sealed class EyeProtectionComponent : Component + { + /// + /// How many seconds to subtract from the status effect. If it's greater than the source + /// of blindness, do not blind. + /// + [DataField("protectionTime")] + public TimeSpan ProtectionTime = TimeSpan.FromSeconds(10); + + public bool IsActive = false; + } +} diff --git a/Content.Shared/Eye/Blinding/EyeProtection/RequiresEyeProtectionComponent.cs b/Content.Shared/Eye/Blinding/EyeProtection/RequiresEyeProtectionComponent.cs new file mode 100644 index 0000000000..7ec3eefab3 --- /dev/null +++ b/Content.Shared/Eye/Blinding/EyeProtection/RequiresEyeProtectionComponent.cs @@ -0,0 +1,21 @@ +namespace Content.Shared.Eye.Blinding.EyeProtection +{ + /// + /// For tools like welders that will damage your eyes when you use them. + /// + [RegisterComponent] + public sealed class RequiresEyeProtectionComponent : Component + { + /// + /// How long to apply temporary blindness to the user. + /// + [DataField("statusEffectTime")] + public TimeSpan StatusEffectTime = TimeSpan.FromSeconds(10); + + /// + /// You probably want to turn this on in yaml if it's something always on and not a welder. + /// + [DataField("toggled")] + public bool Toggled = false; + } +} diff --git a/Content.Shared/Eye/Blinding/SharedBlindingSystem.cs b/Content.Shared/Eye/Blinding/SharedBlindingSystem.cs index 8f5cec54f9..3699df88bb 100644 --- a/Content.Shared/Eye/Blinding/SharedBlindingSystem.cs +++ b/Content.Shared/Eye/Blinding/SharedBlindingSystem.cs @@ -1,18 +1,28 @@ using Content.Shared.Clothing.Components; using Content.Shared.Inventory.Events; using Content.Shared.Inventory; -using Content.Shared.Item; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; using JetBrains.Annotations; namespace Content.Shared.Eye.Blinding { public sealed class SharedBlindingSystem : EntitySystem { + public const string BlindingStatusEffect = "TemporaryBlindness"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnEquipped); SubscribeLocalEvent(OnUnequipped); + + SubscribeLocalEvent(OnGlassesEquipped); + SubscribeLocalEvent(OnGlassesUnequipped); + + SubscribeLocalEvent(OnGetState); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnShutdown); } private void OnEquipped(EntityUid uid, BlindfoldComponent component, GotEquippedEvent args) @@ -39,6 +49,46 @@ namespace Content.Shared.Eye.Blinding AdjustBlindSources(args.Equipee, false, blindComp); } + private void OnGlassesEquipped(EntityUid uid, VisionCorrectionComponent component, GotEquippedEvent args) + { + if (!TryComp(uid, out var clothing) || clothing.Slots == SlotFlags.PREVENTEQUIP) // we live in a society + return; + // Is the clothing in its actual slot? + if (!clothing.Slots.HasFlag(args.SlotFlags)) + return; + + if (!TryComp(args.Equipee, out var blur)) + return; + + component.IsActive = true; + blur.Magnitude += component.VisionBonus; + blur.Dirty(); + } + + private void OnGlassesUnequipped(EntityUid uid, VisionCorrectionComponent component, GotUnequippedEvent args) + { + if (!component.IsActive || !TryComp(args.Equipee, out var blur)) + return; + component.IsActive = false; + blur.Magnitude -= component.VisionBonus; + blur.Dirty(); + } + + private void OnGetState(EntityUid uid, BlurryVisionComponent component, ref ComponentGetState args) + { + args.State = new BlurryVisionComponentState(component.Magnitude); + } + + private void OnInit(EntityUid uid, TemporaryBlindnessComponent component, ComponentInit args) + { + AdjustBlindSources(uid, true); + } + + private void OnShutdown(EntityUid uid, TemporaryBlindnessComponent component, ComponentShutdown args) + { + AdjustBlindSources(uid, false); + } + [PublicAPI] public void AdjustBlindSources(EntityUid uid, bool Add, BlindableComponent? blindable = null) { @@ -52,6 +102,56 @@ namespace Content.Shared.Eye.Blinding { blindable.Sources--; } + + blindable.Sources = Math.Max(blindable.Sources, 0); + } + + public void AdjustEyeDamage(EntityUid uid, bool add, BlindableComponent? blindable = null) + { + if (!Resolve(uid, ref blindable, false)) + return; + + if (add) + { + blindable.EyeDamage++; + } else + { + blindable.EyeDamage--; + } + + if (blindable.EyeDamage > 0) + { + var blurry = EnsureComp(uid); + blurry.Magnitude = (9 - blindable.EyeDamage); + blurry.Dirty(); + } else + { + RemComp(uid); + } + + if (!blindable.EyeTooDamaged && blindable.EyeDamage >= 8) + { + blindable.EyeTooDamaged = true; + AdjustBlindSources(uid, true, blindable); + } + if (blindable.EyeTooDamaged && blindable.EyeDamage < 8) + { + blindable.EyeTooDamaged = false; + AdjustBlindSources(uid, false, blindable); + } + + blindable.EyeDamage = Math.Clamp(blindable.EyeDamage, 0, 8); + } + } + + // I have no idea why blurry vision needs this but blindness doesn't + [Serializable, NetSerializable] + public sealed class BlurryVisionComponentState : ComponentState + { + public float Magnitude; + public BlurryVisionComponentState(float magnitude) + { + Magnitude = magnitude; } } } diff --git a/Content.Shared/Eye/Blinding/TemporaryBlindnessComponent.cs b/Content.Shared/Eye/Blinding/TemporaryBlindnessComponent.cs new file mode 100644 index 0000000000..0c6ccc3980 --- /dev/null +++ b/Content.Shared/Eye/Blinding/TemporaryBlindnessComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Eye.Blinding +{ + /// + /// Blind status effect. + /// + [NetworkedComponent, RegisterComponent] + public sealed class TemporaryBlindnessComponent : Component + {} +} diff --git a/Content.Shared/Eye/Blinding/VisionCorrectionComponent.cs b/Content.Shared/Eye/Blinding/VisionCorrectionComponent.cs new file mode 100644 index 0000000000..dbc9750c59 --- /dev/null +++ b/Content.Shared/Eye/Blinding/VisionCorrectionComponent.cs @@ -0,0 +1,15 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Eye.Blinding +{ + [RegisterComponent] + [NetworkedComponent] + public sealed class VisionCorrectionComponent : Component + { + [ViewVariables] + public bool IsActive = false; + + [DataField("visionBonus")] + public float VisionBonus = 3f; + } +} diff --git a/Resources/Locale/en-US/examine/examine-system.ftl b/Resources/Locale/en-US/examine/examine-system.ftl index d10b04c82b..96a904dbfd 100644 --- a/Resources/Locale/en-US/examine/examine-system.ftl +++ b/Resources/Locale/en-US/examine/examine-system.ftl @@ -2,6 +2,8 @@ examine-system-entity-does-not-exist = That entity doesn't exist +examine-system-cant-see-entity = You can't make out whatever that is. + examine-verb-name = Basic examinable-anchored = It is anchored to the floor diff --git a/Resources/Locale/en-US/reagents/meta/medicine.ftl b/Resources/Locale/en-US/reagents/meta/medicine.ftl index 86ffaffc9a..64211fe4e8 100644 --- a/Resources/Locale/en-US/reagents/meta/medicine.ftl +++ b/Resources/Locale/en-US/reagents/meta/medicine.ftl @@ -85,5 +85,8 @@ reagent-desc-omnizine = A soothing milky liquid with an iridescent gleam. A well reagent-name-ultravasculine = ultravasculine reagent-desc-ultravasculine = Rapidly flushes toxins from the body, but places some stress on the veins. Do not overdose. +reagent-name-oculine = oculine +reagent-desc-oculine = Heals eye damage. + reagent-name-ethylredoxrazine = ethylredoxrazine reagent-desc-ethylredoxrazine = Neutralises the effects of alcohol in the blood stream. Though it is commonly needed, it is rarely requested. diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/engivend.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/engivend.yml index b35681f854..dab8609747 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/engivend.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/engivend.yml @@ -5,6 +5,7 @@ spriteName: engivend startingInventory: ClothingEyesGlassesMeson: 4 + ClothingHeadHatWelding: 4 Multitool: 4 PowerCellMedium: 5 ClothingHandsGlovesColorYellow: 6 diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml index d18a070b2d..b1d4981578 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml @@ -65,17 +65,14 @@ - type: entity parent: ClothingEyesBase id: ClothingEyesGlassesMeson - name: optical meson scanners - description: The pinnacle of modern science, wallhacks in real life + name: engineering goggles #less confusion + description: Green-tinted goggles using a proprietary polymer that provides protection from eye damage of all types. components: - type: Sprite sprite: Clothing/Eyes/Glasses/meson.rsi - type: Clothing sprite: Clothing/Eyes/Glasses/meson.rsi - - type: Armor - modifiers: - coefficients: - Radiation: 0.5 + - type: EyeProtection - type: entity parent: ClothingEyesBase @@ -87,6 +84,7 @@ sprite: Clothing/Eyes/Glasses/glasses.rsi - type: Clothing sprite: Clothing/Eyes/Glasses/glasses.rsi + - type: VisionCorrection - type: entity parent: ClothingEyesBase @@ -99,6 +97,8 @@ - type: Clothing sprite: Clothing/Eyes/Glasses/sunglasses.rsi - type: FlashImmunity + - type: EyeProtection + protectionTime: 5 - type: entity parent: ClothingEyesBase @@ -111,6 +111,8 @@ - type: Clothing sprite: Clothing/Eyes/Glasses/secglasses.rsi - type: FlashImmunity + - type: EyeProtection + protectionTime: 5 #Make a scanner category when these actually function and we get the trayson - type: entity diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/misc.yml b/Resources/Prototypes/Entities/Clothing/Eyes/misc.yml index ec91b8cadd..a213a06afc 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/misc.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/misc.yml @@ -8,6 +8,8 @@ sprite: Clothing/Eyes/Misc/eyepatch.rsi - type: Clothing sprite: Clothing/Eyes/Misc/eyepatch.rsi + - type: EyeProtection + protectionTime: 5 - type: entity parent: ClothingEyesBase diff --git a/Resources/Prototypes/Entities/Clothing/Head/welding.yml b/Resources/Prototypes/Entities/Clothing/Head/welding.yml index 8cc3d7c40f..f410104784 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/welding.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/welding.yml @@ -7,6 +7,7 @@ - type: IngestionBlocker - type: FlashImmunity - type: IdentityBlocker + - type: EyeProtection - type: entity parent: WeldingMaskBase diff --git a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml index 906a5ea817..2cd0aeb434 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml @@ -36,6 +36,8 @@ - type: Clothing sprite: Clothing/Mask/gassyndicate.rsi - type: FlashImmunity + - type: EyeProtection + protectionTime: 5 - type: entity parent: ClothingMaskGas diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml index a5717cfb37..8b83d9298c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/simplemob.yml @@ -112,6 +112,7 @@ - Stutter - Electrocution - ForcedSleep + - TemporaryBlindness - type: Body template: AnimalTemplate preset: AnimalPreset @@ -179,6 +180,7 @@ - Stutter - Electrocution - ForcedSleep + - TemporaryBlindness - type: ThermalRegulator metabolismHeat: 800 radiatedHeat: 100 diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 066c2a8088..6fdeff608d 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -80,6 +80,7 @@ - PressureImmunity - Muted - ForcedSleep + - TemporaryBlindness - type: DiseaseCarrier - type: Blindable # Other diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml index 8d1148555f..454ed331be 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml @@ -108,6 +108,17 @@ - state: carrot - type: SliceableFood slice: FoodCakeCarrotSlice + - type: SolutionContainerManager + solutions: + food: + maxVol: 30 + reagents: + - ReagentId: JuiceCarrot + Quantity: 15 + - ReagentId: Sugar + Quantity: 5 + - ReagentId: Vitamin + Quantity: 5 - type: entity name: slice of carrot cake @@ -120,6 +131,18 @@ - state: plate-small - state: plate-slice-shading - state: carrot-slice + - type: SolutionContainerManager + solutions: + food: + maxVol: 6 + reagents: + - ReagentId: JuiceCarrot + Quantity: 3 + - ReagentId: Sugar + Quantity: 1 + - ReagentId: Vitamin + Quantity: 1 + # Tastes like sweetness, cake, carrot. - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donkpocket.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donkpocket.yml index 2d5b480e34..77db03e3df 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donkpocket.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donkpocket.yml @@ -265,5 +265,7 @@ reagents: - ReagentId: Nutriment Quantity: 2 + - ReagentId: JuiceCarrot + Quantity: 1 - type: Sprite state: dink diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml index ef2d59f253..6f3c466af0 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml @@ -74,7 +74,8 @@ - type: Sprite layers: - state: margherita-slice -# Tastes like crust, tomato, cheese. + - type: SliceableFood + slice: FoodPizzaMeatSlice - type: entity name: meat pizza @@ -133,6 +134,17 @@ - state: vegetable - type: SliceableFood slice: FoodPizzaVegetableSlice + - type: SolutionContainerManager + solutions: + food: + maxVol: 40 + reagents: + - ReagentId: Nutriment + Quantity: 25 + - ReagentId: JuiceCarrot + Quantity: 5 + - ReagentId: Vitamin + Quantity: 5 - type: entity name: slice of vegetable pizza @@ -143,6 +155,18 @@ - type: Sprite layers: - state: vegetable-slice + - type: SolutionContainerManager + solutions: + food: + maxVol: 40 + reagents: + - ReagentId: Nutriment + Quantity: 4 + - ReagentId: JuiceCarrot + Quantity: 1 + - ReagentId: Vitamin + Quantity: 1 + # Tastes like crust, tomato, cheese, carrot. - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/meals.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/meals.yml index e5344b8704..305aa5d762 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/meals.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/meals.yml @@ -61,6 +61,14 @@ components: - type: Sprite state: fries-carrot + netsync: false + - type: SolutionContainerManager + solutions: + food: + maxVol: 26 + reagents: + - ReagentId: JuiceCarrot + Quantity: 20 # Tastes like carrots, salt. - type: entity diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index d1d4de75fc..9f7c69aee0 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -228,7 +228,7 @@ food: maxVol: 9 reagents: - - ReagentId: Nutriment + - ReagentId: JuiceCarrot Quantity: 5 - ReagentId: Vitamin Quantity: 4 diff --git a/Resources/Prototypes/Entities/Objects/Tools/welders.yml b/Resources/Prototypes/Entities/Objects/Tools/welders.yml index 95f7c876c4..d3c694f7c1 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/welders.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/welders.yml @@ -50,6 +50,7 @@ radius: 1.5 color: orange - type: Appearance + - type: RequiresEyeProtection - type: entity name: industrial welding tool diff --git a/Resources/Prototypes/Hydroponics/seeds.yml b/Resources/Prototypes/Hydroponics/seeds.yml index 6e81820dd7..cf7985e526 100644 --- a/Resources/Prototypes/Hydroponics/seeds.yml +++ b/Resources/Prototypes/Hydroponics/seeds.yml @@ -86,7 +86,7 @@ growthStages: 3 waterConsumption: 6 chemicals: - Nutriment: + JuiceCarrot: Min: 1 Max: 5 PotencyDivisor: 20 diff --git a/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml b/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml index 08ff7dc3e7..e8e4f83808 100644 --- a/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml +++ b/Resources/Prototypes/Reagents/Consumable/Drink/juice.yml @@ -57,6 +57,17 @@ desc: reagent-desc-juice-carrot physicalDesc: reagent-physical-desc-crisp color: "#FF8820" + metabolisms: + Drink: + effects: + - !type:SatiateThirst + factor: 2 + - !type:AdjustReagent + reagent: Oculine + amount: 0.15 + - !type:AdjustReagent + reagent: Nutriment + amount: 0.5 - type: reagent id: JuiceGrape diff --git a/Resources/Prototypes/Reagents/medicine.yml b/Resources/Prototypes/Reagents/medicine.yml index bad3d3b318..b151f19511 100644 --- a/Resources/Prototypes/Reagents/medicine.yml +++ b/Resources/Prototypes/Reagents/medicine.yml @@ -658,3 +658,14 @@ reagent: Ultravasculine amount: 0.5 +- type: reagent + id: Oculine + name: reagent-name-oculine + desc: reagent-desc-oculine + group: Medicine + physicalDesc: reagent-physical-desc-translucent + color: "#404040" + metabolisms: + Medicine: + effects: + - !type:ChemHealEyeDamage diff --git a/Resources/Prototypes/Recipes/Reactions/medicine.yml b/Resources/Prototypes/Recipes/Reactions/medicine.yml index b12c5b3f30..2cdafb1096 100644 --- a/Resources/Prototypes/Recipes/Reactions/medicine.yml +++ b/Resources/Prototypes/Recipes/Reactions/medicine.yml @@ -323,6 +323,20 @@ products: Ultravasculine: 2 +- type: reaction + id: Oculine + reactants: + TableSalt: + amount: 1 + Blood: + amount: 1 + Carbon: + amount: 1 + Hydrogen: + amount: 1 + products: + Oculine: 4 + - type: reaction id: Siderlac reactants: diff --git a/Resources/Prototypes/Shaders/shaders.yml b/Resources/Prototypes/Shaders/shaders.yml index c362fa31a0..75b29b34fa 100644 --- a/Resources/Prototypes/Shaders/shaders.yml +++ b/Resources/Prototypes/Shaders/shaders.yml @@ -55,3 +55,13 @@ pixelSize: 32, 32 alphaCutoff: 0 removeTransparency: false + +- type: shader + id: BlurryVisionX + kind: source + path: "/Textures/Shaders/blurryx.swsl" + +- type: shader + id: BlurryVisionY + kind: source + path: "/Textures/Shaders/blurryy.swsl" diff --git a/Resources/Prototypes/status_effects.yml b/Resources/Prototypes/status_effects.yml index 078fd117e1..960f51ea23 100644 --- a/Resources/Prototypes/status_effects.yml +++ b/Resources/Prototypes/status_effects.yml @@ -48,3 +48,5 @@ - type: statusEffect id: ForcedSleep #I.e., they will not wake on damage or similar +- type: statusEffect + id: TemporaryBlindness diff --git a/Resources/Textures/Shaders/blurryx.swsl b/Resources/Textures/Shaders/blurryx.swsl new file mode 100644 index 0000000000..5ffbb3d8fc --- /dev/null +++ b/Resources/Textures/Shaders/blurryx.swsl @@ -0,0 +1,16 @@ +uniform sampler2D SCREEN_TEXTURE; +uniform highp float BLUR_AMOUNT; + +void fragment() { + highp vec3 col = texture(SCREEN_TEXTURE, UV).xyz * BLUR_AMOUNT; + highp vec4 color = zTexture(UV); + col += texture(SCREEN_TEXTURE, UV + vec2(SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.15; + col += texture(SCREEN_TEXTURE, UV + vec2(-SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.15; + col += texture(SCREEN_TEXTURE, UV + vec2(2.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.12; + col += texture(SCREEN_TEXTURE, UV + vec2(2.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.12; + col += texture(SCREEN_TEXTURE, UV + vec2(3.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.09; + col += texture(SCREEN_TEXTURE, UV + vec2(3.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.09; + col += texture(SCREEN_TEXTURE, UV + vec2(4.0 * SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.05; + col += texture(SCREEN_TEXTURE, UV + vec2(4.0 * -SCREEN_PIXEL_SIZE.x, 0.0)).xyz * 0.05; + COLOR = vec4(vec3(col), color.a); +} diff --git a/Resources/Textures/Shaders/blurryy.swsl b/Resources/Textures/Shaders/blurryy.swsl new file mode 100644 index 0000000000..eea24e531d --- /dev/null +++ b/Resources/Textures/Shaders/blurryy.swsl @@ -0,0 +1,16 @@ +uniform sampler2D SCREEN_TEXTURE; +uniform highp float BLUR_AMOUNT; + +void fragment() { + highp vec3 col = texture(SCREEN_TEXTURE, UV).xyz * BLUR_AMOUNT; + highp vec4 color = zTexture(UV); + col += texture(SCREEN_TEXTURE, UV + vec2(0.0, SCREEN_PIXEL_SIZE.y)).xyz * 0.15; + col += texture(SCREEN_TEXTURE, UV + vec2(0.0, -SCREEN_PIXEL_SIZE.y)).xyz * 0.15; + col += texture(SCREEN_TEXTURE, UV + vec2(0.0, 2.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.12; + col += texture(SCREEN_TEXTURE, UV + vec2(0.0, 2.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.12; + col += texture(SCREEN_TEXTURE, UV + vec2(0.0, 3.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.09; + col += texture(SCREEN_TEXTURE, UV + vec2(0.0, 3.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.09; + col += texture(SCREEN_TEXTURE, UV + vec2(0.0, 4.0 * SCREEN_PIXEL_SIZE.y)).xyz * 0.05; + col += texture(SCREEN_TEXTURE, UV + vec2(0.0, 4.0 * -SCREEN_PIXEL_SIZE.y)).xyz * 0.05; + COLOR = vec4(vec3(col), color.a); +}