--- /dev/null
+using Content.Server.Nutrition.EntitySystems;
+using Content.Shared.Construction.Prototypes;
+using Content.Shared.Nutrition.Components;
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+
+namespace Content.Server.Nutrition.Components;
+
+/// <summary>
+/// This is used for a machine that extracts hunger from entities and creates meat. Yum!
+/// </summary>
+[RegisterComponent, Access(typeof(FatExtractorSystem))]
+public sealed class FatExtractorComponent : Component
+{
+ /// <summary>
+ /// Whether or not the extractor is currently extracting fat from someone
+ /// </summary>
+ [DataField("processing")]
+ public bool Processing = true;
+
+ /// <summary>
+ /// How much nutrition is extracted per second.
+ /// </summary>
+ [DataField("nutritionPerSecond"), ViewVariables(VVAccess.ReadWrite)]
+ public int NutritionPerSecond = 10;
+
+ /// <summary>
+ /// The base rate of extraction
+ /// </summary>
+ [DataField("baseNutritionPerSecond"), ViewVariables(VVAccess.ReadWrite)]
+ public int BaseNutritionPerSecond = 10;
+
+ #region Machine Upgrade
+ /// <summary>
+ /// Which machine part affects the nutrition rate
+ /// </summary>
+ [DataField("machinePartNutritionRate", customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))]
+ public string MachinePartNutritionRate = "Laser";
+
+ /// <summary>
+ /// The increase in rate per each rating above 1.
+ /// </summary>
+ [DataField("partRatingRateMultiplier")]
+ public float PartRatingRateMultiplier = 10;
+ #endregion
+
+ /// <summary>
+ /// An accumulator which tracks extracted nutrition to determine
+ /// when to spawn a meat.
+ /// </summary>
+ [DataField("nutrientAccumulator"), ViewVariables(VVAccess.ReadWrite)]
+ public int NutrientAccumulator;
+
+ /// <summary>
+ /// How high <see cref="NutrientAccumulator"/> has to be to spawn meat
+ /// </summary>
+ [DataField("nutrientPerMeat"), ViewVariables(VVAccess.ReadWrite)]
+ public int NutrientPerMeat = 60;
+
+ /// <summary>
+ /// Meat spawned by the extractor.
+ /// </summary>
+ [DataField("meatPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
+ public string MeatPrototype = "FoodMeat";
+
+ /// <summary>
+ /// When the next update will occur
+ /// </summary>
+ [DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan NextUpdate;
+
+ /// <summary>
+ /// How long each update takes
+ /// </summary>
+ [DataField("updateTime"), ViewVariables(VVAccess.ReadWrite)]
+ public TimeSpan UpdateTime = TimeSpan.FromSeconds(1);
+
+ /// <summary>
+ /// The sound played when extracting
+ /// </summary>
+ [DataField("processSound")]
+ public SoundSpecifier? ProcessSound;
+
+ public IPlayingAudioStream? Stream;
+
+ /// <summary>
+ /// A minium hunger threshold for extracting nutrition.
+ /// Ignored when emagged.
+ /// </summary>
+ [DataField("minHungerThreshold")]
+ public HungerThreshold MinHungerThreshold = HungerThreshold.Okay;
+}
--- /dev/null
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Server.Construction;
+using Content.Server.Nutrition.Components;
+using Content.Server.Power.Components;
+using Content.Server.Power.EntitySystems;
+using Content.Server.Storage.Components;
+using Content.Shared.Emag.Components;
+using Content.Shared.Emag.Systems;
+using Content.Shared.Nutrition.Components;
+using Content.Shared.Nutrition.EntitySystems;
+using Content.Shared.Storage.Components;
+using Robust.Shared.Timing;
+
+namespace Content.Server.Nutrition.EntitySystems;
+
+/// <summary>
+/// This handles logic and interactions relating to <see cref="FatExtractorComponent"/>
+/// </summary>
+public sealed class FatExtractorSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly HungerSystem _hunger = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+
+ /// <inheritdoc/>
+ public override void Initialize()
+ {
+ SubscribeLocalEvent<FatExtractorComponent, RefreshPartsEvent>(OnRefreshParts);
+ SubscribeLocalEvent<FatExtractorComponent, UpgradeExamineEvent>(OnUpgradeExamine);
+ SubscribeLocalEvent<FatExtractorComponent, EntityUnpausedEvent>(OnUnpaused);
+ SubscribeLocalEvent<FatExtractorComponent, GotEmaggedEvent>(OnGotEmagged);
+ SubscribeLocalEvent<FatExtractorComponent, StorageAfterCloseEvent>(OnClosed);
+ SubscribeLocalEvent<FatExtractorComponent, StorageAfterOpenEvent>(OnOpen);
+ SubscribeLocalEvent<FatExtractorComponent, PowerChangedEvent>(OnPowerChanged);
+ }
+
+ private void OnRefreshParts(EntityUid uid, FatExtractorComponent component, RefreshPartsEvent args)
+ {
+ var rating = args.PartRatings[component.MachinePartNutritionRate] - 1;
+ component.NutritionPerSecond = component.BaseNutritionPerSecond + (int) (component.PartRatingRateMultiplier * rating);
+ }
+
+ private void OnUpgradeExamine(EntityUid uid, FatExtractorComponent component, UpgradeExamineEvent args)
+ {
+ args.AddPercentageUpgrade("fat-extractor-component-rate", (float) component.NutritionPerSecond / component.BaseNutritionPerSecond);
+ }
+
+ private void OnUnpaused(EntityUid uid, FatExtractorComponent component, ref EntityUnpausedEvent args)
+ {
+ component.NextUpdate += args.PausedTime;
+ }
+
+ private void OnGotEmagged(EntityUid uid, FatExtractorComponent component, ref GotEmaggedEvent args)
+ {
+ args.Handled = true;
+ args.Repeatable = false;
+ }
+
+ private void OnClosed(EntityUid uid, FatExtractorComponent component, ref StorageAfterCloseEvent args)
+ {
+ StartProcessing(uid, component);
+ }
+
+ private void OnOpen(EntityUid uid, FatExtractorComponent component, ref StorageAfterOpenEvent args)
+ {
+ StopProcessing(uid, component);
+ }
+
+ private void OnPowerChanged(EntityUid uid, FatExtractorComponent component, ref PowerChangedEvent args)
+ {
+ if (!args.Powered)
+ StopProcessing(uid, component);
+ }
+
+ public void StartProcessing(EntityUid uid, FatExtractorComponent? component = null, EntityStorageComponent? storage = null)
+ {
+ if (!Resolve(uid, ref component, ref storage))
+ return;
+
+ if (component.Processing)
+ return;
+
+ if (!this.IsPowered(uid, EntityManager))
+ return;
+
+ if (!TryGetValidOccupant(uid, out _, component, storage))
+ return;
+
+ component.Processing = true;
+ _appearance.SetData(uid, FatExtractorVisuals.Processing, true);
+ component.Stream = _audio.PlayPvs(component.ProcessSound, uid);
+ component.NextUpdate = _timing.CurTime + component.UpdateTime;
+ }
+
+ public void StopProcessing(EntityUid uid, FatExtractorComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ if (!component.Processing)
+ return;
+
+ component.Processing = false;
+ _appearance.SetData(uid, FatExtractorVisuals.Processing, false);
+ component.Stream?.Stop();
+ }
+
+ public bool TryGetValidOccupant(EntityUid uid, [NotNullWhen(true)] out EntityUid? occupant, FatExtractorComponent? component = null, EntityStorageComponent? storage = null)
+ {
+ occupant = null;
+ if (!Resolve(uid, ref component, ref storage))
+ return false;
+
+ occupant = storage.Contents.ContainedEntities.FirstOrDefault();
+
+ if (!TryComp<HungerComponent>(occupant, out var hunger))
+ return false;
+
+ if (hunger.CurrentHunger < component.NutritionPerSecond)
+ return false;
+
+ if (hunger.CurrentThreshold < component.MinHungerThreshold && !HasComp<EmaggedComponent>(uid))
+ return false;
+
+ return true;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator<FatExtractorComponent, EntityStorageComponent>();
+ while (query.MoveNext(out var uid, out var fat, out var storage))
+ {
+ if (TryGetValidOccupant(uid, out var occupant, fat, storage))
+ {
+ if (!fat.Processing)
+ StartProcessing(uid, fat, storage);
+ }
+ else
+ {
+ StopProcessing(uid, fat);
+ continue;
+ }
+
+ if (!fat.Processing)
+ continue;
+
+ if (_timing.CurTime < fat.NextUpdate)
+ continue;
+ fat.NextUpdate += fat.UpdateTime;
+
+ _hunger.ModifyHunger(occupant.Value, -fat.NutritionPerSecond);
+ fat.NutrientAccumulator += fat.NutritionPerSecond;
+ if (fat.NutrientAccumulator >= fat.NutrientPerMeat)
+ {
+ fat.NutrientAccumulator -= fat.NutrientPerMeat;
+ Spawn(fat.MeatPrototype, Transform(uid).Coordinates);
+ }
+ }
+ }
+}
/// </summary>
[DataField("lockingSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier LockSound = new SoundPathSpecifier("/Audio/Machines/door_lock_on.ogg");
+
+ /// <summary>
+ /// Whether or not an emag disables it.
+ /// </summary>
+ [DataField("breakOnEmag")]
+ public bool BreakOnEmag = true;
}
[Serializable, NetSerializable]
private bool HasUserAccess(EntityUid uid, EntityUid user, AccessReaderComponent? reader = null, bool quiet = true)
{
// Not having an AccessComponent means you get free access. woo!
- if (!Resolve(uid, ref reader))
+ if (!Resolve(uid, ref reader, false))
return true;
if (_accessReader.IsAllowed(user, reader))
private void OnEmagged(EntityUid uid, LockComponent component, ref GotEmaggedEvent args)
{
- if (!component.Locked)
+ if (!component.Locked || !component.BreakOnEmag)
return;
_audio.PlayPredicted(component.UnlockSound, uid, null, AudioParams.Default.WithVolume(-5));
_appearanceSystem.SetData(uid, StorageVisuals.Locked, false);
--- /dev/null
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Nutrition.Components;
+
+[Serializable, NetSerializable]
+public enum FatExtractorVisuals : byte
+{
+ Processing
+}
+
+public enum FatExtractorVisualLayers : byte
+{
+ Light,
+ Stack,
+ Smoke
+}
--- /dev/null
+fat-extractor-component-rate = extraction rate
+
+fat-extractor-fact-1 = Fats are triglycerides made up of a combination of different building blocks; glycerol and fatty acids.
+fat-extractor-fact-2 = Adults should get a recommended 20-35% of their energy intake from fat.
+fat-extractor-fact-3 = Being overweight or obese puts you at an increased risk of chronic diseases, such as cardiovascular diseases, metabolic syndrome, type 2 diabetes, and some types of cancers.
+fat-extractor-fact-4 = Not all fats are bad. A certain amount of fat is an essential part of a healthy balanced diet.
+fat-extractor-fact-5 = Saturated fat should form no more than 11% of your daily calories.
+fat-extractor-fact-6 = Unsaturated fat, that is monounsaturated fats, polyunsaturated fats, and omega-3 fatty acids, is found in plants and fish.
\ No newline at end of file
- MicrowaveMachineCircuitboard
- BoozeDispenserMachineCircuitboard
- SodaDispenserMachineCircuitboard
+ - FatExtractorMachineCircuitboard
# Biological Technology Tree
--- /dev/null
+- type: advertisementsPack
+ id: FatExtractorFacts
+ advertisements:
+ - fat-extractor-fact-1
+ - fat-extractor-fact-2
+ - fat-extractor-fact-3
+ - fat-extractor-fact-4
+ - fat-extractor-fact-5
+ - fat-extractor-fact-6
Glass: 2
Cable: 2
+- type: entity
+ id: FatExtractorMachineCircuitboard
+ parent: BaseMachineCircuitboard
+ name: lipid extractor machine board
+ components:
+ - type: Sprite
+ state: service
+ - type: MachineBoard
+ prototype: FatExtractor
+ requirements:
+ Laser: 1
+ componentRequirements:
+ Utensil:
+ Amount: 1
+ DefaultPrototype: ForkPlastic
+ ExamineName: Utensil
+
- type: entity
id: EmitterCircuitboard
parent: BaseMachineCircuitboard
--- /dev/null
+- type: entity
+ id: FatExtractor
+ parent: BaseMachinePowered
+ name: lipid extractor
+ description: Safely and efficiently extracts excess fat from a subject.
+ components:
+ - type: FatExtractor
+ processSound:
+ path: /Audio/Machines/microwave_loop.ogg
+ params:
+ loop: true
+ maxdistance: 5
+ - type: Sprite
+ netsync: false
+ sprite: Structures/Machines/fat_sucker.rsi
+ snapCardinals: true
+ layers:
+ - state: fat
+ - state: fat_door_off
+ map: ["enum.StorageVisualLayers.Door"]
+ - state: fat_red
+ shader: unshaded
+ map: ["enum.PowerDeviceVisualLayers.Powered"]
+ - state: fat_green
+ shader: unshaded
+ visible: false
+ map: ["enum.FatExtractorVisualLayers.Light"]
+ - state: fat_panel
+ visible: false
+ map: ["enum.WiresVisualLayers.MaintenancePanel"]
+ - state: fat_stack #cash cash cash
+ visible: false
+ map: ["enum.FatExtractorVisualLayers.Stack"]
+ - state: fat_smoke
+ visible: false
+ map: ["enum.FatExtractorVisualLayers.Smoke"]
+ - type: Lock
+ breakOnEmag: false
+ - type: GenericVisualizer
+ visuals:
+ enum.StorageVisuals.Open:
+ enum.StorageVisualLayers.Door:
+ True: { visible: false }
+ False: { visible: true }
+ enum.FatExtractorVisuals.Processing:
+ enum.StorageVisualLayers.Door:
+ True: { state: fat_door_on }
+ False: { state: fat_door_off }
+ enum.FatExtractorVisualLayers.Smoke:
+ True: { visible: true }
+ False: { visible: false }
+ enum.FatExtractorVisualLayers.Stack:
+ True: { visible: true }
+ False: { visible: false }
+ enum.FatExtractorVisualLayers.Light:
+ True: { visible: true }
+ False: { visible: false }
+ enum.PowerDeviceVisuals.Powered:
+ enum.FatExtractorVisualLayers.Light:
+ False: { visible: false }
+ enum.PowerDeviceVisualLayers.Powered:
+ True: { visible: true }
+ False: { visible: false }
+ enum.StorageVisuals.HasContents:
+ enum.PowerDeviceVisualLayers.Powered:
+ True: { state: fat_yellow }
+ False: { state: fat_red }
+ enum.WiresVisuals.MaintenancePanelState:
+ enum.WiresVisualLayers.MaintenancePanel:
+ True: { visible: true }
+ False: { visible: false }
+ - type: Construction
+ graph: Machine
+ node: machine
+ containers:
+ - machine_board
+ - machine_parts
+ - entity_storage
+ - type: EmptyOnMachineDeconstruct
+ containers:
+ - entity_storage
+ - type: Damageable
+ damageContainer: Inorganic
+ damageModifierSet: StrongMetallic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 100
+ behaviors:
+ - !type:ChangeConstructionNodeBehavior
+ node: machineFrame
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
+ - type: Machine
+ board: FatExtractorMachineCircuitboard
+ - type: Wires
+ BoardName: FatExtractor
+ LayoutId: FatExtractor
+ - type: Appearance
+ - type: Speech
+ - type: Advertise
+ pack: FatExtractorFacts
+ - type: StaticPrice
+ price: 1000
+ - type: ResistLocker
+ - type: EntityStorage
+ capacity: 1
+ - type: ContainerContainer
+ containers:
+ machine_board: !type:Container
+ machine_parts: !type:Container
+ entity_storage: !type:Container
- ReagentGrinderMachineCircuitboard
- HotplateMachineCircuitboard
- MicrowaveMachineCircuitboard
+ - FatExtractorMachineCircuitboard
- UniformPrinterMachineCircuitboard
- ShuttleConsoleCircuitboard
- RadarConsoleCircuitboard
Steel: 100
Glass: 900
+- type: latheRecipe
+ id: FatExtractorMachineCircuitboard
+ result: FatExtractorMachineCircuitboard
+ completetime: 4
+ materials:
+ Steel: 100
+ Glass: 900
+
- type: latheRecipe
id: SurveillanceCameraRouterCircuitboard
result: SurveillanceCameraRouterCircuitboard
--- /dev/null
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "taken from /tg/station at commit https://github.com/tgstation/tgstation/commit/48370e5a35a19eab427d3e403b653e65fa391ca2",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "fat"
+ },
+ {
+ "name": "fat_door_off"
+ },
+ {
+ "name": "fat_door_on"
+ },
+ {
+ "name": "fat_green"
+ },
+ {
+ "name": "fat_yellow"
+ },
+ {
+ "name": "fat_red"
+ },
+ {
+ "name": "fat_stack"
+ },
+ {
+ "name": "fat_smoke",
+ "delays": [
+ [
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.1,
+ 0.4
+ ]
+ ]
+ },
+ {
+ "name": "fat_panel"
+ }
+ ]
+}
\ No newline at end of file