fish-station/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs
Vigers Ray 7eee898afc Merge remote-tracking branch 'space-sunrise/master'
# Conflicts:
#	Content.Client/Medical/CrewMonitoring/CrewMonitoringWindow.xaml
#	Content.Client/_Sunrise/MentorHelp/MentorHelpControl.xaml.cs
#	Content.Packaging/ServerPackaging.cs
#	Content.Server/Content.Server.csproj
#	Content.Server/Entry/EntryPoint.cs
#	Content.Server/Explosion/EntitySystems/ExplosionSystem.Processing.cs
#	Content.Server/Fax/FaxSystem.cs
#	Content.Server/Medical/CrewMonitoring/CrewMonitoringConsoleSystem.cs
#	Content.Server/_Sunrise/Biocode/Systems/BiocodeDeactivationSystem.cs
#	Content.Shared/CCVar/CCVars.Atmos.cs
#	Content.Shared/Repairable/RepairableSystem.cs
#	Content.Shared/_Sunrise/FleshCult/CauseFleshCultInfection.cs
#	Resources/Locale/en-US/_prototypes/_sunrise/entities/clothing/belt/belts.ftl
#	Resources/Locale/en-US/_prototypes/_sunrise/entities/objects/specific/medical/hypospray.ftl
#	Resources/Locale/en-US/_strings/medical/components/crew-monitoring-component.ftl
#	Resources/Locale/en-US/_strings/reagents/meta/medicine.ftl
#	Resources/Locale/ru-RU/_prototypes/_sunrise/entities/clothing/belt/belts.ftl
#	Resources/Locale/ru-RU/_prototypes/_sunrise/entities/objects/specific/mech/mechs.ftl
#	Resources/Locale/ru-RU/_prototypes/_sunrise/entities/objects/specific/medical/hypospray.ftl
#	Resources/Locale/ru-RU/_strings/_sunrise/markings/vulpkanin.ftl
#	Resources/Locale/ru-RU/_strings/_sunrise/mentor-help/mentor-help-ui.ftl
#	Resources/Locale/ru-RU/_strings/reagents/meta/medicine.ftl
#	Resources/Maps/Nonstations/nukieplanet.yml
#	Resources/Maps/_Sunrise/Nonstations/nukieplanet.yml
#	Resources/Prototypes/Catalog/Fills/Items/belt.yml
#	Resources/Prototypes/Catalog/Fills/Items/firstaidkits.yml
#	Resources/Prototypes/Damage/containers.yml
#	Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml
#	Resources/Prototypes/Entities/Mobs/Player/humanoid.yml
#	Resources/Prototypes/Entities/Mobs/Species/base.yml
#	Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
#	Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml
#	Resources/Prototypes/Entities/Objects/Specific/Medical/hypospray.yml
#	Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
#	Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml
#	Resources/Prototypes/Entities/Objects/Tools/cable_coils.yml
#	Resources/Prototypes/Entities/Objects/Weapons/Guns/Battery/battery_guns.yml
#	Resources/Prototypes/Entities/Structures/Storage/Closets/Lockers/lockers.yml
#	Resources/Prototypes/Reagents/fun.yml
#	Resources/Prototypes/Reagents/medicine.yml
#	Resources/Prototypes/Reagents/narcotics.yml
#	Resources/Prototypes/Reagents/toxins.yml
#	Resources/Prototypes/Roles/Antags/nukeops.yml
#	Resources/Prototypes/Roles/Jobs/CentComm/cburn.yml
#	Resources/Prototypes/Roles/Jobs/CentComm/deathsquad.yml
#	Resources/Prototypes/Shuttles/shuttle_incoming_event.yml
#	Resources/Prototypes/Store/currency.yml
#	Resources/Prototypes/_Starlight/Entities/Objects/Weapons/Guns/Projectiles/hitscan.yml
#	Resources/Prototypes/_Sunrise/Catalog/ert_uplink_catalog.yml
#	Resources/Prototypes/_Sunrise/Chemistry/medicine.yml
#	Resources/Prototypes/_Sunrise/Entities/Clothing/Belt/belts.yml
#	Resources/Prototypes/_Sunrise/Entities/Mobs/Player/eviltwin.yml
#	Resources/Prototypes/_Sunrise/Entities/Mobs/Player/ghost_roles.yml
#	Resources/Prototypes/_Sunrise/Entities/Objects/Specific/Mech/mechs.yml
#	Resources/Prototypes/_Sunrise/Entities/Objects/Specific/Medical/hypospray.yml
#	Resources/Prototypes/_Sunrise/Entities/Objects/Specific/NTUplink.yml
#	Resources/Prototypes/_Sunrise/Entities/Objects/Specific/Robotics/borg_brains.yml
#	Resources/Prototypes/_Sunrise/Entities/Objects/Tools/biocode.yml
#	Resources/Prototypes/_Sunrise/GameRules/events.yml
#	Resources/Prototypes/_Sunrise/Reagents/gases.yml
#	Resources/Prototypes/_Sunrise/Reagents/narcotics.yml
#	Resources/Prototypes/_Sunrise/Roles/Jobs/Blueshield/blueshield_officer.yml
#	Resources/Prototypes/_Sunrise/Roles/Jobs/Security/senior_officer.yml
#	Resources/ServerInfo/Guidebook/Medical/MedicalDoctor.xml
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/meta.json
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonalert.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonalertdirect.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonalertnull.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonclose.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinondirect.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinondirectlarge.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinondirectsmall.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinondirectxtrlarge.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonfar.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonmedium.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinonnull.png
#	Resources/Textures/_Sunrise/BloodCult/pinpointer.rsi/pinpointer_bloodcult.png
#	Resources/manifest.yml
2025-12-29 18:37:56 +01:00

309 lines
12 KiB
C#

using System.Diagnostics.CodeAnalysis;
using Content.Shared._Sunrise.Mood;
using Content.Shared._Sunrise.SunriseCCVars;
using Content.Shared.Alert;
using Content.Shared.CCVar;
using Content.Shared.Damage.Systems;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Rejuvenate;
using Content.Shared.StatusIcon;
using Robust.Shared.Network;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared.Nutrition.EntitySystems;
public sealed class HungerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
[Dependency] private readonly SharedJetpackSystem _jetpack = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
private static readonly ProtoId<SatiationIconPrototype> HungerIconOverfedId = "HungerIconOverfed";
private static readonly ProtoId<SatiationIconPrototype> HungerIconPeckishId = "HungerIconPeckish";
private static readonly ProtoId<SatiationIconPrototype> HungerIconStarvingId = "HungerIconStarving";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HungerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<HungerComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<HungerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<HungerComponent, RejuvenateEvent>(OnRejuvenate);
}
private void OnMapInit(EntityUid uid, HungerComponent component, MapInitEvent args)
{
var amount = _random.Next(
(int) component.Thresholds[HungerThreshold.Peckish] + 10,
(int) component.Thresholds[HungerThreshold.Okay]);
SetHunger(uid, amount, component);
}
private void OnShutdown(EntityUid uid, HungerComponent component, ComponentShutdown args)
{
_alerts.ClearAlertCategory(uid, component.HungerAlertCategory);
}
private void OnRefreshMovespeed(EntityUid uid, HungerComponent component, RefreshMovementSpeedModifiersEvent args)
{
if (_config.GetCVar(SunriseCCVars.MoodEnabled) // Sunrise Edit
|| component.CurrentThreshold > HungerThreshold.Starving
|| _jetpack.IsUserFlying(uid))
return;
args.ModifySpeed(component.StarvingSlowdownModifier, component.StarvingSlowdownModifier);
}
private void OnRejuvenate(EntityUid uid, HungerComponent component, RejuvenateEvent args)
{
SetHunger(uid, component.Thresholds[HungerThreshold.Okay], component);
}
/// <summary>
/// Gets the current hunger value of the given <see cref="HungerComponent"/>.
/// </summary>
public float GetHunger(HungerComponent component)
{
var dt = _timing.CurTime - component.LastAuthoritativeHungerChangeTime;
var value = component.LastAuthoritativeHungerValue - (float)dt.TotalSeconds * component.ActualDecayRate;
return ClampHungerWithinThresholds(component, value);
}
/// <summary>
/// Adds to the current hunger of an entity by the specified value
/// </summary>
/// <param name="uid"></param>
/// <param name="amount"></param>
/// <param name="component"></param>
public void ModifyHunger(EntityUid uid, float amount, HungerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
SetHunger(uid, GetHunger(component) + amount, component);
}
/// <summary>
/// Sets the current hunger of an entity to the specified value
/// </summary>
/// <param name="uid"></param>
/// <param name="amount"></param>
/// <param name="component"></param>
public void SetHunger(EntityUid uid, float amount, HungerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
SetAuthoritativeHungerValue((uid, component), amount);
UpdateCurrentThreshold(uid, component);
}
/// <summary>
/// Sets <see cref="HungerComponent.LastAuthoritativeHungerValue"/> and
/// <see cref="HungerComponent.LastAuthoritativeHungerChangeTime"/>, and dirties this entity. This "resets" the
/// starting point for <see cref="GetHunger"/>'s calculation.
/// </summary>
/// <param name="entity">The entity whose hunger will be set.</param>
/// <param name="value">The value to set the entity's hunger to.</param>
private void SetAuthoritativeHungerValue(Entity<HungerComponent> entity, float value)
{
entity.Comp.LastAuthoritativeHungerChangeTime = _timing.CurTime;
entity.Comp.LastAuthoritativeHungerValue = ClampHungerWithinThresholds(entity.Comp, value);
DirtyField(entity.Owner, entity.Comp, nameof(HungerComponent.LastAuthoritativeHungerChangeTime));
DirtyField(entity.Owner, entity.Comp, nameof(HungerComponent.LastAuthoritativeHungerValue));
}
private void UpdateCurrentThreshold(EntityUid uid, HungerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var calculatedHungerThreshold = GetHungerThreshold(component);
if (calculatedHungerThreshold == component.CurrentThreshold)
return;
component.CurrentThreshold = calculatedHungerThreshold;
DirtyField(uid, component, nameof(HungerComponent.CurrentThreshold));
DoHungerThresholdEffects(uid, component);
}
private void DoHungerThresholdEffects(EntityUid uid, HungerComponent? component = null, bool force = false)
{
if (!Resolve(uid, ref component))
return;
if (component.CurrentThreshold == component.LastThreshold && !force)
return;
if (GetMovementThreshold(component.CurrentThreshold) != GetMovementThreshold(component.LastThreshold))
{
// Sunrise Edit
if (!_config.GetCVar(SunriseCCVars.MoodEnabled))
_movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
else if (_net.IsServer)
{
var ev = new MoodEffectEvent("Hunger" + component.CurrentThreshold);
RaiseLocalEvent(uid, ev);
}
// Sunrise Edit
}
if (component.HungerThresholdAlerts.TryGetValue(component.CurrentThreshold, out var alertId))
{
_alerts.ShowAlert(uid, alertId);
}
else
{
_alerts.ClearAlertCategory(uid, component.HungerAlertCategory);
}
if (component.HungerThresholdDecayModifiers.TryGetValue(component.CurrentThreshold, out var modifier))
{
component.ActualDecayRate = component.BaseDecayRate * modifier;
DirtyField(uid, component, nameof(HungerComponent.ActualDecayRate));
SetAuthoritativeHungerValue((uid, component), GetHunger(component));
}
component.LastThreshold = component.CurrentThreshold;
DirtyField(uid, component, nameof(HungerComponent.LastThreshold));
}
private void DoContinuousHungerEffects(EntityUid uid, HungerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.CurrentThreshold <= HungerThreshold.Starving &&
component.StarvationDamage is { } damage &&
!_mobState.IsDead(uid))
{
_damageable.TryChangeDamage(uid, damage, true, false);
}
}
/// <summary>
/// Gets the hunger threshold for an entity based on the amount of food specified.
/// If a specific amount isn't specified, just uses the current hunger of the entity
/// </summary>
/// <param name="component"></param>
/// <param name="food"></param>
/// <returns></returns>
public HungerThreshold GetHungerThreshold(HungerComponent component, float? food = null)
{
food ??= GetHunger(component);
var result = HungerThreshold.Dead;
var value = component.Thresholds[HungerThreshold.Overfed];
foreach (var threshold in component.Thresholds)
{
if (threshold.Value <= value && threshold.Value >= food)
{
result = threshold.Key;
value = threshold.Value;
}
}
return result;
}
/// <summary>
/// A check that returns if the entity is below a hunger threshold.
/// </summary>
public bool IsHungerBelowState(EntityUid uid, HungerThreshold threshold, float? food = null, HungerComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return false; // It's never going to go hungry, so it's probably fine to assume that it's not... you know, hungry.
return GetHungerThreshold(comp, food) < threshold;
}
private bool GetMovementThreshold(HungerThreshold threshold)
{
switch (threshold)
{
case HungerThreshold.Overfed:
case HungerThreshold.Okay:
return true;
case HungerThreshold.Peckish:
case HungerThreshold.Starving:
case HungerThreshold.Dead:
return false;
default:
throw new ArgumentOutOfRangeException(nameof(threshold), threshold, null);
}
}
public bool TryGetStatusIconPrototype(HungerComponent component, [NotNullWhen(true)] out SatiationIconPrototype? prototype)
{
switch (component.CurrentThreshold)
{
case HungerThreshold.Overfed:
_prototype.Resolve(HungerIconOverfedId, out prototype);
break;
case HungerThreshold.Peckish:
_prototype.Resolve(HungerIconPeckishId, out prototype);
break;
case HungerThreshold.Starving:
_prototype.Resolve(HungerIconStarvingId, out prototype);
break;
default:
prototype = null;
break;
}
return prototype != null;
}
private static float ClampHungerWithinThresholds(HungerComponent component, float hungerValue)
{
return Math.Clamp(hungerValue,
component.Thresholds[HungerThreshold.Dead],
component.Thresholds[HungerThreshold.Overfed]);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<HungerComponent>();
while (query.MoveNext(out var uid, out var hunger))
{
if (_timing.CurTime < hunger.NextThresholdUpdateTime || _mobState.IsDead(uid)) // Sunrise-Edit
continue;
hunger.NextThresholdUpdateTime = _timing.CurTime + hunger.ThresholdUpdateRate;
UpdateCurrentThreshold(uid, hunger);
DoContinuousHungerEffects(uid, hunger);
// Fish-start: Consume extra hunger to heal mangleness
if (TryComp<DamageableComponent>(uid, out var damageable))
{
if (damageable.Damage.DamageDict.TryGetValue("Mangleness", out var manglenessDamage) && manglenessDamage.Value > 0)
{
var currentHunger = GetHunger(hunger);
// Fish-Edit: Consume 0.1 extra points of hunger per tick to heal 0.01 mangleness
var normalizedConsumption = 0.1f * (float)hunger.ThresholdUpdateRate.TotalSeconds;
if (currentHunger > hunger.Thresholds[HungerThreshold.Dead] + normalizedConsumption)
{
ModifyHunger(uid, -normalizedConsumption, hunger);
var healAmount = new DamageSpecifier();
healAmount.DamageDict["Mangleness"] = -0.01f * (float)hunger.ThresholdUpdateRate.TotalSeconds;
_damageable.TryChangeDamage(uid, healAmount, true, false);
}
}
}
// Fish-End
}
}
}