From 67e4b626c1c490f98950add60f42c4465cf049a1 Mon Sep 17 00:00:00 2001 From: PrPleGoo Date: Tue, 18 Jul 2023 08:01:22 +0200 Subject: [PATCH] Hunger and thirst HUDs (#18066) * security HUD now shows a job icon on entities with a body * thirst goggles * set starting hud gear --- .../ComponentAddedOverlaySystemBase.cs | 115 ++++++ .../Overlays/ShowHungerIconsSystem.cs | 74 ++++ .../Overlays/ShowSecurityIconsSystem.cs | 90 +++++ .../Overlays/ShowThirstIconsSystem.cs | 73 ++++ .../StatusIcon/StatusIconOverlay.cs | 14 +- .../Chemistry/ReagentEffects/SatiateThirst.cs | 4 +- Content.Server/Medical/VomitSystem.cs | 5 - .../Nutrition/Components/ThirstComponent.cs | 54 --- .../Nutrition/Components/ThirstComponent.cs | 101 +++++ .../Nutrition/EntitySystems/ThirstSystem.cs | 77 +++- .../Overlays/ShowHungerIconsComponent.cs | 8 + .../Overlays/ShowSecurityIconsComponent.cs | 8 + .../Overlays/ShowThirstIconsComponent.cs | 8 + .../StatusIcon/StatusIconPrototype.cs | 17 +- .../Entities/Clothing/Eyes/glasses.yml | 11 - .../Prototypes/Entities/Clothing/Eyes/hud.yml | 37 ++ .../Roles/Jobs/Civilian/bartender.yml | 1 + .../Prototypes/Roles/Jobs/Civilian/chef.yml | 1 + Resources/Prototypes/StatusEffects/hunger.yml | 49 +++ Resources/Prototypes/StatusEffects/job.yml | 375 ++++++++++++++++++ .../beergoggles.rsi/equipped-EYES.png | Bin .../{Glasses => Hud}/beergoggles.rsi/icon.png | Bin .../beergoggles.rsi/meta.json | 0 .../friedoniongoggles.rsi/equipped-EYES.png | Bin .../friedoniongoggles.rsi/icon.png | Bin .../friedoniongoggles.rsi/meta.json | 0 26 files changed, 1027 insertions(+), 95 deletions(-) create mode 100644 Content.Client/Overlays/ComponentAddedOverlaySystemBase.cs create mode 100644 Content.Client/Overlays/ShowHungerIconsSystem.cs create mode 100644 Content.Client/Overlays/ShowSecurityIconsSystem.cs create mode 100644 Content.Client/Overlays/ShowThirstIconsSystem.cs delete mode 100644 Content.Server/Nutrition/Components/ThirstComponent.cs create mode 100644 Content.Shared/Nutrition/Components/ThirstComponent.cs rename {Content.Server => Content.Shared}/Nutrition/EntitySystems/ThirstSystem.cs (71%) create mode 100644 Content.Shared/Overlays/ShowHungerIconsComponent.cs create mode 100644 Content.Shared/Overlays/ShowSecurityIconsComponent.cs create mode 100644 Content.Shared/Overlays/ShowThirstIconsComponent.cs create mode 100644 Resources/Prototypes/StatusEffects/hunger.yml create mode 100644 Resources/Prototypes/StatusEffects/job.yml rename Resources/Textures/Clothing/Eyes/{Glasses => Hud}/beergoggles.rsi/equipped-EYES.png (100%) rename Resources/Textures/Clothing/Eyes/{Glasses => Hud}/beergoggles.rsi/icon.png (100%) rename Resources/Textures/Clothing/Eyes/{Glasses => Hud}/beergoggles.rsi/meta.json (100%) rename Resources/Textures/Clothing/Eyes/{Glasses => Hud}/friedoniongoggles.rsi/equipped-EYES.png (100%) rename Resources/Textures/Clothing/Eyes/{Glasses => Hud}/friedoniongoggles.rsi/icon.png (100%) rename Resources/Textures/Clothing/Eyes/{Glasses => Hud}/friedoniongoggles.rsi/meta.json (100%) diff --git a/Content.Client/Overlays/ComponentAddedOverlaySystemBase.cs b/Content.Client/Overlays/ComponentAddedOverlaySystemBase.cs new file mode 100644 index 0000000000..7281f57dcb --- /dev/null +++ b/Content.Client/Overlays/ComponentAddedOverlaySystemBase.cs @@ -0,0 +1,115 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.GameTicking; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Robust.Client.GameObjects; +using Robust.Client.Player; + +namespace Content.Client.Overlays +{ + public abstract class ComponentAddedOverlaySystemBase : EntitySystem where T : IComponent + { + [Dependency] private readonly IPlayerManager _player = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + private InventorySystem _invSystem = default!; + + protected bool IsActive = false; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + + SubscribeLocalEvent(OnCompEquip); + SubscribeLocalEvent(OnCompUnequip); + + SubscribeLocalEvent(OnRoundRestart); + + _invSystem = _entityManager.System(); + } + + public void ApplyOverlay(T component) + { + IsActive = true; + OnApplyOverlay(component); + } + + public void RemoveOverlay() + { + IsActive = false; + OnRemoveOverlay(); + } + + protected virtual void OnApplyOverlay(T component) { } + + protected virtual void OnRemoveOverlay() { } + + private void OnInit(EntityUid uid, T component, ComponentInit args) + { + if (_player.LocalPlayer?.ControlledEntity == uid) + { + ApplyOverlay(component); + } + } + + private void OnRemove(EntityUid uid, T component, ComponentRemove args) + { + if (_player.LocalPlayer?.ControlledEntity == uid) + { + RemoveOverlay(); + } + } + + private void OnPlayerAttached(PlayerAttachedEvent args) + { + if (TryComp(args.Entity, out var component)) + ApplyOverlay(component); + + if (TryComp(args.Entity, out InventoryComponent? inventoryComponent) + && _invSystem.TryGetSlots(args.Entity, out var slotDefinitions, inventoryComponent)) + { + foreach (var slot in slotDefinitions) + { + if (_invSystem.TryGetSlotEntity(args.Entity, slot.Name, out var itemUid) + && TryComp(itemUid.Value, out component)) + { + OnCompEquip(itemUid.Value, component, new GotEquippedEvent(args.Entity, itemUid.Value, slot)); + } + } + } + } + + private void OnPlayerDetached(PlayerDetachedEvent args) + { + RemoveOverlay(); + } + + private void OnCompEquip(EntityUid uid, T component, GotEquippedEvent args) + { + if (!TryComp(uid, out var clothing)) return; + + if (args.Equipee != _player.LocalPlayer?.ControlledEntity) return; + + if (!clothing.Slots.HasFlag(args.SlotFlags)) return; + + ApplyOverlay(component); + } + + private void OnCompUnequip(EntityUid uid, T component, GotUnequippedEvent args) + { + if (args.Equipee != _player.LocalPlayer?.ControlledEntity) return; + + RemoveOverlay(); + } + + private void OnRoundRestart(RoundRestartCleanupEvent args) + { + RemoveOverlay(); + } + } +} diff --git a/Content.Client/Overlays/ShowHungerIconsSystem.cs b/Content.Client/Overlays/ShowHungerIconsSystem.cs new file mode 100644 index 0000000000..ed88128a57 --- /dev/null +++ b/Content.Client/Overlays/ShowHungerIconsSystem.cs @@ -0,0 +1,74 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Overlays; +using Content.Shared.StatusIcon; +using Content.Shared.StatusIcon.Components; +using Robust.Shared.Prototypes; + +namespace Content.Client.Overlays +{ + public sealed class ShowHungerIconsSystem : ComponentAddedOverlaySystemBase + { + [Dependency] private readonly IPrototypeManager _prototypeMan = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + + private StatusIconPrototype? _overfed; + private StatusIconPrototype? _peckish; + private StatusIconPrototype? _starving; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetStatusIconsEvent); + + } + + private void OnGetStatusIconsEvent(EntityUid uid, HungerComponent hungerComponent, ref GetStatusIconsEvent @event) + { + if (!IsActive) + return; + + var healthIcons = DecideHungerIcon(uid, hungerComponent); + + @event.StatusIcons.AddRange(healthIcons); + } + + private IReadOnlyList DecideHungerIcon(EntityUid uid, HungerComponent hungerComponent) + { + var result = new List(); + + if (_entManager.TryGetComponent(uid, out var metaDataComponent) && + metaDataComponent.Flags.HasFlag(MetaDataFlags.InContainer)) + { + return result; + } + + switch (hungerComponent.CurrentThreshold) + { + case HungerThreshold.Overfed: + if (_overfed != null || + _prototypeMan.TryIndex("HungerIcon_Overfed", out _overfed)) + { + result.Add(_overfed); + } + break; + case HungerThreshold.Peckish: + if (_peckish != null || + _prototypeMan.TryIndex("HungerIcon_Peckish", out _peckish)) + { + result.Add(_peckish); + } + break; + case HungerThreshold.Starving: + if (_starving != null || + _prototypeMan.TryIndex("HungerIcon_Starving", out _starving)) + { + result.Add(_starving); + } + break; + } + + return result; + } + } +} diff --git a/Content.Client/Overlays/ShowSecurityIconsSystem.cs b/Content.Client/Overlays/ShowSecurityIconsSystem.cs new file mode 100644 index 0000000000..7459d2ab6d --- /dev/null +++ b/Content.Client/Overlays/ShowSecurityIconsSystem.cs @@ -0,0 +1,90 @@ +using Content.Shared.Access.Components; +using Content.Shared.Body.Components; +using Content.Shared.Inventory; +using Content.Shared.Overlays; +using Content.Shared.PDA; +using Content.Shared.StatusIcon; +using Content.Shared.StatusIcon.Components; +using Robust.Shared.Prototypes; + +namespace Content.Client.Overlays +{ + public sealed class ShowSecurityIconsSystem : ComponentAddedOverlaySystemBase + { + [Dependency] private readonly IPrototypeManager _prototypeMan = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + + private Dictionary _jobIcons = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetStatusIconsEvent); + } + + private void OnGetStatusIconsEvent(EntityUid uid, BodyComponent _, ref GetStatusIconsEvent @event) + { + if (!IsActive) + return; + + var healthIcons = DecideSecurityIcon(uid); + + @event.StatusIcons.AddRange(healthIcons); + } + + private IReadOnlyList DecideSecurityIcon(EntityUid uid) + { + var result = new List(); + if (_entManager.TryGetComponent(uid, out var metaDataComponent) && + metaDataComponent.Flags.HasFlag(MetaDataFlags.InContainer)) + { + return result; + } + + var iconToGet = "NoId"; + if (_inventorySystem.TryGetSlotEntity(uid, "id", out var idUid)) + { + // PDA + if (EntityManager.TryGetComponent(idUid, out PdaComponent? pda)) + { + iconToGet = pda.ContainedId?.JobTitle ?? string.Empty; + } + // ID Card + else if (EntityManager.TryGetComponent(idUid, out IdCardComponent? id)) + { + iconToGet = id.JobTitle ?? string.Empty; + } + + iconToGet = iconToGet.Replace(" ", ""); + } + + iconToGet = EnsureIcon(iconToGet, _jobIcons); + result.Add(_jobIcons[iconToGet]); + + // Add arrest icons here, WYCI. + + return result; + } + + private string EnsureIcon(string iconKey, Dictionary icons) + { + if (!icons.ContainsKey(iconKey)) + { + if (_prototypeMan.TryIndex($"JobIcon_{iconKey}", out var securityIcon)) + { + icons.Add(iconKey, securityIcon); + return iconKey; + } + } + else + { + return iconKey; + } + + iconKey = "Unknown"; + return EnsureIcon(iconKey, icons); + } + } +} diff --git a/Content.Client/Overlays/ShowThirstIconsSystem.cs b/Content.Client/Overlays/ShowThirstIconsSystem.cs new file mode 100644 index 0000000000..a9ece7ad7f --- /dev/null +++ b/Content.Client/Overlays/ShowThirstIconsSystem.cs @@ -0,0 +1,73 @@ +using Content.Shared.Nutrition.Components; +using Content.Shared.Overlays; +using Content.Shared.StatusIcon; +using Content.Shared.StatusIcon.Components; +using Robust.Shared.Prototypes; + +namespace Content.Client.Overlays +{ + public sealed class ShowThirstIconsSystem : ComponentAddedOverlaySystemBase + { + [Dependency] private readonly IPrototypeManager _prototypeMan = default!; + [Dependency] private readonly IEntityManager _entManager = default!; + + private StatusIconPrototype? _overhydrated; + private StatusIconPrototype? _thirsty; + private StatusIconPrototype? _parched; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetStatusIconsEvent); + } + + private void OnGetStatusIconsEvent(EntityUid uid, ThirstComponent thirstComponent, ref GetStatusIconsEvent @event) + { + if (!IsActive) + return; + + var healthIcons = DecideThirstIcon(uid, thirstComponent); + + @event.StatusIcons.AddRange(healthIcons); + } + + private IReadOnlyList DecideThirstIcon(EntityUid uid, ThirstComponent thirstComponent) + { + var result = new List(); + + if (_entManager.TryGetComponent(uid, out var metaDataComponent) && + metaDataComponent.Flags.HasFlag(MetaDataFlags.InContainer)) + { + return result; + } + + switch (thirstComponent.CurrentThirstThreshold) + { + case ThirstThreshold.OverHydrated: + if (_overhydrated != null || + _prototypeMan.TryIndex("ThirstIcon_Overhydrated", out _overhydrated)) + { + result.Add(_overhydrated); + } + break; + case ThirstThreshold.Thirsty: + if (_thirsty != null || + _prototypeMan.TryIndex("ThirstIcon_Thirsty", out _thirsty)) + { + result.Add(_thirsty); + } + break; + case ThirstThreshold.Parched: + if (_parched != null || + _prototypeMan.TryIndex("ThirstIcon_Parched", out _parched)) + { + result.Add(_parched); + } + break; + } + + return result; + } + } +} diff --git a/Content.Client/StatusIcon/StatusIconOverlay.cs b/Content.Client/StatusIcon/StatusIconOverlay.cs index c5add7f9bd..2788eeb493 100644 --- a/Content.Client/StatusIcon/StatusIconOverlay.cs +++ b/Content.Client/StatusIcon/StatusIconOverlay.cs @@ -1,8 +1,9 @@ -using System.Numerics; +using Content.Shared.StatusIcon; using Content.Shared.StatusIcon.Components; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.Enums; +using System.Numerics; namespace Content.Client.StatusIcon; @@ -57,7 +58,8 @@ public sealed class StatusIconOverlay : Overlay Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty); handle.SetTransform(matty); - var count = 0; + var countL = 0; + var countR = 0; var accOffsetL = 0; var accOffsetR = 0; icons.Sort(); @@ -71,13 +73,16 @@ public sealed class StatusIconOverlay : Overlay // the icons are ordered left to right, top to bottom. // extra icons that don't fit are just cut off. - if (count % 2 == 0) + if (proto.LocationPreference == StatusIconLocationPreference.Left || + proto.LocationPreference == StatusIconLocationPreference.None && countL <= countR) { if (accOffsetL + texture.Height > sprite.Bounds.Height * EyeManager.PixelsPerMeter) break; accOffsetL += texture.Height; yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float) accOffsetL / EyeManager.PixelsPerMeter; xOffset = -(bounds.Width + sprite.Offset.X) / 2f; + + countL++; } else { @@ -86,8 +91,9 @@ public sealed class StatusIconOverlay : Overlay accOffsetR += texture.Height; yOffset = (bounds.Height + sprite.Offset.Y) / 2f - (float) accOffsetR / EyeManager.PixelsPerMeter; xOffset = (bounds.Width + sprite.Offset.X) / 2f - (float) texture.Width / EyeManager.PixelsPerMeter; + + countR++; } - count++; var position = new Vector2(xOffset, yOffset); handle.DrawTexture(texture, position); diff --git a/Content.Server/Chemistry/ReagentEffects/SatiateThirst.cs b/Content.Server/Chemistry/ReagentEffects/SatiateThirst.cs index 79911110a9..bd8b575334 100644 --- a/Content.Server/Chemistry/ReagentEffects/SatiateThirst.cs +++ b/Content.Server/Chemistry/ReagentEffects/SatiateThirst.cs @@ -1,6 +1,6 @@ -using Content.Server.Nutrition.Components; using Content.Shared.Chemistry.Reagent; -using Content.Server.Nutrition.EntitySystems; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; using Robust.Shared.Prototypes; namespace Content.Server.Chemistry.ReagentEffects diff --git a/Content.Server/Medical/VomitSystem.cs b/Content.Server/Medical/VomitSystem.cs index ed62176973..b560c8159c 100644 --- a/Content.Server/Medical/VomitSystem.cs +++ b/Content.Server/Medical/VomitSystem.cs @@ -1,16 +1,11 @@ using Content.Server.Body.Components; using Content.Server.Body.Systems; using Content.Server.Chemistry.EntitySystems; -using Content.Server.Fluids.Components; using Content.Server.Fluids.EntitySystems; using Content.Server.Forensics; -using Content.Server.Nutrition.Components; -using Content.Server.Nutrition.EntitySystems; using Content.Server.Popups; using Content.Server.Stunnable; -using Content.Shared.Audio; using Content.Shared.Chemistry.Components; -using Content.Shared.Fluids.Components; using Content.Shared.IdentityManagement; using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.EntitySystems; diff --git a/Content.Server/Nutrition/Components/ThirstComponent.cs b/Content.Server/Nutrition/Components/ThirstComponent.cs deleted file mode 100644 index d0fbd00abe..0000000000 --- a/Content.Server/Nutrition/Components/ThirstComponent.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Content.Shared.Alert; - -namespace Content.Server.Nutrition.Components -{ - [Flags] - public enum ThirstThreshold : byte - { - // Hydrohomies - Dead = 0, - Parched = 1 << 0, - Thirsty = 1 << 1, - Okay = 1 << 2, - OverHydrated = 1 << 3, - } - - [RegisterComponent] - public sealed class ThirstComponent : Component - { - // Base stuff - [ViewVariables(VVAccess.ReadWrite)] - [DataField("baseDecayRate")] - public float BaseDecayRate = 0.1f; - - [ViewVariables(VVAccess.ReadWrite)] - public float ActualDecayRate; - - // Thirst - [ViewVariables(VVAccess.ReadOnly)] - public ThirstThreshold CurrentThirstThreshold; - - public ThirstThreshold LastThirstThreshold; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("startingThirst")] - public float CurrentThirst = -1f; - - [DataField("thresholds")] - public Dictionary ThirstThresholds { get; } = new() - { - {ThirstThreshold.OverHydrated, 600.0f}, - {ThirstThreshold.Okay, 450.0f}, - {ThirstThreshold.Thirsty, 300.0f}, - {ThirstThreshold.Parched, 150.0f}, - {ThirstThreshold.Dead, 0.0f}, - }; - - public static readonly Dictionary ThirstThresholdAlertTypes = new() - { - {ThirstThreshold.Thirsty, AlertType.Thirsty}, - {ThirstThreshold.Parched, AlertType.Parched}, - {ThirstThreshold.Dead, AlertType.Parched}, - }; - } -} diff --git a/Content.Shared/Nutrition/Components/ThirstComponent.cs b/Content.Shared/Nutrition/Components/ThirstComponent.cs new file mode 100644 index 0000000000..4264c91bfc --- /dev/null +++ b/Content.Shared/Nutrition/Components/ThirstComponent.cs @@ -0,0 +1,101 @@ +using Content.Shared.Alert; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Nutrition.Components +{ + [RegisterComponent, NetworkedComponent, Access(typeof(ThirstSystem))] + public sealed class ThirstComponent : Component + { + // Base stuff + [ViewVariables(VVAccess.ReadWrite)] + [DataField("baseDecayRate")] + public float BaseDecayRate = 0.1f; + + [ViewVariables(VVAccess.ReadWrite)] + public float ActualDecayRate; + + // Thirst + [ViewVariables(VVAccess.ReadOnly)] + public ThirstThreshold CurrentThirstThreshold; + + public ThirstThreshold LastThirstThreshold; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("startingThirst")] + public float CurrentThirst = -1f; + + /// + /// The time when the hunger will update next. + /// + [DataField("nextUpdateTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan NextUpdateTime; + + /// + /// The time between each update. + /// + [ViewVariables(VVAccess.ReadWrite)] + public TimeSpan UpdateRate = TimeSpan.FromSeconds(1); + + [DataField("thresholds")] + public Dictionary ThirstThresholds { get; } = new() + { + {ThirstThreshold.OverHydrated, 600.0f}, + {ThirstThreshold.Okay, 450.0f}, + {ThirstThreshold.Thirsty, 300.0f}, + {ThirstThreshold.Parched, 150.0f}, + {ThirstThreshold.Dead, 0.0f}, + }; + + public static readonly Dictionary ThirstThresholdAlertTypes = new() + { + {ThirstThreshold.Thirsty, AlertType.Thirsty}, + {ThirstThreshold.Parched, AlertType.Parched}, + {ThirstThreshold.Dead, AlertType.Parched}, + }; + } + + [Serializable, NetSerializable] + public sealed class ThirstComponentState : ComponentState + { + public float BaseDecayRate; + + public float ActualDecayRate; + + public ThirstThreshold CurrentThirstThreshold; + + public ThirstThreshold LastThirstThreshold; + + public float CurrentThirst; + + public TimeSpan NextUpdateTime; + + public ThirstComponentState(float baseDecayRate, + float actualDecayRate, + ThirstThreshold currentThirstThreshold, + ThirstThreshold lastThirstThreshold, + float currentThirst, + TimeSpan nextUpdateTime) + { + BaseDecayRate = baseDecayRate; + ActualDecayRate = actualDecayRate; + CurrentThirstThreshold = currentThirstThreshold; + LastThirstThreshold = lastThirstThreshold; + CurrentThirst = currentThirst; + NextUpdateTime = nextUpdateTime; + } + } + + [Flags] + public enum ThirstThreshold : byte + { + // Hydrohomies + Dead = 0, + Parched = 1 << 0, + Thirsty = 1 << 1, + Okay = 1 << 2, + OverHydrated = 1 << 3, + } +} diff --git a/Content.Server/Nutrition/EntitySystems/ThirstSystem.cs b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs similarity index 71% rename from Content.Server/Nutrition/EntitySystems/ThirstSystem.cs rename to Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs index ab966e3e90..ef0a133da5 100644 --- a/Content.Server/Nutrition/EntitySystems/ThirstSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs @@ -1,33 +1,71 @@ -using Content.Server.Nutrition.Components; -using JetBrains.Annotations; -using Robust.Shared.Random; -using Content.Shared.Movement.Components; using Content.Shared.Alert; +using Content.Shared.Movement.Components; using Content.Shared.Movement.Systems; +using Content.Shared.Nutrition.Components; using Content.Shared.Rejuvenate; +using JetBrains.Annotations; +using Robust.Shared.GameStates; +using Robust.Shared.Random; +using Robust.Shared.Timing; -namespace Content.Server.Nutrition.EntitySystems +namespace Content.Shared.Nutrition.EntitySystems { - [UsedImplicitly] public sealed class ThirstSystem : EntitySystem { + [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly AlertsSystem _alerts = default!; [Dependency] private readonly MovementSpeedModifierSystem _movement = default!; [Dependency] private readonly SharedJetpackSystem _jetpack = default!; private ISawmill _sawmill = default!; - private float _accumulatedFrameTime; public override void Initialize() { base.Initialize(); _sawmill = Logger.GetSawmill("thirst"); + SubscribeLocalEvent(OnGetState); + SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnUnpaused); SubscribeLocalEvent(OnRefreshMovespeed); + SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnComponentStartup); SubscribeLocalEvent(OnRejuvenate); } + + private void OnGetState(EntityUid uid, ThirstComponent component, ref ComponentGetState args) + { + args.State = new ThirstComponentState(component.BaseDecayRate, + component.ActualDecayRate, + component.CurrentThirstThreshold, + component.LastThirstThreshold, + component.CurrentThirst, + component.NextUpdateTime); + } + + private void OnHandleState(EntityUid uid, ThirstComponent component, ref ComponentHandleState args) + { + if (args.Current is not ThirstComponentState state) + return; + component.BaseDecayRate = state.BaseDecayRate; + component.ActualDecayRate = state.ActualDecayRate; + component.CurrentThirstThreshold = state.CurrentThirstThreshold; + component.LastThirstThreshold = state.LastThirstThreshold; + component.CurrentThirst = state.CurrentThirst; + component.NextUpdateTime = state.NextUpdateTime; + } + + private void OnUnpaused(EntityUid uid, ThirstComponent component, ref EntityUnpausedEvent args) + { + component.NextUpdateTime += args.PausedTime; + } + + private void OnShutdown(EntityUid uid, ThirstComponent component, ComponentShutdown args) + { + _alerts.ClearAlertCategory(uid, AlertCategory.Thirst); + } + private void OnComponentStartup(EntityUid uid, ThirstComponent component, ComponentStartup args) { // Do not change behavior unless starting value is explicitly defined @@ -149,23 +187,26 @@ namespace Content.Server.Nutrition.EntitySystems throw new ArgumentOutOfRangeException($"No thirst threshold found for {component.CurrentThirstThreshold}"); } } + public override void Update(float frameTime) { - _accumulatedFrameTime += frameTime; + base.Update(frameTime); - if (_accumulatedFrameTime > 1) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var thirst)) { - foreach (var component in EntityManager.EntityQuery()) + if (_timing.CurTime < thirst.NextUpdateTime) + continue; + thirst.NextUpdateTime = _timing.CurTime + thirst.UpdateRate; + + UpdateThirst(thirst, -thirst.ActualDecayRate); + var calculatedThirstThreshold = GetThirstThreshold(thirst, thirst.CurrentThirst); + if (calculatedThirstThreshold != thirst.CurrentThirstThreshold) { - UpdateThirst(component, - component.ActualDecayRate); - var calculatedThirstThreshold = GetThirstThreshold(component, component.CurrentThirst); - if (calculatedThirstThreshold != component.CurrentThirstThreshold) - { - component.CurrentThirstThreshold = calculatedThirstThreshold; - UpdateEffects(component); - } + thirst.CurrentThirstThreshold = calculatedThirstThreshold; + UpdateEffects(thirst); } - _accumulatedFrameTime -= 1; + Dirty(thirst); } } } diff --git a/Content.Shared/Overlays/ShowHungerIconsComponent.cs b/Content.Shared/Overlays/ShowHungerIconsComponent.cs new file mode 100644 index 0000000000..54e67097c6 --- /dev/null +++ b/Content.Shared/Overlays/ShowHungerIconsComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Overlays +{ + /// + /// This component allows you to see the hungriness of mobs. + /// + [RegisterComponent] + public sealed class ShowHungerIconsComponent : Component { } +} diff --git a/Content.Shared/Overlays/ShowSecurityIconsComponent.cs b/Content.Shared/Overlays/ShowSecurityIconsComponent.cs new file mode 100644 index 0000000000..034f6eacc0 --- /dev/null +++ b/Content.Shared/Overlays/ShowSecurityIconsComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Overlays +{ + /// + /// This component allows you to see job icons above mobs. + /// + [RegisterComponent] + public sealed class ShowSecurityIconsComponent : Component { } +} diff --git a/Content.Shared/Overlays/ShowThirstIconsComponent.cs b/Content.Shared/Overlays/ShowThirstIconsComponent.cs new file mode 100644 index 0000000000..94beb4b320 --- /dev/null +++ b/Content.Shared/Overlays/ShowThirstIconsComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Shared.Overlays +{ + /// + /// This component allows you to see the thirstiness of mobs. + /// + [RegisterComponent] + public sealed class ShowThirstIconsComponent : Component { } +} diff --git a/Content.Shared/StatusIcon/StatusIconPrototype.cs b/Content.Shared/StatusIcon/StatusIconPrototype.cs index 582c6cacf4..16c24fb102 100644 --- a/Content.Shared/StatusIcon/StatusIconPrototype.cs +++ b/Content.Shared/StatusIcon/StatusIconPrototype.cs @@ -1,4 +1,5 @@ -using Robust.Shared.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; using Robust.Shared.Utility; namespace Content.Shared.StatusIcon; @@ -22,6 +23,12 @@ public class StatusIconData : IComparable [DataField("priority")] public int Priority = 10; + /// + /// A preference for where the icon will be displayed. None | Left | Right + /// + [DataField("locationPreference")] + public StatusIconLocationPreference LocationPreference = StatusIconLocationPreference.None; + public int CompareTo(StatusIconData? other) { return Priority.CompareTo(other?.Priority ?? int.MaxValue); @@ -38,3 +45,11 @@ public sealed class StatusIconPrototype : StatusIconData, IPrototype [IdDataField] public string ID { get; } = default!; } + +[Serializable, NetSerializable] +public enum StatusIconLocationPreference : byte +{ + None, + Left, + Right, +} diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml index 64a17abff2..93a3ec443d 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml @@ -1,14 +1,3 @@ -- type: entity - parent: ClothingEyesBase - id: ClothingEyesGlassesBeer - name: beer goggles - description: A pair of sunglasses outfitted with apparatus to scan reagents, as well as providing an innate understanding of liquid viscosity while in motion. - components: - - type: Sprite - sprite: Clothing/Eyes/Glasses/beergoggles.rsi - - type: Clothing - sprite: Clothing/Eyes/Glasses/beergoggles.rsi - - type: entity parent: ClothingEyesBase id: ClothingEyesGlassesGar diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml b/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml index 1493c321b9..66a452942e 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml @@ -30,3 +30,40 @@ sprite: Clothing/Eyes/Hud/sec.rsi - type: Clothing sprite: Clothing/Eyes/Hud/sec.rsi + - type: ShowSecurityIcons + +- type: entity + parent: ClothingEyesBase + id: ClothingEyesHudChef + name: onion goggles + description: Two onion rings that fused together while frying. They scan the humanoids in view and provide accurate data about their peckishness. Can be eaten as a little treat. + components: + - type: Sprite + sprite: Clothing/Eyes/Hud/friedoniongoggles.rsi + - type: Clothing + sprite: Clothing/Eyes/Hud/friedoniongoggles.rsi + - type: ShowHungerIcons + - type: Food + - type: SolutionContainerManager + solutions: + food: + maxVol: 3 + reagents: + - ReagentId: Nutriment + Quantity: 3 + - type: FlavorProfile + flavors: + - onion + - greasey + +- type: entity + parent: ClothingEyesBase + id: ClothingEyesGlassesBeer + name: beer goggles + description: A pair of sunglasses outfitted with apparatus to scan reagents, as well as providing an innate understanding of liquid viscosity while in motion. + components: + - type: Sprite + sprite: Clothing/Eyes/Hud/beergoggles.rsi + - type: Clothing + sprite: Clothing/Eyes/Hud/beergoggles.rsi + - type: ShowThirstIcons \ No newline at end of file diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml index b64d2c8230..028ea3bbe1 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml @@ -22,6 +22,7 @@ id: BartenderGear equipment: head: ClothingHeadHatTophat + eyes: ClothingEyesGlassesBeer jumpsuit: ClothingUniformJumpsuitBartender outerClothing: ClothingOuterVest back: ClothingBackpackFilled diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml index fe0c7e791d..f077dfc697 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml @@ -23,6 +23,7 @@ equipment: jumpsuit: ClothingUniformJumpsuitChef head: ClothingHeadHatChef + eyes: ClothingEyesHudChef back: ClothingBackpackFilled mask: ClothingMaskItalianMoustache shoes: ClothingShoesColorBlack diff --git a/Resources/Prototypes/StatusEffects/hunger.yml b/Resources/Prototypes/StatusEffects/hunger.yml new file mode 100644 index 0000000000..73d3c342b3 --- /dev/null +++ b/Resources/Prototypes/StatusEffects/hunger.yml @@ -0,0 +1,49 @@ +#Hunger +- type: statusIcon + id: HungerIcon_Overfed + priority: 5 + icon: + sprite: Interface/Misc/food_icons.rsi + state: overfed + locationPreference: Right + +- type: statusIcon + id: HungerIcon_Peckish + priority: 5 + icon: + sprite: Interface/Misc/food_icons.rsi + state: peckish + locationPreference: Right + +- type: statusIcon + id: HungerIcon_Starving + priority: 5 + icon: + sprite: Interface/Misc/food_icons.rsi + state: starving + locationPreference: Right + +#Thirst +- type: statusIcon + id: ThirstIcon_Overhydrated + priority: 5 + icon: + sprite: Interface/Misc/food_icons.rsi + state: overhydrated + locationPreference: Left + +- type: statusIcon + id: ThirstIcon_Thirsty + priority: 5 + icon: + sprite: Interface/Misc/food_icons.rsi + state: thirsty + locationPreference: Left + +- type: statusIcon + id: ThirstIcon_Parched + priority: 5 + icon: + sprite: Interface/Misc/food_icons.rsi + state: parched + locationPreference: Left diff --git a/Resources/Prototypes/StatusEffects/job.yml b/Resources/Prototypes/StatusEffects/job.yml new file mode 100644 index 0000000000..a6f19b6cb8 --- /dev/null +++ b/Resources/Prototypes/StatusEffects/job.yml @@ -0,0 +1,375 @@ +- type: statusIcon + id: JobIcon_Detective + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Detective + locationPreference: Left + +- type: statusIcon + id: JobIcon_QuarterMaster + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: QuarterMaster + locationPreference: Left + +- type: statusIcon + id: JobIcon_Botanist + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Botanist + locationPreference: Left + +- type: statusIcon + id: JobIcon_Boxer + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Boxer + locationPreference: Left + +- type: statusIcon + id: JobIcon_AtmosphericTechnician + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: AtmosphericTechnician + locationPreference: Left + +- type: statusIcon + id: JobIcon_Nanotrasen + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Nanotrasen + locationPreference: Left + +- type: statusIcon + id: JobIcon_Prisoner + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Prisoner + locationPreference: Left + +- type: statusIcon + id: JobIcon_Janitor + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Janitor + locationPreference: Left + +- type: statusIcon + id: JobIcon_Chemist + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Chemist + locationPreference: Left + +- type: statusIcon + id: JobIcon_StationEngineer + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: StationEngineer + locationPreference: Left + +- type: statusIcon + id: JobIcon_SecurityOfficer + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: SecurityOfficer + locationPreference: Left + +- type: statusIcon + id: JobIcon_NoId + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: NoId + locationPreference: Left + +- type: statusIcon + id: JobIcon_ChiefMedicalOfficer + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: ChiefMedicalOfficer + locationPreference: Left + +- type: statusIcon + id: JobIcon_Roboticist + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Roboticist + locationPreference: Left + +- type: statusIcon + id: JobIcon_Chaplain + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Chaplain + locationPreference: Left + +- type: statusIcon + id: JobIcon_Lawyer + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Lawyer + locationPreference: Left + +- type: statusIcon + id: JobIcon_Unknown + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Unknown + locationPreference: Left + +- type: statusIcon + id: JobIcon_Librarian + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Librarian + locationPreference: Left + +- type: statusIcon + id: JobIcon_CargoTechnician + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: CargoTechnician + locationPreference: Left + +- type: statusIcon + id: JobIcon_Scientist + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Scientist + locationPreference: Left + +- type: statusIcon + id: JobIcon_ResearchAssistant + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: ResearchAssistant + locationPreference: Left + +- type: statusIcon + id: JobIcon_Geneticist + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Geneticist + locationPreference: Left + +- type: statusIcon + id: JobIcon_Clown + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Clown + locationPreference: Left + +- type: statusIcon + id: JobIcon_Captain + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Captain + locationPreference: Left + +- type: statusIcon + id: JobIcon_HeadOfPersonnel + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: HeadOfPersonnel + locationPreference: Left + +- type: statusIcon + id: JobIcon_Virologist + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Virologist + locationPreference: Left + +- type: statusIcon + id: JobIcon_ShaftMiner + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: ShaftMiner + locationPreference: Left + +- type: statusIcon + id: JobIcon_Passenger + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Passenger + locationPreference: Left + +- type: statusIcon + id: JobIcon_ChiefEngineer + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: ChiefEngineer + locationPreference: Left + +- type: statusIcon + id: JobIcon_Bartender + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Bartender + locationPreference: Left + +- type: statusIcon + id: JobIcon_HeadOfSecurity + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: HeadOfSecurity + locationPreference: Left + +- type: statusIcon + id: JobIcon_Brigmedic + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Brigmedic + locationPreference: Left + +- type: statusIcon + id: JobIcon_MedicalDoctor + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: MedicalDoctor + locationPreference: Left + +- type: statusIcon + id: JobIcon_Paramedic + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Paramedic + locationPreference: Left + +- type: statusIcon + id: JobIcon_Chef + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Chef + locationPreference: Left + +- type: statusIcon + id: JobIcon_Warden + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Warden + locationPreference: Left + +- type: statusIcon + id: JobIcon_ResearchDirector + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: ResearchDirector + locationPreference: Left + +- type: statusIcon + id: JobIcon_Mime + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Mime + locationPreference: Left + +- type: statusIcon + id: JobIcon_Musician + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Musician + locationPreference: Left + +- type: statusIcon + id: JobIcon_Reporter + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Reporter + locationPreference: Left + +- type: statusIcon + id: JobIcon_Psychologist + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Psychologist + locationPreference: Left + +- type: statusIcon + id: JobIcon_MedicalIntern + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: MedicalIntern + locationPreference: Left + +- type: statusIcon + id: JobIcon_TechnicalAssistant + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: TechnicalAssistant + locationPreference: Left + +- type: statusIcon + id: JobIcon_ServiceWorker + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: ServiceWorker + locationPreference: Left + +- type: statusIcon + id: JobIcon_SecurityCadet + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: SecurityCadet + locationPreference: Left + +- type: statusIcon + id: JobIcon_Zombie + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Zombie + locationPreference: Left + +- type: statusIcon + id: JobIcon_Zookeeper + priority: 1 + icon: + sprite: Interface/Misc/job_icons.rsi + state: Zookeeper + locationPreference: Left diff --git a/Resources/Textures/Clothing/Eyes/Glasses/beergoggles.rsi/equipped-EYES.png b/Resources/Textures/Clothing/Eyes/Hud/beergoggles.rsi/equipped-EYES.png similarity index 100% rename from Resources/Textures/Clothing/Eyes/Glasses/beergoggles.rsi/equipped-EYES.png rename to Resources/Textures/Clothing/Eyes/Hud/beergoggles.rsi/equipped-EYES.png diff --git a/Resources/Textures/Clothing/Eyes/Glasses/beergoggles.rsi/icon.png b/Resources/Textures/Clothing/Eyes/Hud/beergoggles.rsi/icon.png similarity index 100% rename from Resources/Textures/Clothing/Eyes/Glasses/beergoggles.rsi/icon.png rename to Resources/Textures/Clothing/Eyes/Hud/beergoggles.rsi/icon.png diff --git a/Resources/Textures/Clothing/Eyes/Glasses/beergoggles.rsi/meta.json b/Resources/Textures/Clothing/Eyes/Hud/beergoggles.rsi/meta.json similarity index 100% rename from Resources/Textures/Clothing/Eyes/Glasses/beergoggles.rsi/meta.json rename to Resources/Textures/Clothing/Eyes/Hud/beergoggles.rsi/meta.json diff --git a/Resources/Textures/Clothing/Eyes/Glasses/friedoniongoggles.rsi/equipped-EYES.png b/Resources/Textures/Clothing/Eyes/Hud/friedoniongoggles.rsi/equipped-EYES.png similarity index 100% rename from Resources/Textures/Clothing/Eyes/Glasses/friedoniongoggles.rsi/equipped-EYES.png rename to Resources/Textures/Clothing/Eyes/Hud/friedoniongoggles.rsi/equipped-EYES.png diff --git a/Resources/Textures/Clothing/Eyes/Glasses/friedoniongoggles.rsi/icon.png b/Resources/Textures/Clothing/Eyes/Hud/friedoniongoggles.rsi/icon.png similarity index 100% rename from Resources/Textures/Clothing/Eyes/Glasses/friedoniongoggles.rsi/icon.png rename to Resources/Textures/Clothing/Eyes/Hud/friedoniongoggles.rsi/icon.png diff --git a/Resources/Textures/Clothing/Eyes/Glasses/friedoniongoggles.rsi/meta.json b/Resources/Textures/Clothing/Eyes/Hud/friedoniongoggles.rsi/meta.json similarity index 100% rename from Resources/Textures/Clothing/Eyes/Glasses/friedoniongoggles.rsi/meta.json rename to Resources/Textures/Clothing/Eyes/Hud/friedoniongoggles.rsi/meta.json