Merge remote-tracking branch 'upstream/master' into upstream-sync
|
|
@ -1,72 +1,15 @@
|
|||
using Content.Client.Items.Components;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Crayon;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.Crayon
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class CrayonComponent : SharedCrayonComponent, IItemStatus
|
||||
public sealed class CrayonComponent : SharedCrayonComponent
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)] private bool _uiUpdateNeeded;
|
||||
[ViewVariables(VVAccess.ReadWrite)] private string Color => _color;
|
||||
[ViewVariables] private int Charges { get; set; }
|
||||
[ViewVariables] private int Capacity { get; set; }
|
||||
|
||||
Control IItemStatus.MakeControl()
|
||||
{
|
||||
return new StatusControl(this);
|
||||
}
|
||||
|
||||
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
|
||||
{
|
||||
if (curState is not CrayonComponentState state)
|
||||
return;
|
||||
|
||||
_color = state.Color;
|
||||
SelectedState = state.State;
|
||||
Charges = state.Charges;
|
||||
Capacity = state.Capacity;
|
||||
|
||||
_uiUpdateNeeded = true;
|
||||
}
|
||||
|
||||
private sealed class StatusControl : Control
|
||||
{
|
||||
private readonly CrayonComponent _parent;
|
||||
private readonly RichTextLabel _label;
|
||||
|
||||
public StatusControl(CrayonComponent parent)
|
||||
{
|
||||
_parent = parent;
|
||||
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
|
||||
AddChild(_label);
|
||||
|
||||
parent._uiUpdateNeeded = true;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (!_parent._uiUpdateNeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_parent._uiUpdateNeeded = false;
|
||||
_label.SetMarkup(Loc.GetString("crayon-drawing-label",
|
||||
("color",_parent.Color),
|
||||
("state",_parent.SelectedState),
|
||||
("charges", _parent.Charges),
|
||||
("capacity",_parent.Capacity)));
|
||||
}
|
||||
}
|
||||
[ViewVariables(VVAccess.ReadWrite)] public bool UIUpdateNeeded;
|
||||
[ViewVariables(VVAccess.ReadWrite)] public string Color => _color;
|
||||
[ViewVariables] public int Charges { get; set; }
|
||||
[ViewVariables] public int Capacity { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
72
Content.Client/Crayon/CrayonSystem.cs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
using Content.Client.Items;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Crayon;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Crayon;
|
||||
|
||||
public sealed class CrayonSystem : EntitySystem
|
||||
{
|
||||
// Didn't do in shared because I don't think most of the server stuff can be predicted.
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CrayonComponent, ComponentHandleState>(OnCrayonHandleState);
|
||||
SubscribeLocalEvent<CrayonComponent, ItemStatusCollectMessage>(OnCrayonItemStatus);
|
||||
}
|
||||
|
||||
private static void OnCrayonHandleState(EntityUid uid, CrayonComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not CrayonComponentState state) return;
|
||||
|
||||
component._color = state.Color;
|
||||
component.SelectedState = state.State;
|
||||
component.Charges = state.Charges;
|
||||
component.Capacity = state.Capacity;
|
||||
|
||||
component.UIUpdateNeeded = true;
|
||||
}
|
||||
|
||||
private static void OnCrayonItemStatus(EntityUid uid, CrayonComponent component, ItemStatusCollectMessage args)
|
||||
{
|
||||
args.Controls.Add(new StatusControl(component));
|
||||
}
|
||||
|
||||
private sealed class StatusControl : Control
|
||||
{
|
||||
private readonly CrayonComponent _parent;
|
||||
private readonly RichTextLabel _label;
|
||||
|
||||
public StatusControl(CrayonComponent parent)
|
||||
{
|
||||
_parent = parent;
|
||||
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
|
||||
AddChild(_label);
|
||||
|
||||
parent.UIUpdateNeeded = true;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (!_parent.UIUpdateNeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_parent.UIUpdateNeeded = false;
|
||||
_label.SetMarkup(Loc.GetString("crayon-drawing-label",
|
||||
("color",_parent.Color),
|
||||
("state",_parent.SelectedState),
|
||||
("charges", _parent.Charges),
|
||||
("capacity",_parent.Capacity)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@ namespace Content.Client.Crayon.UI
|
|||
if (disposing)
|
||||
{
|
||||
_menu?.Close();
|
||||
_menu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ namespace Content.Client.Entry
|
|||
"BodyScanner",
|
||||
"Stunbaton",
|
||||
"Tool",
|
||||
"TriggerOnProximity",
|
||||
"TilePrying",
|
||||
"RandomSpriteColor",
|
||||
"ConditionalSpawner",
|
||||
|
|
@ -301,7 +302,8 @@ namespace Content.Client.Entry
|
|||
"ArtifactInteractionTrigger",
|
||||
"Artifact",
|
||||
"RandomArtifactSprite",
|
||||
"EnergySword"
|
||||
"EnergySword",
|
||||
"DoorRemote",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
Content.Client/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Global usings for Content.Client
|
||||
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using Robust.Shared.Analyzers;
|
||||
global using Robust.Shared.Log;
|
||||
global using Robust.Shared.Localization;
|
||||
global using Robust.Shared.GameObjects;
|
||||
global using Robust.Shared.IoC;
|
||||
global using Robust.Shared.Maths;
|
||||
global using Robust.Shared.ViewVariables;
|
||||
global using Robust.Shared.Serialization.Manager.Attributes;
|
||||
96
Content.Client/Trigger/ProximityTriggerVisualizer.cs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
using System;
|
||||
using Content.Shared.Trigger;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Client.Trigger
|
||||
{
|
||||
public sealed class ProximityTriggerVisualizer : AppearanceVisualizer
|
||||
{
|
||||
[DataField("animationState")]
|
||||
private string? _animationState;
|
||||
|
||||
[DataField("duration")]
|
||||
private float _animationDuration = 0.3f;
|
||||
|
||||
private const string AnimKey = "proximity";
|
||||
|
||||
private static Animation _animation = default!;
|
||||
|
||||
public override void InitializeEntity(EntityUid entityUid)
|
||||
{
|
||||
|
||||
base.InitializeEntity(entityUid);
|
||||
|
||||
if (_animationState == null) return;
|
||||
|
||||
IoCManager.Resolve<IEntityManager>().EnsureComponent<AnimationPlayerComponent>(entityUid);
|
||||
|
||||
_animation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(_animationDuration),
|
||||
AnimationTracks = {new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = ProximityTriggerVisualLayers.Base,
|
||||
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(_animationState, 0f)}
|
||||
|
||||
},
|
||||
new AnimationTrackComponentProperty()
|
||||
{
|
||||
ComponentType = typeof(PointLightComponent),
|
||||
InterpolationMode = AnimationInterpolationMode.Nearest,
|
||||
Property = nameof(PointLightComponent.Radius),
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(0.1f, 0),
|
||||
new AnimationTrackProperty.KeyFrame(3f, 0.1f),
|
||||
new AnimationTrackProperty.KeyFrame(0.1f, 0.5f)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
base.OnChangeData(component);
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
if (!entityManager.TryGetComponent(component.Owner, out SpriteComponent spriteComponent)) return;
|
||||
|
||||
var animSystem = EntitySystem.Get<AnimationPlayerSystem>();
|
||||
entityManager.TryGetComponent(component.Owner, out AnimationPlayerComponent? player);
|
||||
component.TryGetData(ProximityTriggerVisualState.State, out ProximityTriggerVisuals state);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ProximityTriggerVisuals.Inactive:
|
||||
if (player != null)
|
||||
animSystem.Stop(player, AnimKey);
|
||||
|
||||
spriteComponent.LayerSetState(ProximityTriggerVisualLayers.Base, "on");
|
||||
break;
|
||||
case ProximityTriggerVisuals.Active:
|
||||
/* TODO: Waiting on ECS OnChangeData so we can actually subscribe to the animation finishing properly.
|
||||
if (_animationState == null || player == null ||
|
||||
animSystem.HasRunningAnimation(player, AnimKey)) return;
|
||||
|
||||
animSystem.Play(player, _animation, AnimKey);
|
||||
*/
|
||||
break;
|
||||
case ProximityTriggerVisuals.Off:
|
||||
default:
|
||||
spriteComponent.LayerSetState(ProximityTriggerVisualLayers.Base, "off");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ProximityTriggerVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
using System;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Binary.Components;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.NodeContainer;
|
||||
|
|
@ -6,12 +7,15 @@ using Content.Server.NodeContainer.Nodes;
|
|||
using Content.Shared.Atmos;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class GasPassiveGateSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
|
@ -46,7 +50,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
|||
var transferMoles = pressureDelta * outlet.Air.Volume / (inlet.Air.Temperature * Atmospherics.R);
|
||||
|
||||
// Actually transfer the gas.
|
||||
outlet.AssumeAir(inlet.Air.Remove(transferMoles));
|
||||
_atmosphereSystem.Merge(outlet.Air, inlet.Air.Remove(transferMoles));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Binary.Components;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.NodeContainer;
|
||||
|
|
@ -25,6 +26,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
|||
{
|
||||
[Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private AdminLogSystem _adminLogSystem = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -81,7 +83,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
|||
var transferMoles = pressureDelta * outlet.Air.Volume / inlet.Air.Temperature * Atmospherics.R;
|
||||
|
||||
var removed = inlet.Air.Remove(transferMoles);
|
||||
outlet.AssumeAir(removed);
|
||||
_atmosphereSystem.Merge(outlet.Air, removed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ namespace Content.Server.Atmos.Piping.Binary.EntitySystems
|
|||
}
|
||||
}
|
||||
|
||||
outlet.AssumeAir(removed);
|
||||
_atmosphereSystem.Merge(outlet.Air, removed);
|
||||
}
|
||||
|
||||
private void OnPumpInteractHand(EntityUid uid, GasVolumePumpComponent component, InteractHandEvent args)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.Atmos.Piping.Trinary.Components;
|
||||
using Content.Server.NodeContainer;
|
||||
|
|
@ -26,6 +27,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
|
|||
[Dependency] private IGameTiming _gameTiming = default!;
|
||||
[Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private AdminLogSystem _adminLogSystem = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -76,10 +78,10 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
|
|||
removed.SetMoles(filter.FilteredGas.Value, 0f);
|
||||
|
||||
var target = filterNode.Air.Pressure < Atmospherics.MaxOutputPressure ? filterNode : inletNode;
|
||||
target.AssumeAir(filteredOut);
|
||||
_atmosphereSystem.Merge(target.Air, filteredOut);
|
||||
}
|
||||
|
||||
outletNode.AssumeAir(removed);
|
||||
_atmosphereSystem.Merge(outletNode.Air, removed);
|
||||
}
|
||||
|
||||
private void OnFilterInteractHand(EntityUid uid, GasFilterComponent component, InteractHandEvent args)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.Atmos.Piping.Trinary.Components;
|
||||
using Content.Server.NodeContainer;
|
||||
|
|
@ -22,6 +23,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
|
|||
{
|
||||
[Dependency] private UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private AdminLogSystem _adminLogSystem = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -98,13 +100,13 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
|
|||
if (transferMolesOne > 0f)
|
||||
{
|
||||
var removed = inletOne.Air.Remove(transferMolesOne);
|
||||
outlet.AssumeAir(removed);
|
||||
_atmosphereSystem.Merge(outlet.Air, removed);
|
||||
}
|
||||
|
||||
if (transferMolesTwo > 0f)
|
||||
{
|
||||
var removed = inletTwo.Air.Remove(transferMolesTwo);
|
||||
outlet.AssumeAir(removed);
|
||||
_atmosphereSystem.Merge(outlet.Air, removed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||
var transferMoles = (pressureDelta * outputVolume) / (airTemperature * Atmospherics.R);
|
||||
transferMoles = MathF.Min(transferMoles, environment.TotalMoles * inlet.Air.Volume / environment.Volume);
|
||||
var removed = environment.Remove(transferMoles);
|
||||
inlet.AssumeAir(removed);
|
||||
_atmosphereSystem.Merge(inlet.Air, removed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||
if (molesDelta > 0)
|
||||
{
|
||||
var removed = environment.Remove(molesDelta);
|
||||
pipe.AssumeAir(removed);
|
||||
_atmosphereSystem.Merge(pipe.Air, removed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
|||
|
||||
var removed = tile.Remove(transferMoles);
|
||||
|
||||
outlet.AssumeAir(removed);
|
||||
_atmosphereSystem.Merge(outlet.Air, removed);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,37 +1,19 @@
|
|||
using System.Threading.Tasks;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Server.Botany.Systems;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Botany.Components
|
||||
namespace Content.Server.Botany.Components;
|
||||
// TODO: This should probably be merged with SliceableFood somehow or made into a more generic Choppable.
|
||||
|
||||
[RegisterComponent]
|
||||
[Friend(typeof(LogSystem))]
|
||||
public sealed class LogComponent : Component
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class LogComponent : Component, IInteractUsing
|
||||
{
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!EntitySystem.Get<ActionBlockerSystem>().CanInteract(eventArgs.User))
|
||||
return false;
|
||||
[DataField("spawnedPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string SpawnedPrototype = "MaterialWoodPlank1";
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
if (eventArgs.Using.HasTag("BotanySharp"))
|
||||
{
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var plank = entMan.SpawnEntity("MaterialWoodPlank1", entMan.GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
plank.RandomOffset(0.25f);
|
||||
}
|
||||
|
||||
entMan.QueueDeleteEntity(Owner);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
[DataField("spawnCount")] public int SpawnCount = 2;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Threading.Tasks;
|
||||
using Content.Server.Atmos;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Botany.Systems;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Server.Fluids.Components;
|
||||
|
|
@ -107,7 +108,7 @@ namespace Content.Server.Botany.Components
|
|||
public float WeedCoefficient { get; set; } = 1f;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Seed? Seed { get; set; }
|
||||
public SeedPrototype? Seed { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool ImproperHeat { get; set; }
|
||||
|
|
@ -146,6 +147,8 @@ namespace Content.Server.Botany.Components
|
|||
|
||||
_lastCycle = curTime;
|
||||
|
||||
// todo ecs.
|
||||
var botanySystem = EntitySystem.Get<BotanySystem>();
|
||||
|
||||
// Weeds like water and nutrients! They may appear even if there's not a seed planted.
|
||||
if (WaterLevel > 10 && NutritionLevel > 2 && _random.Prob(Seed == null ? 0.05f : 0.01f))
|
||||
|
|
@ -272,7 +275,7 @@ namespace Content.Server.Botany.Components
|
|||
}
|
||||
}
|
||||
|
||||
// Seed pressure resistance.
|
||||
// SeedPrototype pressure resistance.
|
||||
var pressure = environment.Pressure;
|
||||
if (pressure < Seed.LowPressureTolerance || pressure > Seed.HighPressureTolerance)
|
||||
{
|
||||
|
|
@ -286,7 +289,7 @@ namespace Content.Server.Botany.Components
|
|||
ImproperPressure = false;
|
||||
}
|
||||
|
||||
// Seed ideal temperature.
|
||||
// SeedPrototype ideal temperature.
|
||||
if (MathF.Abs(environment.Temperature - Seed.IdealHeat) > Seed.HeatTolerance)
|
||||
{
|
||||
Health -= healthMod;
|
||||
|
|
@ -359,7 +362,7 @@ namespace Content.Server.Botany.Components
|
|||
}
|
||||
else if (Age < 0) // Revert back to seed packet!
|
||||
{
|
||||
Seed.SpawnSeedPacket(_entMan.GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
botanySystem.SpawnSeedPacket(Seed, _entMan.GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
RemovePlant();
|
||||
ForceUpdate = true;
|
||||
Update();
|
||||
|
|
@ -422,19 +425,21 @@ namespace Content.Server.Botany.Components
|
|||
if (Seed == null || _entMan.Deleted(user) || !EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
|
||||
return false;
|
||||
|
||||
var botanySystem = EntitySystem.Get<BotanySystem>();
|
||||
|
||||
if (Harvest && !Dead)
|
||||
{
|
||||
if (_entMan.TryGetComponent(user, out HandsComponent? hands))
|
||||
{
|
||||
if (!Seed.CheckHarvest(user, hands.GetActiveHandItem?.Owner))
|
||||
if (!botanySystem.CanHarvest(Seed, hands.GetActiveHandItem?.Owner))
|
||||
return false;
|
||||
}
|
||||
else if (!Seed.CheckHarvest(user))
|
||||
else if (!botanySystem.CanHarvest(Seed))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Seed.Harvest(user, YieldMod);
|
||||
botanySystem.Harvest(Seed, user, YieldMod);
|
||||
AfterHarvest();
|
||||
return true;
|
||||
}
|
||||
|
|
@ -451,7 +456,9 @@ namespace Content.Server.Botany.Components
|
|||
if (Seed == null || !Harvest)
|
||||
return;
|
||||
|
||||
Seed.AutoHarvest(_entMan.GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
var botanySystem = EntitySystem.Get<BotanySystem>();
|
||||
|
||||
botanySystem.AutoHarvest(Seed, _entMan.GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
AfterHarvest();
|
||||
}
|
||||
|
||||
|
|
@ -633,7 +640,7 @@ namespace Content.Server.Botany.Components
|
|||
// If this seed is not in the global seed list, then no products of this line have been harvested yet.
|
||||
// It is then safe to assume it's restricted to this tray.
|
||||
if (Seed == null) return;
|
||||
var plantSystem = EntitySystem.Get<PlantSystem>();
|
||||
var plantSystem = EntitySystem.Get<BotanySystem>();
|
||||
if (plantSystem.Seeds.ContainsKey(Seed.Uid))
|
||||
Seed = Seed.Diverge(modified);
|
||||
}
|
||||
|
|
@ -653,22 +660,21 @@ namespace Content.Server.Botany.Components
|
|||
if ((!_entMan.EntityExists(usingItem) ? EntityLifeStage.Deleted : _entMan.GetComponent<MetaDataComponent>(usingItem).EntityLifeStage) >= EntityLifeStage.Deleted || !EntitySystem.Get<ActionBlockerSystem>().CanInteract(user))
|
||||
return false;
|
||||
|
||||
var botanySystem = EntitySystem.Get<BotanySystem>();
|
||||
|
||||
if (_entMan.TryGetComponent(usingItem, out SeedComponent? seeds))
|
||||
{
|
||||
if (Seed == null)
|
||||
{
|
||||
if (seeds.Seed == null)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("plant-holder-component-empty-seed-packet-message"));
|
||||
_entMan.QueueDeleteEntity(usingItem);
|
||||
var protoMan = IoCManager.Resolve<IPrototypeManager>();
|
||||
if (!protoMan.TryIndex<SeedPrototype>(seeds.SeedName, out var seed))
|
||||
return false;
|
||||
}
|
||||
|
||||
user.PopupMessageCursor(Loc.GetString("plant-holder-component-plant-success-message",
|
||||
("seedName", seeds.Seed.SeedName),
|
||||
("seedNoun", seeds.Seed.SeedNoun)));
|
||||
("seedName", seed.SeedName),
|
||||
("seedNoun", seed.SeedNoun)));
|
||||
|
||||
Seed = seeds.Seed;
|
||||
Seed = seed;
|
||||
Dead = false;
|
||||
Age = 1;
|
||||
Health = Seed.Endurance;
|
||||
|
|
@ -778,7 +784,7 @@ namespace Content.Server.Botany.Components
|
|||
return false;
|
||||
}
|
||||
|
||||
var seed = Seed.SpawnSeedPacket(_entMan.GetComponent<TransformComponent>(user).Coordinates);
|
||||
var seed = botanySystem.SpawnSeedPacket(Seed, _entMan.GetComponent<TransformComponent>(user).Coordinates);
|
||||
seed.RandomOffset(0.25f);
|
||||
user.PopupMessageCursor(Loc.GetString("plant-holder-component-take-sample-message",
|
||||
("seedName", Seed.DisplayName)));
|
||||
|
|
|
|||
|
|
@ -1,62 +1,15 @@
|
|||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Server.GameObjects;
|
||||
using Content.Server.Botany.Systems;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Botany.Components
|
||||
namespace Content.Server.Botany.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
[Friend(typeof(BotanySystem))]
|
||||
public sealed class ProduceComponent : Component
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class ProduceComponent : Component, ISerializationHooks
|
||||
{
|
||||
[DataField("targetSolution")] public string SolutionName { get; set; } = "food";
|
||||
[DataField("targetSolution")] public string SolutionName { get; set; } = "food";
|
||||
|
||||
[DataField("seed")] private string? _seedName;
|
||||
|
||||
[ViewVariables]
|
||||
public Seed? Seed
|
||||
{
|
||||
get => _seedName != null ? IoCManager.Resolve<IPrototypeManager>().Index<Seed>(_seedName) : null;
|
||||
set => _seedName = value?.ID;
|
||||
}
|
||||
|
||||
public float Potency => Seed?.Potency ?? 0;
|
||||
|
||||
public void Grown()
|
||||
{
|
||||
if (Seed == null)
|
||||
return;
|
||||
|
||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent(Owner, out SpriteComponent? sprite))
|
||||
{
|
||||
sprite.LayerSetRSI(0, Seed.PlantRsi);
|
||||
sprite.LayerSetState(0, Seed.PlantIconState);
|
||||
}
|
||||
|
||||
var solutionContainer = EntitySystem.Get<SolutionContainerSystem>().EnsureSolution(Owner, SolutionName);
|
||||
if (solutionContainer == null)
|
||||
{
|
||||
Logger.Warning($"No solution container found in {nameof(ProduceComponent)}.");
|
||||
return;
|
||||
}
|
||||
|
||||
solutionContainer.RemoveAllSolution();
|
||||
foreach (var (chem, quantity) in Seed.Chemicals)
|
||||
{
|
||||
var amount = FixedPoint2.New(quantity.Min);
|
||||
if (quantity.PotencyDivisor > 0 && Potency > 0)
|
||||
amount += FixedPoint2.New(Potency / quantity.PotencyDivisor);
|
||||
amount = FixedPoint2.New((int) MathHelper.Clamp(amount.Float(), quantity.Min, quantity.Max));
|
||||
solutionContainer.MaxVolume += amount;
|
||||
solutionContainer.AddReagent(chem, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
[DataField("seed", required: true)] public string SeedName = default!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,15 @@
|
|||
using Content.Shared.Examine;
|
||||
using Content.Server.Botany.Systems;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Botany.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
#pragma warning disable 618
|
||||
public class SeedComponent : Component, IExamine
|
||||
#pragma warning restore 618
|
||||
[RegisterComponent, Friend(typeof(BotanySystem))]
|
||||
public sealed class SeedComponent : Component
|
||||
{
|
||||
[DataField("seed")]
|
||||
private string? _seedName;
|
||||
|
||||
[ViewVariables]
|
||||
public Seed? Seed
|
||||
{
|
||||
get => _seedName != null ? IoCManager.Resolve<IPrototypeManager>().Index<Seed>(_seedName) : null;
|
||||
set => _seedName = value?.ID;
|
||||
}
|
||||
|
||||
public void Examine(FormattedMessage message, bool inDetailsRange)
|
||||
{
|
||||
if (!inDetailsRange)
|
||||
return;
|
||||
|
||||
if (Seed == null)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString("seed-component-no-seeds-message") + "\n");
|
||||
return;
|
||||
}
|
||||
|
||||
message.AddMarkup(Loc.GetString($"seed-component-description", ("seedName", Seed.DisplayName)) + "\n");
|
||||
|
||||
if (!Seed.RoundStart)
|
||||
{
|
||||
message.AddMarkup(Loc.GetString($"seed-component-has-variety-tag", ("seedUid", Seed.Uid)) + "\n");
|
||||
}
|
||||
else
|
||||
{
|
||||
message.AddMarkup(Loc.GetString($"seed-component-plant-yield-text", ("seedYield", Seed.Yield)) + "\n");
|
||||
message.AddMarkup(Loc.GetString($"seed-component-plant-potency-text", ("seedPotency", Seed.Potency)) + "\n");
|
||||
}
|
||||
}
|
||||
[DataField("seed", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<SeedPrototype>))]
|
||||
public string SeedName = default!;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,16 @@
|
|||
using System.Threading.Tasks;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Server.Botany.Systems;
|
||||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Botany.Components
|
||||
namespace Content.Server.Botany.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
[Friend(typeof(SeedExtractorSystem))]
|
||||
public sealed class SeedExtractorComponent : Component
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class SeedExtractorComponent : Component, IInteractUsing
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
// TODO: Upgradeable machines.
|
||||
[DataField("minSeeds")] public int MinSeeds = 1;
|
||||
|
||||
// TODO: Upgradeable machines.
|
||||
private int _minSeeds = 1;
|
||||
private int _maxSeeds = 4;
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
if (!_entMan.TryGetComponent<ApcPowerReceiverComponent>(Owner, out var powerReceiverComponent) || !powerReceiverComponent.Powered)
|
||||
return false;
|
||||
|
||||
if (_entMan.TryGetComponent(eventArgs.Using, out ProduceComponent? produce) && produce.Seed != null)
|
||||
{
|
||||
eventArgs.User.PopupMessageCursor(Loc.GetString("seed-extractor-component-interact-message",("name", _entMan.GetComponent<MetaDataComponent>(eventArgs.Using).EntityName)));
|
||||
|
||||
_entMan.QueueDeleteEntity(eventArgs.Using);
|
||||
|
||||
var random = _random.Next(_minSeeds, _maxSeeds);
|
||||
|
||||
for (var i = 0; i < random; i++)
|
||||
{
|
||||
produce.Seed.SpawnSeedPacket(_entMan.GetComponent<TransformComponent>(Owner).Coordinates, _entMan);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
[DataField("maxSeeds")] public int MaxSeeds = 4;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,362 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Botany.Components;
|
||||
using Content.Server.Plants;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Botany
|
||||
{
|
||||
public enum HarvestType : byte
|
||||
{
|
||||
NoRepeat,
|
||||
Repeat,
|
||||
SelfHarvest,
|
||||
}
|
||||
|
||||
/*
|
||||
public enum PlantSpread : byte
|
||||
{
|
||||
NoSpread,
|
||||
Creepers,
|
||||
Vines,
|
||||
}
|
||||
|
||||
public enum PlantMutation : byte
|
||||
{
|
||||
NoMutation,
|
||||
Mutable,
|
||||
HighlyMutable,
|
||||
}
|
||||
|
||||
public enum PlantCarnivorous : byte
|
||||
{
|
||||
NotCarnivorous,
|
||||
EatPests,
|
||||
EatLivingBeings,
|
||||
}
|
||||
|
||||
public enum PlantJuicy : byte
|
||||
{
|
||||
NotJuicy,
|
||||
Juicy,
|
||||
Slippery,
|
||||
}
|
||||
*/
|
||||
|
||||
[DataDefinition]
|
||||
public struct SeedChemQuantity
|
||||
{
|
||||
[DataField("Min")]
|
||||
public int Min;
|
||||
[DataField("Max")]
|
||||
public int Max;
|
||||
[DataField("PotencyDivisor")]
|
||||
public int PotencyDivisor;
|
||||
}
|
||||
|
||||
[Prototype("seed")]
|
||||
public class Seed : IPrototype
|
||||
{
|
||||
private const string SeedPrototype = "SeedBase";
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("id", required: true)]
|
||||
public string ID { get; private init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier of this seed. Do NOT set this.
|
||||
/// </summary>
|
||||
public int Uid { get; internal set; } = -1;
|
||||
|
||||
#region Tracking
|
||||
|
||||
[ViewVariables] [DataField("name")] public string Name { get; set; } = string.Empty;
|
||||
[ViewVariables] [DataField("seedName")] public string SeedName { get; set; } = string.Empty;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("seedNoun")]
|
||||
public string SeedNoun { get; set; } = "seeds";
|
||||
[ViewVariables] [DataField("displayName")] public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("roundStart")]
|
||||
public bool RoundStart { get; private set; } = true;
|
||||
[ViewVariables] [DataField("mysterious")] public bool Mysterious { get; set; }
|
||||
[ViewVariables] [DataField("immutable")] public bool Immutable { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Output
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("productPrototypes", customTypeSerializer:typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
||||
public List<string> ProductPrototypes { get; set; } = new();
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("chemicals")]
|
||||
public Dictionary<string, SeedChemQuantity> Chemicals { get; set; } = new();
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("consumeGasses")]
|
||||
public Dictionary<Gas, float> ConsumeGasses { get; set; } = new();
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("exudeGasses")]
|
||||
public Dictionary<Gas, float> ExudeGasses { get; set; } = new();
|
||||
#endregion
|
||||
|
||||
#region Tolerances
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("nutrientConsumption")]
|
||||
public float NutrientConsumption { get; set; } = 0.25f;
|
||||
|
||||
[ViewVariables] [DataField("waterConsumption")] public float WaterConsumption { get; set; } = 3f;
|
||||
[ViewVariables] [DataField("idealHeat")] public float IdealHeat { get; set; } = 293f;
|
||||
[ViewVariables] [DataField("heatTolerance")] public float HeatTolerance { get; set; } = 20f;
|
||||
[ViewVariables] [DataField("idealLight")] public float IdealLight { get; set; } = 7f;
|
||||
[ViewVariables] [DataField("lightTolerance")] public float LightTolerance { get; set; } = 5f;
|
||||
[ViewVariables] [DataField("toxinsTolerance")] public float ToxinsTolerance { get; set; } = 4f;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("lowPressureTolerance")]
|
||||
public float LowPressureTolerance { get; set; } = 25f;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("highPressureTolerance")]
|
||||
public float HighPressureTolerance { get; set; } = 200f;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("pestTolerance")]
|
||||
public float PestTolerance { get; set; } = 5f;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("weedTolerance")]
|
||||
public float WeedTolerance { get; set; } = 5f;
|
||||
#endregion
|
||||
|
||||
#region General traits
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("endurance")]
|
||||
public float Endurance { get; set; } = 100f;
|
||||
[ViewVariables] [DataField("yield")] public int Yield { get; set; }
|
||||
[ViewVariables] [DataField("lifespan")] public float Lifespan { get; set; }
|
||||
[ViewVariables] [DataField("maturation")] public float Maturation { get; set; }
|
||||
[ViewVariables] [DataField("production")] public float Production { get; set; }
|
||||
[ViewVariables] [DataField("growthStages")] public int GrowthStages { get; set; } = 6;
|
||||
[ViewVariables] [DataField("harvestRepeat")] public HarvestType HarvestRepeat { get; set; } = HarvestType.NoRepeat;
|
||||
|
||||
[ViewVariables] [DataField("potency")] public float Potency { get; set; } = 1f;
|
||||
// No, I'm not removing these.
|
||||
//public PlantSpread Spread { get; set; }
|
||||
//public PlantMutation Mutation { get; set; }
|
||||
//public float AlterTemperature { get; set; }
|
||||
//public PlantCarnivorous Carnivorous { get; set; }
|
||||
//public bool Parasite { get; set; }
|
||||
//public bool Hematophage { get; set; }
|
||||
//public bool Thorny { get; set; }
|
||||
//public bool Stinging { get; set; }
|
||||
[DataField("ligneous")]
|
||||
public bool Ligneous { get; set; }
|
||||
// public bool Teleporting { get; set; }
|
||||
// public PlantJuicy Juicy { get; set; }
|
||||
#endregion
|
||||
|
||||
#region Cosmetics
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("plantRsi", required: true)]
|
||||
public ResourcePath PlantRsi { get; set; } = default!;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("plantIconState")]
|
||||
public string PlantIconState { get; set; } = "produce";
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("bioluminescent")]
|
||||
public bool Bioluminescent { get; set; }
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("bioluminescentColor")]
|
||||
public Color BioluminescentColor { get; set; } = Color.White;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("splatPrototype")]
|
||||
public string? SplatPrototype { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
public Seed Clone()
|
||||
{
|
||||
var newSeed = new Seed()
|
||||
{
|
||||
ID = ID,
|
||||
Name = Name,
|
||||
SeedName = SeedName,
|
||||
SeedNoun = SeedNoun,
|
||||
RoundStart = RoundStart,
|
||||
Mysterious = Mysterious,
|
||||
|
||||
ProductPrototypes = new List<string>(ProductPrototypes),
|
||||
Chemicals = new Dictionary<string, SeedChemQuantity>(Chemicals),
|
||||
ConsumeGasses = new Dictionary<Gas, float>(ConsumeGasses),
|
||||
ExudeGasses = new Dictionary<Gas, float>(ExudeGasses),
|
||||
|
||||
NutrientConsumption = NutrientConsumption,
|
||||
WaterConsumption = WaterConsumption,
|
||||
IdealHeat = IdealHeat,
|
||||
HeatTolerance = HeatTolerance,
|
||||
IdealLight = IdealLight,
|
||||
LightTolerance = LightTolerance,
|
||||
ToxinsTolerance = ToxinsTolerance,
|
||||
LowPressureTolerance = LowPressureTolerance,
|
||||
HighPressureTolerance = HighPressureTolerance,
|
||||
PestTolerance = PestTolerance,
|
||||
WeedTolerance = WeedTolerance,
|
||||
|
||||
Endurance = Endurance,
|
||||
Yield = Yield,
|
||||
Lifespan = Lifespan,
|
||||
Maturation = Maturation,
|
||||
Production = Production,
|
||||
GrowthStages = GrowthStages,
|
||||
HarvestRepeat = HarvestRepeat,
|
||||
Potency = Potency,
|
||||
|
||||
PlantRsi = PlantRsi,
|
||||
PlantIconState = PlantIconState,
|
||||
Bioluminescent = Bioluminescent,
|
||||
BioluminescentColor = BioluminescentColor,
|
||||
SplatPrototype = SplatPrototype,
|
||||
};
|
||||
|
||||
return newSeed;
|
||||
}
|
||||
|
||||
public EntityUid SpawnSeedPacket(EntityCoordinates transformCoordinates, IEntityManager? entityManager = null)
|
||||
{
|
||||
entityManager ??= IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
var seed = entityManager.SpawnEntity(SeedPrototype, transformCoordinates);
|
||||
|
||||
var seedComp = seed.EnsureComponent<SeedComponent>();
|
||||
seedComp.Seed = this;
|
||||
|
||||
if (entityManager.TryGetComponent(seed, out SpriteComponent? sprite))
|
||||
{
|
||||
// Seed state will always be seed. Blame the spriter if that's not the case!
|
||||
sprite.LayerSetSprite(0, new SpriteSpecifier.Rsi(PlantRsi, "seed"));
|
||||
}
|
||||
|
||||
string val = Loc.GetString("botany-seed-packet-name", ("seedName", SeedName), ("seedNoun", SeedNoun));
|
||||
entityManager.GetComponent<MetaDataComponent>(seed).EntityName = val;
|
||||
|
||||
return seed;
|
||||
}
|
||||
|
||||
private void AddToDatabase()
|
||||
{
|
||||
var plantSystem = EntitySystem.Get<PlantSystem>();
|
||||
if (plantSystem.AddSeedToDatabase(this))
|
||||
{
|
||||
Name = Uid.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> AutoHarvest(EntityCoordinates position, int yieldMod = 1)
|
||||
{
|
||||
if (position.IsValid(IoCManager.Resolve<IEntityManager>()) && ProductPrototypes != null &&
|
||||
ProductPrototypes.Count > 0)
|
||||
return GenerateProduct(position, yieldMod);
|
||||
|
||||
return Enumerable.Empty<EntityUid>();
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> Harvest(EntityUid user, int yieldMod = 1)
|
||||
{
|
||||
AddToDatabase();
|
||||
|
||||
if (ProductPrototypes == null || ProductPrototypes.Count == 0 || Yield <= 0)
|
||||
{
|
||||
user.PopupMessageCursor(Loc.GetString("botany-harvest-fail-message"));
|
||||
return Enumerable.Empty<EntityUid>();
|
||||
}
|
||||
|
||||
user.PopupMessageCursor(Loc.GetString("botany-harvest-success-message", ("name", DisplayName)));
|
||||
return GenerateProduct(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(user).Coordinates, yieldMod);
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> GenerateProduct(EntityCoordinates position, int yieldMod = 1)
|
||||
{
|
||||
var totalYield = 0;
|
||||
if (Yield > -1)
|
||||
{
|
||||
if (yieldMod < 0)
|
||||
{
|
||||
yieldMod = 1;
|
||||
totalYield = Yield;
|
||||
}
|
||||
else
|
||||
{
|
||||
totalYield = Yield * yieldMod;
|
||||
}
|
||||
|
||||
totalYield = Math.Max(1, totalYield);
|
||||
}
|
||||
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
|
||||
var products = new List<EntityUid>();
|
||||
|
||||
for (var i = 0; i < totalYield; i++)
|
||||
{
|
||||
var product = random.Pick(ProductPrototypes);
|
||||
|
||||
var entity = entityManager.SpawnEntity(product, position);
|
||||
entity.RandomOffset(0.25f);
|
||||
products.Add(entity);
|
||||
|
||||
var produce = entity.EnsureComponent<ProduceComponent>();
|
||||
|
||||
produce.Seed = this;
|
||||
produce.Grown();
|
||||
|
||||
if (Mysterious)
|
||||
{
|
||||
var metaData = entityManager.GetComponent<MetaDataComponent>(entity);
|
||||
metaData.EntityName += "?";
|
||||
metaData.EntityDescription += (" " + Loc.GetString("botany-mysterious-description-addon"));
|
||||
}
|
||||
}
|
||||
|
||||
return products;
|
||||
}
|
||||
|
||||
public Seed Diverge(bool modified)
|
||||
{
|
||||
return Clone();
|
||||
}
|
||||
|
||||
public bool CheckHarvest(EntityUid user, EntityUid? held = null)
|
||||
{
|
||||
return !Ligneous || (Ligneous && held != null && held.Value.HasTag("BotanySharp"));
|
||||
}
|
||||
}
|
||||
}
|
||||
222
Content.Server/Botany/SeedPrototype.cs
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Botany.Components;
|
||||
using Content.Server.Botany.Systems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Botany;
|
||||
|
||||
public enum HarvestType : byte
|
||||
{
|
||||
NoRepeat,
|
||||
Repeat,
|
||||
SelfHarvest
|
||||
}
|
||||
|
||||
/*
|
||||
public enum PlantSpread : byte
|
||||
{
|
||||
NoSpread,
|
||||
Creepers,
|
||||
Vines,
|
||||
}
|
||||
|
||||
public enum PlantMutation : byte
|
||||
{
|
||||
NoMutation,
|
||||
Mutable,
|
||||
HighlyMutable,
|
||||
}
|
||||
|
||||
public enum PlantCarnivorous : byte
|
||||
{
|
||||
NotCarnivorous,
|
||||
EatPests,
|
||||
EatLivingBeings,
|
||||
}
|
||||
|
||||
public enum PlantJuicy : byte
|
||||
{
|
||||
NotJuicy,
|
||||
Juicy,
|
||||
Slippery,
|
||||
}
|
||||
*/
|
||||
|
||||
[DataDefinition]
|
||||
public struct SeedChemQuantity
|
||||
{
|
||||
[DataField("Min")] public int Min;
|
||||
[DataField("Max")] public int Max;
|
||||
[DataField("PotencyDivisor")] public int PotencyDivisor;
|
||||
}
|
||||
|
||||
[Prototype("seed")]
|
||||
public sealed class SeedPrototype : IPrototype
|
||||
{
|
||||
public const string Prototype = "SeedBase";
|
||||
|
||||
[DataField("id", required: true)] public string ID { get; private init; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Unique identifier of this seed. Do NOT set this.
|
||||
/// </summary>
|
||||
public int Uid { get; internal set; } = -1;
|
||||
|
||||
#region Tracking
|
||||
|
||||
[DataField("name")] public string Name = string.Empty;
|
||||
[DataField("seedName")] public string SeedName = string.Empty;
|
||||
[DataField("seedNoun")] public string SeedNoun = "seeds";
|
||||
[DataField("displayName")] public string DisplayName = string.Empty;
|
||||
|
||||
[DataField("roundStart")] public bool RoundStart = true;
|
||||
[DataField("mysterious")] public bool Mysterious;
|
||||
[DataField("immutable")] public bool Immutable;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Output
|
||||
|
||||
[DataField("productPrototypes", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
||||
public List<string> ProductPrototypes = new();
|
||||
|
||||
[DataField("chemicals")] public Dictionary<string, SeedChemQuantity> Chemicals = new();
|
||||
|
||||
[DataField("consumeGasses")] public Dictionary<Gas, float> ConsumeGasses = new();
|
||||
|
||||
[DataField("exudeGasses")] public Dictionary<Gas, float> ExudeGasses = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Tolerances
|
||||
|
||||
[DataField("nutrientConsumption")] public float NutrientConsumption = 0.25f;
|
||||
|
||||
[DataField("waterConsumption")] public float WaterConsumption = 3f;
|
||||
[DataField("idealHeat")] public float IdealHeat = 293f;
|
||||
[DataField("heatTolerance")] public float HeatTolerance = 20f;
|
||||
[DataField("idealLight")] public float IdealLight = 7f;
|
||||
[DataField("lightTolerance")] public float LightTolerance = 5f;
|
||||
[DataField("toxinsTolerance")] public float ToxinsTolerance = 4f;
|
||||
|
||||
[DataField("lowPressureTolerance")] public float LowPressureTolerance = 25f;
|
||||
|
||||
[DataField("highPressureTolerance")] public float HighPressureTolerance = 200f;
|
||||
|
||||
[DataField("pestTolerance")] public float PestTolerance = 5f;
|
||||
|
||||
[DataField("weedTolerance")] public float WeedTolerance = 5f;
|
||||
|
||||
#endregion
|
||||
|
||||
#region General traits
|
||||
|
||||
[DataField("endurance")] public float Endurance = 100f;
|
||||
|
||||
[DataField("yield")] public int Yield;
|
||||
[DataField("lifespan")] public float Lifespan;
|
||||
[DataField("maturation")] public float Maturation;
|
||||
[DataField("production")] public float Production;
|
||||
[DataField("growthStages")] public int GrowthStages = 6;
|
||||
[DataField("harvestRepeat")] public HarvestType HarvestRepeat = HarvestType.NoRepeat;
|
||||
|
||||
[DataField("potency")] public float Potency = 1f;
|
||||
|
||||
// No, I'm not removing these.
|
||||
//public PlantSpread Spread { get; set; }
|
||||
//public PlantMutation Mutation { get; set; }
|
||||
//public float AlterTemperature { get; set; }
|
||||
//public PlantCarnivorous Carnivorous { get; set; }
|
||||
//public bool Parasite { get; set; }
|
||||
//public bool Hematophage { get; set; }
|
||||
//public bool Thorny { get; set; }
|
||||
//public bool Stinging { get; set; }
|
||||
|
||||
[DataField("ligneous")] public bool Ligneous;
|
||||
// public bool Teleporting { get; set; }
|
||||
// public PlantJuicy Juicy { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Cosmetics
|
||||
|
||||
[DataField("plantRsi", required: true)]
|
||||
public ResourcePath PlantRsi { get; set; } = default!;
|
||||
|
||||
[DataField("plantIconState")] public string PlantIconState { get; set; } = "produce";
|
||||
|
||||
[DataField("bioluminescent")] public bool Bioluminescent { get; set; }
|
||||
|
||||
[DataField("bioluminescentColor")] public Color BioluminescentColor { get; set; } = Color.White;
|
||||
|
||||
[DataField("splatPrototype")] public string? SplatPrototype { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
public SeedPrototype Clone()
|
||||
{
|
||||
var newSeed = new SeedPrototype
|
||||
{
|
||||
ID = ID,
|
||||
Name = Name,
|
||||
SeedName = SeedName,
|
||||
SeedNoun = SeedNoun,
|
||||
RoundStart = RoundStart,
|
||||
Mysterious = Mysterious,
|
||||
|
||||
ProductPrototypes = new List<string>(ProductPrototypes),
|
||||
Chemicals = new Dictionary<string, SeedChemQuantity>(Chemicals),
|
||||
ConsumeGasses = new Dictionary<Gas, float>(ConsumeGasses),
|
||||
ExudeGasses = new Dictionary<Gas, float>(ExudeGasses),
|
||||
|
||||
NutrientConsumption = NutrientConsumption,
|
||||
WaterConsumption = WaterConsumption,
|
||||
IdealHeat = IdealHeat,
|
||||
HeatTolerance = HeatTolerance,
|
||||
IdealLight = IdealLight,
|
||||
LightTolerance = LightTolerance,
|
||||
ToxinsTolerance = ToxinsTolerance,
|
||||
LowPressureTolerance = LowPressureTolerance,
|
||||
HighPressureTolerance = HighPressureTolerance,
|
||||
PestTolerance = PestTolerance,
|
||||
WeedTolerance = WeedTolerance,
|
||||
|
||||
Endurance = Endurance,
|
||||
Yield = Yield,
|
||||
Lifespan = Lifespan,
|
||||
Maturation = Maturation,
|
||||
Production = Production,
|
||||
GrowthStages = GrowthStages,
|
||||
HarvestRepeat = HarvestRepeat,
|
||||
Potency = Potency,
|
||||
|
||||
PlantRsi = PlantRsi,
|
||||
PlantIconState = PlantIconState,
|
||||
Bioluminescent = Bioluminescent,
|
||||
BioluminescentColor = BioluminescentColor,
|
||||
SplatPrototype = SplatPrototype
|
||||
};
|
||||
|
||||
return newSeed;
|
||||
}
|
||||
|
||||
public SeedPrototype Diverge(bool modified)
|
||||
{
|
||||
return Clone();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,28 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Server.Botany;
|
||||
using Content.Server.Botany.Components;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.GameTicking;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Plants
|
||||
namespace Content.Server.Botany.Systems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class PlantSystem : EntitySystem
|
||||
public sealed partial class BotanySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly IRobustRandom _robustRandom = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
|
||||
private int _nextUid = 0;
|
||||
private readonly Dictionary<int, Seed> _seeds = new();
|
||||
|
||||
private float _timer = 0f;
|
||||
|
||||
public IReadOnlyDictionary<int, Seed> Seeds => _seeds;
|
||||
public readonly Dictionary<int, SeedPrototype> Seeds = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -27,6 +30,8 @@ namespace Content.Server.Plants
|
|||
|
||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
|
||||
|
||||
InitializeSeeds();
|
||||
|
||||
PopulateDatabase();
|
||||
}
|
||||
|
||||
|
|
@ -34,22 +39,22 @@ namespace Content.Server.Plants
|
|||
{
|
||||
_nextUid = 0;
|
||||
|
||||
_seeds.Clear();
|
||||
Seeds.Clear();
|
||||
|
||||
foreach (var seed in _prototypeManager.EnumeratePrototypes<Seed>())
|
||||
foreach (var seed in _prototypeManager.EnumeratePrototypes<SeedPrototype>())
|
||||
{
|
||||
AddSeedToDatabase(seed);
|
||||
}
|
||||
}
|
||||
|
||||
public bool AddSeedToDatabase(Seed seed)
|
||||
public bool AddSeedToDatabase(SeedPrototype seed)
|
||||
{
|
||||
// If it's not -1, it's already in the database. Probably.
|
||||
if (seed.Uid != -1)
|
||||
return false;
|
||||
|
||||
seed.Uid = GetNextSeedUid();
|
||||
_seeds[seed.Uid] = seed;
|
||||
Seeds[seed.Uid] = seed;
|
||||
return true;
|
||||
}
|
||||
|
||||
35
Content.Server/Botany/Systems/BotanySystem.Produce.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
using Content.Server.Botany.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Server.Botany.Systems;
|
||||
|
||||
public partial class BotanySystem
|
||||
{
|
||||
public void ProduceGrown(EntityUid uid, ProduceComponent produce)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<SeedPrototype>(produce.SeedName, out var seed))
|
||||
return;
|
||||
|
||||
if (TryComp(uid, out SpriteComponent? sprite))
|
||||
{
|
||||
sprite.LayerSetRSI(0, seed.PlantRsi);
|
||||
sprite.LayerSetState(0, seed.PlantIconState);
|
||||
}
|
||||
|
||||
var solutionContainer = _solutionContainerSystem.EnsureSolution(uid, produce.SolutionName);
|
||||
|
||||
solutionContainer.RemoveAllSolution();
|
||||
foreach (var (chem, quantity) in seed.Chemicals)
|
||||
{
|
||||
var amount = FixedPoint2.New(quantity.Min);
|
||||
if (quantity.PotencyDivisor > 0 && seed.Potency > 0)
|
||||
amount += FixedPoint2.New(seed.Potency / quantity.PotencyDivisor);
|
||||
amount = FixedPoint2.New((int) MathHelper.Clamp(amount.Float(), quantity.Min, quantity.Max));
|
||||
solutionContainer.MaxVolume += amount;
|
||||
solutionContainer.AddReagent(chem, amount);
|
||||
}
|
||||
}
|
||||
}
|
||||
143
Content.Server/Botany/Systems/BotanySystem.Seed.cs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Botany.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Botany.Systems;
|
||||
|
||||
public partial class BotanySystem
|
||||
{
|
||||
public void InitializeSeeds()
|
||||
{
|
||||
SubscribeLocalEvent<SeedComponent, ExaminedEvent>(OnExamined);
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, SeedComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!args.IsInDetailsRange)
|
||||
return;
|
||||
|
||||
if (!_prototypeManager.TryIndex<SeedPrototype>(component.SeedName, out var seed))
|
||||
return;
|
||||
|
||||
args.PushMarkup(Loc.GetString($"seed-component-description", ("seedName", seed.DisplayName)));
|
||||
|
||||
if (!seed.RoundStart)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString($"seed-component-has-variety-tag", ("seedUid", seed.Uid)));
|
||||
}
|
||||
else
|
||||
{
|
||||
args.PushMarkup(Loc.GetString($"seed-component-plant-yield-text", ("seedYield", seed.Yield)));
|
||||
args.PushMarkup(Loc.GetString($"seed-component-plant-potency-text", ("seedPotency", seed.Potency)));
|
||||
}
|
||||
}
|
||||
|
||||
#region SeedPrototype prototype stuff
|
||||
|
||||
public EntityUid SpawnSeedPacket(SeedPrototype proto, EntityCoordinates transformCoordinates)
|
||||
{
|
||||
var seed = Spawn(SeedPrototype.Prototype, transformCoordinates);
|
||||
|
||||
var seedComp = EnsureComp<SeedComponent>(seed);
|
||||
seedComp.SeedName = proto.ID;
|
||||
|
||||
if (TryComp(seed, out SpriteComponent? sprite))
|
||||
{
|
||||
// TODO visualizer
|
||||
// SeedPrototype state will always be seed. Blame the spriter if that's not the case!
|
||||
sprite.LayerSetSprite(0, new SpriteSpecifier.Rsi(proto.PlantRsi, "seed"));
|
||||
}
|
||||
|
||||
string val = Loc.GetString("botany-seed-packet-name", ("seedName", proto.SeedName), ("seedNoun", proto.SeedNoun));
|
||||
MetaData(seed).EntityName = val;
|
||||
|
||||
return seed;
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> AutoHarvest(SeedPrototype proto, EntityCoordinates position, int yieldMod = 1)
|
||||
{
|
||||
if (position.IsValid(EntityManager) &&
|
||||
proto.ProductPrototypes.Count > 0)
|
||||
return GenerateProduct(proto, position, yieldMod);
|
||||
|
||||
return Enumerable.Empty<EntityUid>();
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> Harvest(SeedPrototype proto, EntityUid user, int yieldMod = 1)
|
||||
{
|
||||
if (AddSeedToDatabase(proto)) proto.Name = proto.Uid.ToString();
|
||||
|
||||
if (proto.ProductPrototypes.Count == 0 || proto.Yield <= 0)
|
||||
{
|
||||
_popupSystem.PopupCursor(Loc.GetString("botany-harvest-fail-message"),
|
||||
Filter.Entities(user));
|
||||
return Enumerable.Empty<EntityUid>();
|
||||
}
|
||||
|
||||
_popupSystem.PopupCursor(Loc.GetString("botany-harvest-success-message", ("name", proto.DisplayName)),
|
||||
Filter.Entities(user));
|
||||
return GenerateProduct(proto, Transform(user).Coordinates, yieldMod);
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> GenerateProduct(SeedPrototype proto, EntityCoordinates position, int yieldMod = 1)
|
||||
{
|
||||
var totalYield = 0;
|
||||
if (proto.Yield > -1)
|
||||
{
|
||||
if (yieldMod < 0)
|
||||
{
|
||||
yieldMod = 1;
|
||||
totalYield = proto.Yield;
|
||||
}
|
||||
else
|
||||
totalYield = proto.Yield * yieldMod;
|
||||
|
||||
totalYield = Math.Max(1, totalYield);
|
||||
}
|
||||
|
||||
var products = new List<EntityUid>();
|
||||
|
||||
for (var i = 0; i < totalYield; i++)
|
||||
{
|
||||
var product = _robustRandom.Pick(proto.ProductPrototypes);
|
||||
|
||||
var entity = Spawn(product, position);
|
||||
entity.RandomOffset(0.25f);
|
||||
products.Add(entity);
|
||||
|
||||
var produce = EnsureComp<ProduceComponent>(entity);
|
||||
|
||||
produce.SeedName = proto.ID;
|
||||
ProduceGrown(entity, produce);
|
||||
|
||||
if (proto.Mysterious)
|
||||
{
|
||||
var metaData = MetaData(entity);
|
||||
metaData.EntityName += "?";
|
||||
metaData.EntityDescription += " " + Loc.GetString("botany-mysterious-description-addon");
|
||||
}
|
||||
}
|
||||
|
||||
return products;
|
||||
}
|
||||
|
||||
public bool CanHarvest(SeedPrototype proto, EntityUid? held = null)
|
||||
{
|
||||
return !proto.Ligneous || proto.Ligneous && held != null && held.Value.HasTag("BotanySharp");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
32
Content.Server/Botany/Systems/LogSystem.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using Content.Server.Botany.Components;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Botany.Systems;
|
||||
|
||||
public sealed class LogSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<LogComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
}
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, LogComponent component, InteractUsingEvent args)
|
||||
{
|
||||
if (args.Used.HasTag("BotanySharp"))
|
||||
{
|
||||
for (var i = 0; i < component.SpawnCount; i++)
|
||||
{
|
||||
var plank = Spawn(component.SpawnedPrototype, Transform(uid).Coordinates);
|
||||
plank.RandomOffset(0.25f);
|
||||
}
|
||||
|
||||
QueueDel(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Content.Server/Botany/Systems/SeedExtractorSystem.cs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
using Content.Server.Botany.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Botany.Systems;
|
||||
|
||||
public sealed class SeedExtractorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly BotanySystem _botanySystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SeedExtractorComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
}
|
||||
|
||||
private void OnInteractUsing(EntityUid uid, SeedExtractorComponent component, InteractUsingEvent args)
|
||||
{
|
||||
if (!TryComp<ApcPowerReceiverComponent>(uid, out var powerReceiverComponent) || !powerReceiverComponent.Powered)
|
||||
return;
|
||||
|
||||
if (TryComp(args.Used, out ProduceComponent? produce))
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<SeedPrototype>(produce.SeedName, out var seed))
|
||||
return;
|
||||
|
||||
_popupSystem.PopupCursor(Loc.GetString("seed-extractor-component-interact-message",("name", args.Used)),
|
||||
Filter.Entities(args.User));
|
||||
|
||||
QueueDel(args.Used);
|
||||
|
||||
var random = _random.Next(component.MinSeeds, component.MaxSeeds);
|
||||
var coords = Transform(uid).Coordinates;
|
||||
|
||||
for (var i = 0; i < random; i++)
|
||||
{
|
||||
_botanySystem.SpawnSeedPacket(seed, coords);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,10 +34,8 @@ namespace Content.Server.Chemistry.Components
|
|||
/// containers, and can directly inject into a mobs bloodstream.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public class InjectorComponent : SharedInjectorComponent, IAfterInteract, IUse
|
||||
public class InjectorComponent : SharedInjectorComponent
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
|
||||
public const string SolutionName = "injector";
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -46,7 +44,7 @@ namespace Content.Server.Chemistry.Components
|
|||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("injectOnly")]
|
||||
private bool _injectOnly;
|
||||
public bool InjectOnly;
|
||||
|
||||
/// <summary>
|
||||
/// Amount to inject or draw on each usage. If the injector is inject only, it will
|
||||
|
|
@ -54,7 +52,7 @@ namespace Content.Server.Chemistry.Components
|
|||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("transferAmount")]
|
||||
private FixedPoint2 _transferAmount = FixedPoint2.New(5);
|
||||
public FixedPoint2 TransferAmount = FixedPoint2.New(5);
|
||||
|
||||
/// <summary>
|
||||
/// Injection delay (seconds) when the target is a mob.
|
||||
|
|
@ -90,331 +88,5 @@ namespace Content.Server.Chemistry.Components
|
|||
Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
Dirty();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle between draw/inject state if applicable
|
||||
/// </summary>
|
||||
private void Toggle(EntityUid user)
|
||||
{
|
||||
if (_injectOnly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string msg;
|
||||
switch (ToggleState)
|
||||
{
|
||||
case InjectorToggleMode.Inject:
|
||||
ToggleState = InjectorToggleMode.Draw;
|
||||
msg = "injector-component-drawing-text";
|
||||
break;
|
||||
case InjectorToggleMode.Draw:
|
||||
ToggleState = InjectorToggleMode.Inject;
|
||||
msg = "injector-component-injecting-text";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
Owner.PopupMessage(user, Loc.GetString(msg));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when clicking on entities while holding in active hand
|
||||
/// </summary>
|
||||
/// <param name="eventArgs"></param>
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (CancelToken != null)
|
||||
{
|
||||
CancelToken.Cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!eventArgs.CanReach)
|
||||
return false;
|
||||
|
||||
var solutionsSys = EntitySystem.Get<SolutionContainerSystem>();
|
||||
//Make sure we have the attacking entity
|
||||
if (eventArgs.Target is not {Valid: true} target ||
|
||||
!_entities.HasComponent<SolutionContainerManagerComponent>(Owner))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Is the target a mob? If yes, use a do-after to give them time to respond.
|
||||
if (_entities.HasComponent<MobStateComponent>(target) ||
|
||||
_entities.HasComponent<BloodstreamComponent>(target))
|
||||
{
|
||||
if (!await TryInjectDoAfter(eventArgs.User, target))
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle injecting/drawing for solutions
|
||||
if (ToggleState == InjectorToggleMode.Inject)
|
||||
{
|
||||
if (solutionsSys.TryGetInjectableSolution(target, out var injectableSolution))
|
||||
{
|
||||
TryInject(target, injectableSolution, eventArgs.User, false);
|
||||
}
|
||||
else if (solutionsSys.TryGetRefillableSolution(target, out var refillableSolution))
|
||||
{
|
||||
TryInject(target, refillableSolution, eventArgs.User, true);
|
||||
}
|
||||
else if (_entities.TryGetComponent(target, out BloodstreamComponent? bloodstream))
|
||||
{
|
||||
TryInjectIntoBloodstream(bloodstream, eventArgs.User);
|
||||
}
|
||||
else
|
||||
{
|
||||
eventArgs.User.PopupMessage(eventArgs.User,
|
||||
Loc.GetString("injector-component-cannot-transfer-message",
|
||||
("target", target)));
|
||||
}
|
||||
}
|
||||
else if (ToggleState == InjectorToggleMode.Draw)
|
||||
{
|
||||
if (solutionsSys.TryGetDrawableSolution(target, out var drawableSolution))
|
||||
{
|
||||
TryDraw(target, drawableSolution, eventArgs.User);
|
||||
}
|
||||
else
|
||||
{
|
||||
eventArgs.User.PopupMessage(eventArgs.User,
|
||||
Loc.GetString("injector-component-cannot-draw-message",
|
||||
("target", target)));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send informative pop-up messages and wait for a do-after to complete.
|
||||
/// </summary>
|
||||
public async Task<bool> TryInjectDoAfter(EntityUid user, EntityUid target)
|
||||
{
|
||||
var popupSys = EntitySystem.Get<SharedPopupSystem>();
|
||||
|
||||
// Create a pop-up for the user
|
||||
popupSys.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, Filter.Entities(user));
|
||||
|
||||
if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
|
||||
return false;
|
||||
|
||||
// Get entity for logging. Log with EntityUids when?
|
||||
var logSys = EntitySystem.Get<AdminLogSystem>();
|
||||
|
||||
var actualDelay = MathF.Max(Delay, 1f);
|
||||
if (user != target)
|
||||
{
|
||||
// Create a pop-up for the target
|
||||
var userName = _entities.GetComponent<MetaDataComponent>(user).EntityName;
|
||||
popupSys.PopupEntity(Loc.GetString("injector-component-injecting-target",
|
||||
("user", userName)), user, Filter.Entities(target));
|
||||
|
||||
// Check if the target is incapacitated or in combat mode and modify time accordingly.
|
||||
if (_entities.TryGetComponent<MobStateComponent>(target, out var mobState) &&
|
||||
mobState.IsIncapacitated())
|
||||
{
|
||||
actualDelay /= 2;
|
||||
}
|
||||
else if (_entities.TryGetComponent<CombatModeComponent>(target, out var combat) &&
|
||||
combat.IsInCombatMode)
|
||||
{
|
||||
// Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
|
||||
// combat with fast syringes & lag.
|
||||
actualDelay += 1;
|
||||
}
|
||||
|
||||
// Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
|
||||
if (ToggleState == InjectorToggleMode.Inject)
|
||||
{
|
||||
logSys.Add(LogType.ForceFeed,
|
||||
$"{_entities.ToPrettyString(user):user} is attempting to inject {_entities.ToPrettyString(target):target} with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}");
|
||||
// TODO solution pretty string.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Self-injections take half as long.
|
||||
actualDelay /= 2;
|
||||
|
||||
if (ToggleState == InjectorToggleMode.Inject)
|
||||
logSys.Add(LogType.Ingestion,
|
||||
$"{_entities.ToPrettyString(user):user} is attempting to inject themselves with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}.");
|
||||
//TODO solution pretty string.
|
||||
}
|
||||
|
||||
CancelToken = new();
|
||||
var status = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(
|
||||
new DoAfterEventArgs(user, actualDelay, CancelToken.Token, target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnTargetMove = true,
|
||||
MovementThreshold = 1.0f
|
||||
});
|
||||
CancelToken = null;
|
||||
|
||||
return status == DoAfterStatus.Finished;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when use key is pressed when held in active hand
|
||||
/// </summary>
|
||||
/// <param name="eventArgs"></param>
|
||||
/// <returns></returns>
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
Toggle(eventArgs.User);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void TryInjectIntoBloodstream(BloodstreamComponent targetBloodstream, EntityUid user)
|
||||
{
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = FixedPoint2.Min(_transferAmount, targetBloodstream.Solution.AvailableVolume);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("injector-component-cannot-inject-message", ("target", targetBloodstream.Owner)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution =
|
||||
EntitySystem.Get<SolutionContainerSystem>().SplitSolution(user, targetBloodstream.Solution, realTransferAmount);
|
||||
|
||||
var bloodstreamSys = EntitySystem.Get<BloodstreamSystem>();
|
||||
bloodstreamSys.TryAddToBloodstream((targetBloodstream).Owner, removedSolution, targetBloodstream);
|
||||
|
||||
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
|
||||
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("injector-component-inject-success-message",
|
||||
("amount", removedSolution.TotalVolume),
|
||||
("target", targetBloodstream.Owner)));
|
||||
Dirty();
|
||||
AfterInject();
|
||||
}
|
||||
|
||||
private void TryInject(EntityUid targetEntity, Solution targetSolution, EntityUid user, bool asRefill)
|
||||
{
|
||||
if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
|
||||
|| solution.CurrentVolume == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = FixedPoint2.Min(_transferAmount, targetSolution.AvailableVolume);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("injector-component-target-already-full-message", ("target", targetEntity)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution = EntitySystem.Get<SolutionContainerSystem>().SplitSolution(Owner, solution, realTransferAmount);
|
||||
|
||||
removedSolution.DoEntityReaction(targetEntity, ReactionMethod.Injection);
|
||||
|
||||
if (!asRefill)
|
||||
{
|
||||
EntitySystem.Get<SolutionContainerSystem>()
|
||||
.Inject(targetEntity, targetSolution, removedSolution);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntitySystem.Get<SolutionContainerSystem>()
|
||||
.Refill(targetEntity, targetSolution, removedSolution);
|
||||
}
|
||||
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("injector-component-transfer-success-message",
|
||||
("amount", removedSolution.TotalVolume),
|
||||
("target", targetEntity)));
|
||||
Dirty();
|
||||
AfterInject();
|
||||
}
|
||||
|
||||
private void AfterInject()
|
||||
{
|
||||
// Automatically set syringe to draw after completely draining it.
|
||||
if (EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
|
||||
&& solution.CurrentVolume == 0)
|
||||
{
|
||||
ToggleState = InjectorToggleMode.Draw;
|
||||
}
|
||||
}
|
||||
|
||||
private void AfterDraw()
|
||||
{
|
||||
// Automatically set syringe to inject after completely filling it.
|
||||
if (EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
|
||||
&& solution.AvailableVolume == 0)
|
||||
{
|
||||
ToggleState = InjectorToggleMode.Inject;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryDraw(EntityUid targetEntity, Solution targetSolution, EntityUid user)
|
||||
{
|
||||
if (!EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
|
||||
|| solution.AvailableVolume == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = FixedPoint2.Min(_transferAmount, targetSolution.DrawAvailable);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("injector-component-target-is-empty-message", ("target", targetEntity)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution = EntitySystem.Get<SolutionContainerSystem>()
|
||||
.Draw(targetEntity, targetSolution, realTransferAmount);
|
||||
|
||||
if (!EntitySystem.Get<SolutionContainerSystem>().TryAddSolution(targetEntity, solution, removedSolution))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Owner.PopupMessage(user,
|
||||
Loc.GetString("injector-component-draw-success-message",
|
||||
("amount", removedSolution.TotalVolume),
|
||||
("target", targetEntity)));
|
||||
Dirty();
|
||||
AfterDraw();
|
||||
}
|
||||
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
_entities.EntitySysManager.GetEntitySystem<SolutionContainerSystem>()
|
||||
.TryGetSolution(Owner, SolutionName, out var solution);
|
||||
|
||||
var currentVolume = solution?.CurrentVolume ?? FixedPoint2.Zero;
|
||||
var maxVolume = solution?.MaxVolume ?? FixedPoint2.Zero;
|
||||
|
||||
return new InjectorComponentState(currentVolume, maxVolume, ToggleState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,385 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Server.Chemistry.Components.SolutionManager;
|
||||
using Content.Server.CombatMode;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems;
|
||||
|
||||
public sealed partial class ChemistrySystem
|
||||
{
|
||||
private void InitializeInjector()
|
||||
{
|
||||
SubscribeLocalEvent<InjectorComponent, SolutionChangedEvent>(OnSolutionChange);
|
||||
SubscribeLocalEvent<InjectorComponent, HandDeselectedEvent>(OnInjectorDeselected);
|
||||
SubscribeLocalEvent<InjectorComponent, ComponentStartup>(OnInjectorStartup);
|
||||
SubscribeLocalEvent<InjectorComponent, UseInHandEvent>(OnInjectorUse);
|
||||
SubscribeLocalEvent<InjectorComponent, AfterInteractEvent>(OnInjectorAfterInteract);
|
||||
SubscribeLocalEvent<InjectorComponent, ComponentGetState>(OnInjectorGetState);
|
||||
|
||||
SubscribeLocalEvent<InjectionCompleteEvent>(OnInjectionComplete);
|
||||
SubscribeLocalEvent<InjectionCancelledEvent>(OnInjectionCancelled);
|
||||
}
|
||||
|
||||
private static void OnInjectionCancelled(InjectionCancelledEvent ev)
|
||||
{
|
||||
ev.Component.CancelToken = null;
|
||||
}
|
||||
|
||||
private void OnInjectionComplete(InjectionCompleteEvent ev)
|
||||
{
|
||||
var component = ev.Component;
|
||||
var user = ev.User;
|
||||
var target = ev.Target;
|
||||
|
||||
component.CancelToken = null;
|
||||
|
||||
// Handle injecting/drawing for solutions
|
||||
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
|
||||
{
|
||||
if (_solutions.TryGetInjectableSolution(target, out var injectableSolution))
|
||||
{
|
||||
TryInject(component, target, injectableSolution, user, false);
|
||||
}
|
||||
else if (_solutions.TryGetRefillableSolution(target, out var refillableSolution))
|
||||
{
|
||||
TryInject(component, target, refillableSolution, user, true);
|
||||
}
|
||||
else if (TryComp<BloodstreamComponent>(target, out var bloodstream))
|
||||
{
|
||||
TryInjectIntoBloodstream(component, bloodstream, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-cannot-transfer-message",
|
||||
("target", target)), component.Owner, Filter.Entities(user));
|
||||
}
|
||||
}
|
||||
else if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Draw)
|
||||
{
|
||||
if (_solutions.TryGetDrawableSolution(target, out var drawableSolution))
|
||||
{
|
||||
TryDraw(component, target, drawableSolution, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-cannot-draw-message",
|
||||
("target", target)), component.Owner, Filter.Entities(user));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnInjectorDeselected(EntityUid uid, InjectorComponent component, HandDeselectedEvent args)
|
||||
{
|
||||
component.CancelToken?.Cancel();
|
||||
component.CancelToken = null;
|
||||
}
|
||||
|
||||
private void OnSolutionChange(EntityUid uid, InjectorComponent component, SolutionChangedEvent args)
|
||||
{
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
private void OnInjectorGetState(EntityUid uid, InjectorComponent component, ref ComponentGetState args)
|
||||
{
|
||||
_solutions.TryGetSolution(uid, InjectorComponent.SolutionName, out var solution);
|
||||
|
||||
var currentVolume = solution?.CurrentVolume ?? FixedPoint2.Zero;
|
||||
var maxVolume = solution?.MaxVolume ?? FixedPoint2.Zero;
|
||||
|
||||
args.State = new SharedInjectorComponent.InjectorComponentState(currentVolume, maxVolume, component.ToggleState);
|
||||
}
|
||||
|
||||
private void OnInjectorAfterInteract(EntityUid uid, InjectorComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach) return;
|
||||
|
||||
if (component.CancelToken != null)
|
||||
{
|
||||
component.CancelToken.Cancel();
|
||||
component.CancelToken = null;
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_blocker.CanInteract(args.User))
|
||||
return;
|
||||
|
||||
//Make sure we have the attacking entity
|
||||
if (args.Target is not { Valid: true } target ||
|
||||
!HasComp<SolutionContainerManagerComponent>(uid))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Is the target a mob? If yes, use a do-after to give them time to respond.
|
||||
if (HasComp<MobStateComponent>(target) ||
|
||||
HasComp<BloodstreamComponent>(target))
|
||||
{
|
||||
InjectDoAfter(component, args.User, target);
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInjectorStartup(EntityUid uid, InjectorComponent component, ComponentStartup args)
|
||||
{
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
private void OnInjectorUse(EntityUid uid, InjectorComponent component, UseInHandEvent args)
|
||||
{
|
||||
if (args.Handled) return;
|
||||
|
||||
Toggle(component, args.User);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle between draw/inject state if applicable
|
||||
/// </summary>
|
||||
private void Toggle(InjectorComponent component, EntityUid user)
|
||||
{
|
||||
if (component.InjectOnly)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string msg;
|
||||
switch (component.ToggleState)
|
||||
{
|
||||
case SharedInjectorComponent.InjectorToggleMode.Inject:
|
||||
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
|
||||
msg = "injector-component-drawing-text";
|
||||
break;
|
||||
case SharedInjectorComponent.InjectorToggleMode.Draw:
|
||||
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
|
||||
msg = "injector-component-injecting-text";
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
|
||||
_popup.PopupEntity(Loc.GetString(msg), component.Owner, Filter.Entities(user));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send informative pop-up messages and wait for a do-after to complete.
|
||||
/// </summary>
|
||||
private void InjectDoAfter(InjectorComponent component, EntityUid user, EntityUid target)
|
||||
{
|
||||
// Create a pop-up for the user
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-injecting-user"), target, Filter.Entities(user));
|
||||
|
||||
if (!_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution))
|
||||
return;
|
||||
|
||||
// Get entity for logging. Log with EntityUids when?
|
||||
var actualDelay = MathF.Max(component.Delay, 1f);
|
||||
if (user != target)
|
||||
{
|
||||
// Create a pop-up for the target
|
||||
var userName = MetaData(user).EntityName;
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-injecting-target",
|
||||
("user", userName)), user, Filter.Entities(target));
|
||||
|
||||
// Check if the target is incapacitated or in combat mode and modify time accordingly.
|
||||
if (TryComp<MobStateComponent>(target, out var mobState) && mobState.IsIncapacitated())
|
||||
{
|
||||
actualDelay /= 2;
|
||||
}
|
||||
else if (TryComp<CombatModeComponent>(target, out var combat) && combat.IsInCombatMode)
|
||||
{
|
||||
// Slightly increase the delay when the target is in combat mode. Helps prevents cheese injections in
|
||||
// combat with fast syringes & lag.
|
||||
actualDelay += 1;
|
||||
}
|
||||
|
||||
// Add an admin log, using the "force feed" log type. It's not quite feeding, but the effect is the same.
|
||||
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
|
||||
{
|
||||
_logs.Add(LogType.ForceFeed,
|
||||
$"{EntityManager.ToPrettyString(user):user} is attempting to inject {EntityManager.ToPrettyString(target):target} with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}");
|
||||
// TODO solution pretty string.
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Self-injections take half as long.
|
||||
actualDelay /= 2;
|
||||
|
||||
if (component.ToggleState == SharedInjectorComponent.InjectorToggleMode.Inject)
|
||||
_logs.Add(LogType.Ingestion,
|
||||
$"{EntityManager.ToPrettyString(user):user} is attempting to inject themselves with a solution {SolutionContainerSystem.ToPrettyString(solution):solution}.");
|
||||
//TODO solution pretty string.
|
||||
}
|
||||
|
||||
component.CancelToken = new CancellationTokenSource();
|
||||
|
||||
_doAfter.DoAfter(new DoAfterEventArgs(user, actualDelay, component.CancelToken.Token, target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnTargetMove = true,
|
||||
MovementThreshold = 0.1f,
|
||||
BroadcastFinishedEvent = new InjectionCompleteEvent()
|
||||
{
|
||||
Component = component,
|
||||
User = user,
|
||||
Target = target,
|
||||
},
|
||||
BroadcastCancelledEvent = new InjectionCancelledEvent()
|
||||
{
|
||||
Component = component,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void TryInjectIntoBloodstream(InjectorComponent component, BloodstreamComponent targetBloodstream, EntityUid user)
|
||||
{
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetBloodstream.Solution.AvailableVolume);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-cannot-inject-message", ("target", targetBloodstream.Owner)),
|
||||
component.Owner, Filter.Entities(user));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution = _solutions.SplitSolution(user, targetBloodstream.Solution, realTransferAmount);
|
||||
|
||||
_blood.TryAddToBloodstream((targetBloodstream).Owner, removedSolution, targetBloodstream);
|
||||
|
||||
removedSolution.DoEntityReaction(targetBloodstream.Owner, ReactionMethod.Injection);
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-inject-success-message",
|
||||
("amount", removedSolution.TotalVolume),
|
||||
("target", targetBloodstream.Owner)), component.Owner, Filter.Entities(user));
|
||||
|
||||
Dirty(component);
|
||||
AfterInject(component);
|
||||
}
|
||||
|
||||
private void TryInject(InjectorComponent component, EntityUid targetEntity, Solution targetSolution, EntityUid user, bool asRefill)
|
||||
{
|
||||
if (!_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution)
|
||||
|| solution.CurrentVolume == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetSolution.AvailableVolume);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-target-already-full-message", ("target", targetEntity)),
|
||||
component.Owner, Filter.Entities(user));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution = _solutions.SplitSolution(component.Owner, solution, realTransferAmount);
|
||||
|
||||
removedSolution.DoEntityReaction(targetEntity, ReactionMethod.Injection);
|
||||
|
||||
if (!asRefill)
|
||||
{
|
||||
_solutions.Inject(targetEntity, targetSolution, removedSolution);
|
||||
}
|
||||
else
|
||||
{
|
||||
_solutions.Refill(targetEntity, targetSolution, removedSolution);
|
||||
}
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-transfer-success-message",
|
||||
("amount", removedSolution.TotalVolume),
|
||||
("target", targetEntity)), component.Owner, Filter.Entities(user));
|
||||
|
||||
Dirty(component);
|
||||
AfterInject(component);
|
||||
}
|
||||
|
||||
private void AfterInject(InjectorComponent component)
|
||||
{
|
||||
// Automatically set syringe to draw after completely draining it.
|
||||
if (_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution)
|
||||
&& solution.CurrentVolume == 0)
|
||||
{
|
||||
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Draw;
|
||||
}
|
||||
}
|
||||
|
||||
private void AfterDraw(InjectorComponent component)
|
||||
{
|
||||
// Automatically set syringe to inject after completely filling it.
|
||||
if (_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution)
|
||||
&& solution.AvailableVolume == 0)
|
||||
{
|
||||
component.ToggleState = SharedInjectorComponent.InjectorToggleMode.Inject;
|
||||
}
|
||||
}
|
||||
|
||||
private void TryDraw(InjectorComponent component, EntityUid targetEntity, Solution targetSolution, EntityUid user)
|
||||
{
|
||||
if (!_solutions.TryGetSolution(component.Owner, InjectorComponent.SolutionName, out var solution)
|
||||
|| solution.AvailableVolume == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get transfer amount. May be smaller than _transferAmount if not enough room
|
||||
var realTransferAmount = FixedPoint2.Min(component.TransferAmount, targetSolution.DrawAvailable);
|
||||
|
||||
if (realTransferAmount <= 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-target-is-empty-message", ("target", targetEntity)),
|
||||
component.Owner, Filter.Entities(user));
|
||||
return;
|
||||
}
|
||||
|
||||
// Move units from attackSolution to targetSolution
|
||||
var removedSolution = _solutions.Draw(targetEntity, targetSolution, realTransferAmount);
|
||||
|
||||
if (!_solutions.TryAddSolution(targetEntity, solution, removedSolution))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("injector-component-draw-success-message",
|
||||
("amount", removedSolution.TotalVolume),
|
||||
("target", targetEntity)), component.Owner, Filter.Entities(user));
|
||||
|
||||
Dirty(component);
|
||||
AfterDraw(component);
|
||||
}
|
||||
|
||||
private sealed class InjectionCompleteEvent : EntityEventArgs
|
||||
{
|
||||
public InjectorComponent Component { get; init; } = default!;
|
||||
public EntityUid User { get; init; }
|
||||
public EntityUid Target { get; init; }
|
||||
}
|
||||
|
||||
private sealed class InjectionCancelledEvent : EntityEventArgs
|
||||
{
|
||||
public InjectorComponent Component { get; init; } = default!;
|
||||
}
|
||||
}
|
||||
25
Content.Server/Chemistry/EntitySystems/ChemistrySystem.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems;
|
||||
|
||||
public sealed partial class ChemistrySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||
[Dependency] private readonly AdminLogSystem _logs = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _blood = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutions = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
InitializeHypospray();
|
||||
InitializeInjector();
|
||||
}
|
||||
}
|
||||
|
|
@ -6,13 +6,10 @@ using Robust.Shared.GameObjects;
|
|||
|
||||
namespace Content.Server.Chemistry.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class HypospraySystem : EntitySystem
|
||||
public sealed partial class ChemistrySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
private void InitializeHypospray()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HyposprayComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<HyposprayComponent, ClickAttackEvent>(OnClickAttack);
|
||||
SubscribeLocalEvent<HyposprayComponent, SolutionChangedEvent>(OnSolutionChange);
|
||||
|
|
@ -27,6 +24,7 @@ namespace Content.Server.Chemistry.EntitySystems
|
|||
{
|
||||
if (!args.CanReach)
|
||||
return;
|
||||
|
||||
var target = args.Target;
|
||||
var user = args.User;
|
||||
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
using Content.Server.Chemistry.Components;
|
||||
using Content.Shared.Hands;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using System;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public class InjectorSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<InjectorComponent, SolutionChangedEvent>(OnSolutionChange);
|
||||
SubscribeLocalEvent<InjectorComponent, HandDeselectedEvent>(OnInjectorDeselected);
|
||||
}
|
||||
|
||||
private void OnInjectorDeselected(EntityUid uid, InjectorComponent component, HandDeselectedEvent args)
|
||||
{
|
||||
if (component.CancelToken != null)
|
||||
{
|
||||
component.CancelToken.Cancel();
|
||||
component.CancelToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSolutionChange(EntityUid uid, InjectorComponent component, SolutionChangedEvent args)
|
||||
{
|
||||
Dirty(component);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,9 @@
|
|||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Crayon;
|
||||
using Content.Server.Decals;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
|
@ -27,16 +11,12 @@ using Robust.Shared.ViewVariables;
|
|||
namespace Content.Server.Crayon
|
||||
{
|
||||
[RegisterComponent]
|
||||
public class CrayonComponent : SharedCrayonComponent, IAfterInteract, IUse, IDropped, ISerializationHooks
|
||||
public sealed class CrayonComponent : SharedCrayonComponent, ISerializationHooks
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
[DataField("useSound")]
|
||||
private SoundSpecifier? _useSound = null;
|
||||
[DataField("useSound")] public SoundSpecifier? UseSound;
|
||||
|
||||
[ViewVariables]
|
||||
public Color Color { get; set; }
|
||||
public Color Color { get; private set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Charges { get; set; }
|
||||
|
|
@ -45,104 +25,11 @@ namespace Content.Server.Crayon
|
|||
[DataField("capacity")]
|
||||
public int Capacity { get; set; } = 30;
|
||||
|
||||
[ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(CrayonUiKey.Key);
|
||||
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(CrayonUiKey.Key);
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
Color = Color.FromName(_color);
|
||||
}
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
if (UserInterface != null)
|
||||
{
|
||||
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
|
||||
}
|
||||
Charges = Capacity;
|
||||
|
||||
// Get the first one from the catalog and set it as default
|
||||
var decal = _prototypeManager.EnumeratePrototypes<DecalPrototype>().FirstOrDefault(x => x.Tags.Contains("crayon"));
|
||||
SelectedState = decal?.ID ?? string.Empty;
|
||||
Dirty();
|
||||
}
|
||||
|
||||
private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage serverMsg)
|
||||
{
|
||||
switch (serverMsg.Message)
|
||||
{
|
||||
case CrayonSelectMessage msg:
|
||||
// Check if the selected state is valid
|
||||
if (_prototypeManager.TryIndex<DecalPrototype>(msg.State, out var prototype) && prototype.Tags.Contains("crayon"))
|
||||
{
|
||||
SelectedState = msg.State;
|
||||
Dirty();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override ComponentState GetComponentState()
|
||||
{
|
||||
return new CrayonComponentState(_color, SelectedState, Charges, Capacity);
|
||||
}
|
||||
|
||||
// Opens the selection window
|
||||
bool IUse.UseEntity(UseEntityEventArgs eventArgs)
|
||||
{
|
||||
if (_entMan.TryGetComponent(eventArgs.User, out ActorComponent? actor))
|
||||
{
|
||||
UserInterface?.Toggle(actor.PlayerSession);
|
||||
if (UserInterface?.SessionHasOpen(actor.PlayerSession) == true)
|
||||
{
|
||||
// Tell the user interface the selected stuff
|
||||
UserInterface.SetState(
|
||||
new CrayonBoundUserInterfaceState(SelectedState, Color));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
if (!eventArgs.User.InRangeUnobstructed(eventArgs.ClickLocation, ignoreInsideBlocker: false, popup: true,
|
||||
collisionMask: Shared.Physics.CollisionGroup.MobImpassable))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Charges <= 0)
|
||||
{
|
||||
eventArgs.User.PopupMessage(Loc.GetString("crayon-interact-not-enough-left-text"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!eventArgs.ClickLocation.IsValid(_entMan))
|
||||
{
|
||||
eventArgs.User.PopupMessage(Loc.GetString("crayon-interact-invalid-location"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!EntitySystem.Get<DecalSystem>().TryAddDecal(SelectedState, eventArgs.ClickLocation.Offset(new Vector2(-0.5f,-0.5f)), out _, Color.FromName(_color), cleanable: true))
|
||||
return false;
|
||||
|
||||
if (_useSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(Owner), _useSound.GetSound(), Owner, AudioHelpers.WithVariation(0.125f));
|
||||
|
||||
// Decrease "Ammo"
|
||||
Charges--;
|
||||
Dirty();
|
||||
EntitySystem.Get<AdminLogSystem>().Add(LogType.CrayonDraw, LogImpact.Low, $"{_entMan.ToPrettyString(eventArgs.User):user} drew a {_color:color} {SelectedState}");
|
||||
return true;
|
||||
}
|
||||
|
||||
void IDropped.Dropped(DroppedEventArgs eventArgs)
|
||||
{
|
||||
if (_entMan.TryGetComponent(eventArgs.User, out ActorComponent? actor))
|
||||
UserInterface?.Close(actor.PlayerSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
121
Content.Server/Crayon/CrayonSystem.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Decals;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Crayon;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Crayon;
|
||||
|
||||
public sealed class CrayonSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly AdminLogSystem _logs = default!;
|
||||
[Dependency] private readonly DecalSystem _decals = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CrayonComponent, ComponentInit>(OnCrayonInit);
|
||||
SubscribeLocalEvent<CrayonComponent, CrayonSelectMessage>(OnCrayonBoundUI);
|
||||
SubscribeLocalEvent<CrayonComponent, UseInHandEvent>(OnCrayonUse);
|
||||
SubscribeLocalEvent<CrayonComponent, AfterInteractEvent>(OnCrayonAfterInteract);
|
||||
SubscribeLocalEvent<CrayonComponent, DroppedEvent>(OnCrayonDropped);
|
||||
SubscribeLocalEvent<CrayonComponent, ComponentGetState>(OnCrayonGetState);
|
||||
}
|
||||
|
||||
private static void OnCrayonGetState(EntityUid uid, CrayonComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new CrayonComponentState(component._color, component.SelectedState, component.Charges, component.Capacity);
|
||||
}
|
||||
|
||||
private void OnCrayonAfterInteract(EntityUid uid, CrayonComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (component.Charges <= 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("crayon-interact-not-enough-left-text"), uid, Filter.Entities(args.User));
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.ClickLocation.IsValid(EntityManager))
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("crayon-interact-invalid-location"), uid, Filter.Entities(args.User));
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if(!_decals.TryAddDecal(component.SelectedState, args.ClickLocation.Offset(new Vector2(-0.5f,-0.5f)), out _, Color.FromName(component._color), cleanable: true))
|
||||
return;
|
||||
|
||||
if (component.UseSound != null)
|
||||
SoundSystem.Play(Filter.Pvs(uid), component.UseSound.GetSound(), uid, AudioHelpers.WithVariation(0.125f));
|
||||
|
||||
// Decrease "Ammo"
|
||||
component.Charges--;
|
||||
Dirty(component);
|
||||
_logs.Add(LogType.CrayonDraw, LogImpact.Low, $"{EntityManager.ToPrettyString(args.User):user} drew a {component._color:color} {component.SelectedState}");
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnCrayonUse(EntityUid uid, CrayonComponent component, UseInHandEvent args)
|
||||
{
|
||||
// Open crayon window if neccessary.
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (!TryComp<ActorComponent>(args.User, out var actor)) return;
|
||||
|
||||
component.UserInterface?.Toggle(actor.PlayerSession);
|
||||
|
||||
if (component.UserInterface?.SessionHasOpen(actor.PlayerSession) == true)
|
||||
{
|
||||
// Tell the user interface the selected stuff
|
||||
component.UserInterface.SetState(new CrayonBoundUserInterfaceState(component.SelectedState, component.Color));
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnCrayonBoundUI(EntityUid uid, CrayonComponent component, CrayonSelectMessage args)
|
||||
{
|
||||
// Check if the selected state is valid
|
||||
if (!_prototypeManager.TryIndex<DecalPrototype>(args.State, out var prototype) || !prototype.Tags.Contains("crayon")) return;
|
||||
|
||||
component.SelectedState = args.State;
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
private void OnCrayonInit(EntityUid uid, CrayonComponent component, ComponentInit args)
|
||||
{
|
||||
component.Charges = component.Capacity;
|
||||
|
||||
// Get the first one from the catalog and set it as default
|
||||
var decal = _prototypeManager.EnumeratePrototypes<DecalPrototype>().FirstOrDefault(x => x.Tags.Contains("crayon"));
|
||||
component.SelectedState = decal?.ID ?? string.Empty;
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
private void OnCrayonDropped(EntityUid uid, CrayonComponent component, DroppedEvent args)
|
||||
{
|
||||
if (TryComp<ActorComponent>(args.UserUid, out var actor))
|
||||
component.UserInterface?.Close(actor.PlayerSession);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Power.Nodes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Electrocution
|
||||
|
|
@ -16,10 +15,13 @@ namespace Content.Server.Electrocution
|
|||
[DataField("node")]
|
||||
public string NodeName = default!;
|
||||
|
||||
public override IEnumerable<Node> GetReachableNodes()
|
||||
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
var ent = IoCManager.Resolve<IEntityManager>();
|
||||
if (!ent.TryGetComponent(CableEntity, out NodeContainerComponent? nodeContainer))
|
||||
if (!nodeQuery.TryGetComponent(CableEntity, out var nodeContainer))
|
||||
yield break;
|
||||
|
||||
if (nodeContainer.TryGetNode(NodeName, out Node? node))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
using Content.Server.Explosion.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Physics.Collision.Shapes;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Content.Server.Explosion.Components
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Raises a <see cref="TriggerEvent"/> whenever an entity collides with a fixture attached to the owner of this component.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class TriggerOnProximityComponent : Component
|
||||
{
|
||||
public const string FixtureID = "trigger-on-proximity-fixture";
|
||||
|
||||
public HashSet<PhysicsComponent> Colliding = new();
|
||||
|
||||
[DataField("shape", required: true)]
|
||||
public IPhysShape Shape { get; set; } = new PhysShapeCircle {Radius = 2};
|
||||
|
||||
/// <summary>
|
||||
/// How long the the proximity trigger animation plays for.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("animationDuration")]
|
||||
public float AnimationDuration = 0.3f;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the entity needs to be anchored for the proximity to work.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("requiresAnchored")]
|
||||
public bool RequiresAnchored { get; set; } = true;
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("enabled")]
|
||||
public bool Enabled = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("cooldown")]
|
||||
public float Cooldown { get; set; } = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// How much cooldown has elapsed (if relevant).
|
||||
/// </summary>
|
||||
public float Accumulator = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// What speed should the other object be moving at to trigger the proximity fixture?
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("triggerSpeed")]
|
||||
public float TriggerSpeed { get; set; } = 3.5f;
|
||||
|
||||
/// <summary>
|
||||
/// If this proximity is triggered should we continually repeat it?
|
||||
/// </summary>
|
||||
[DataField("repeating")]
|
||||
internal bool Repeating = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Server.Explosion.Components;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Trigger;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems;
|
||||
|
||||
public sealed partial class TriggerSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// Anything that has stuff touching it (to check speed) or is on cooldown.
|
||||
/// </summary>
|
||||
private HashSet<TriggerOnProximityComponent> _activeProximities = new();
|
||||
|
||||
private void InitializeProximity()
|
||||
{
|
||||
SubscribeLocalEvent<TriggerOnProximityComponent, StartCollideEvent>(OnProximityStartCollide);
|
||||
SubscribeLocalEvent<TriggerOnProximityComponent, EndCollideEvent>(OnProximityEndCollide);
|
||||
SubscribeLocalEvent<TriggerOnProximityComponent, ComponentStartup>(OnProximityStartup);
|
||||
SubscribeLocalEvent<TriggerOnProximityComponent, ComponentShutdown>(OnProximityShutdown);
|
||||
SubscribeLocalEvent<TriggerOnProximityComponent, AnchorStateChangedEvent>(OnProximityAnchor);
|
||||
}
|
||||
|
||||
private void OnProximityAnchor(EntityUid uid, TriggerOnProximityComponent component, ref AnchorStateChangedEvent args)
|
||||
{
|
||||
component.Enabled = !component.RequiresAnchored ||
|
||||
args.Anchored;
|
||||
|
||||
SetProximityAppearance(uid, component);
|
||||
|
||||
if (!component.Enabled)
|
||||
{
|
||||
_activeProximities.Remove(component);
|
||||
component.Colliding.Clear();
|
||||
}
|
||||
// Re-check for contacts as we cleared them.
|
||||
else if (TryComp<PhysicsComponent>(uid, out var body))
|
||||
{
|
||||
_broadphase.RegenerateContacts(body);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProximityShutdown(EntityUid uid, TriggerOnProximityComponent component, ComponentShutdown args)
|
||||
{
|
||||
_activeProximities.Remove(component);
|
||||
component.Colliding.Clear();
|
||||
}
|
||||
|
||||
private void OnProximityStartup(EntityUid uid, TriggerOnProximityComponent component, ComponentStartup args)
|
||||
{
|
||||
component.Enabled = !component.RequiresAnchored ||
|
||||
EntityManager.GetComponent<TransformComponent>(uid).Anchored;
|
||||
|
||||
SetProximityAppearance(uid, component);
|
||||
|
||||
if (!TryComp<PhysicsComponent>(uid, out var body)) return;
|
||||
|
||||
_fixtures.CreateFixture(body, new Fixture(body, component.Shape)
|
||||
{
|
||||
// TODO: Should probably have these settable via datafield but I'm lazy and it's a pain
|
||||
CollisionLayer = (int) (CollisionGroup.MobImpassable | CollisionGroup.SmallImpassable | CollisionGroup.VaultImpassable), Hard = false, ID = TriggerOnProximityComponent.FixtureID
|
||||
});
|
||||
}
|
||||
|
||||
private void OnProximityStartCollide(EntityUid uid, TriggerOnProximityComponent component, StartCollideEvent args)
|
||||
{
|
||||
if (args.OurFixture.ID != TriggerOnProximityComponent.FixtureID) return;
|
||||
|
||||
_activeProximities.Add(component);
|
||||
component.Colliding.Add(args.OtherFixture.Body);
|
||||
}
|
||||
|
||||
private void OnProximityEndCollide(EntityUid uid, TriggerOnProximityComponent component, EndCollideEvent args)
|
||||
{
|
||||
if (args.OurFixture.ID != TriggerOnProximityComponent.FixtureID) return;
|
||||
|
||||
component.Colliding.Remove(args.OtherFixture.Body);
|
||||
|
||||
if (component.Colliding.Count == 0)
|
||||
_activeProximities.Remove(component);
|
||||
}
|
||||
|
||||
private void SetProximityAppearance(EntityUid uid, TriggerOnProximityComponent component)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(uid, out AppearanceComponent? appearanceComponent))
|
||||
{
|
||||
appearanceComponent.SetData(ProximityTriggerVisualState.State, component.Enabled ? ProximityTriggerVisuals.Inactive : ProximityTriggerVisuals.Off);
|
||||
}
|
||||
}
|
||||
|
||||
private void Activate(TriggerOnProximityComponent component)
|
||||
{
|
||||
DebugTools.Assert(component.Enabled);
|
||||
|
||||
if (!component.Repeating)
|
||||
{
|
||||
component.Enabled = false;
|
||||
_activeProximities.Remove(component);
|
||||
component.Colliding.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
component.Accumulator = component.Cooldown;
|
||||
}
|
||||
|
||||
SetProximityAppearance(component.Owner, component);
|
||||
Trigger(component.Owner);
|
||||
}
|
||||
|
||||
private void UpdateProximity(float frameTime)
|
||||
{
|
||||
var toRemove = new RemQueue<TriggerOnProximityComponent>();
|
||||
|
||||
foreach (var comp in _activeProximities)
|
||||
{
|
||||
if (!comp.Enabled)
|
||||
{
|
||||
toRemove.Add(comp);
|
||||
continue;
|
||||
}
|
||||
|
||||
MetaDataComponent? metadata = null;
|
||||
|
||||
if (Deleted(comp.Owner, metadata))
|
||||
{
|
||||
toRemove.Add(comp);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Paused(comp.Owner, metadata)) continue;
|
||||
|
||||
comp.Accumulator -= frameTime;
|
||||
|
||||
if (comp.Accumulator > 0f) continue;
|
||||
|
||||
// Alright now that we have no cd check everything in range.
|
||||
|
||||
foreach (var colliding in comp.Colliding)
|
||||
{
|
||||
if (Deleted(colliding.Owner)) continue;
|
||||
|
||||
if (colliding.LinearVelocity.Length < comp.TriggerSpeed) continue;
|
||||
|
||||
// Trigger!
|
||||
Activate(comp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var prox in toRemove)
|
||||
{
|
||||
_activeProximities.Remove(prox);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,25 +6,29 @@ using Content.Server.Doors.Systems;
|
|||
using Content.Server.Explosion.Components;
|
||||
using Content.Server.Flash;
|
||||
using Content.Server.Flash.Components;
|
||||
using Content.Server.Projectiles.Components;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Throwing;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Threading;
|
||||
using Content.Server.Construction.Components;
|
||||
using Content.Shared.Trigger;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
using Content.Shared.Physics;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Content.Server.Explosion.EntitySystems
|
||||
{
|
||||
/// <summary>
|
||||
/// Raised whenever something is Triggered on the entity.
|
||||
/// </summary>
|
||||
public class TriggerEvent : HandledEntityEventArgs
|
||||
public sealed class TriggerEvent : HandledEntityEventArgs
|
||||
{
|
||||
public EntityUid Triggered { get; }
|
||||
public EntityUid? User { get; }
|
||||
|
|
@ -40,16 +44,19 @@ namespace Content.Server.Explosion.EntitySystems
|
|||
public sealed partial class TriggerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ExplosionSystem _explosions = default!;
|
||||
[Dependency] private readonly FixtureSystem _fixtures = default!;
|
||||
[Dependency] private readonly FlashSystem _flashSystem = default!;
|
||||
[Dependency] private readonly DoorSystem _sharedDoorSystem = default!;
|
||||
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
InitializeProximity();
|
||||
InitializeOnUse();
|
||||
|
||||
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(HandleCollide);
|
||||
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
|
||||
|
||||
SubscribeLocalEvent<DeleteOnTriggerComponent, TriggerEvent>(HandleDeleteTrigger);
|
||||
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(HandleSoundTrigger);
|
||||
|
|
@ -88,11 +95,8 @@ namespace Content.Server.Explosion.EntitySystems
|
|||
#region Flash
|
||||
private void HandleFlashTrigger(EntityUid uid, FlashOnTriggerComponent component, TriggerEvent args)
|
||||
{
|
||||
if (component.Flashed) return;
|
||||
|
||||
// TODO Make flash durations sane ffs.
|
||||
_flashSystem.FlashArea(uid, args.User, component.Range, component.Duration * 1000f);
|
||||
component.Flashed = true;
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
|
@ -112,15 +116,9 @@ namespace Content.Server.Explosion.EntitySystems
|
|||
_sharedDoorSystem.TryToggleDoor(uid);
|
||||
}
|
||||
|
||||
private void HandleCollide(EntityUid uid, TriggerOnCollideComponent component, StartCollideEvent args)
|
||||
private void OnTriggerCollide(EntityUid uid, TriggerOnCollideComponent component, StartCollideEvent args)
|
||||
{
|
||||
EntityUid? user = null;
|
||||
if (EntityManager.TryGetComponent(uid, out ProjectileComponent projectile))
|
||||
user = projectile.Shooter;
|
||||
else if (EntityManager.TryGetComponent(uid, out ThrownItemComponent thrown))
|
||||
user = thrown.Thrower;
|
||||
|
||||
Trigger(component.Owner, user);
|
||||
Trigger(component.Owner);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -144,5 +142,12 @@ namespace Content.Server.Explosion.EntitySystems
|
|||
Trigger(triggered, user);
|
||||
});
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
UpdateProximity(frameTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,5 @@ namespace Content.Server.Flash.Components
|
|||
{
|
||||
[DataField("range")] internal float Range = 1.0f;
|
||||
[DataField("duration")] internal float Duration = 8.0f;
|
||||
|
||||
internal bool Flashed;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
Content.Server/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Global usings for Content.Server
|
||||
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using Robust.Shared.Analyzers;
|
||||
global using Robust.Shared.Log;
|
||||
global using Robust.Shared.Localization;
|
||||
global using Robust.Shared.GameObjects;
|
||||
global using Robust.Shared.IoC;
|
||||
global using Robust.Shared.Maths;
|
||||
global using Robust.Shared.ViewVariables;
|
||||
global using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
|
@ -12,6 +12,7 @@ using Robust.Shared.Enums;
|
|||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ namespace Content.Server.NodeContainer.EntitySystems
|
|||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly INodeGroupFactory _nodeGroupFactory = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
|
||||
private readonly List<int> _visDeletes = new();
|
||||
private readonly List<BaseNodeGroup> _visSends = new();
|
||||
|
|
@ -141,6 +143,9 @@ namespace Content.Server.NodeContainer.EntitySystems
|
|||
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
var xformQuery = EntityManager.GetEntityQuery<TransformComponent>();
|
||||
var nodeQuery = EntityManager.GetEntityQuery<NodeContainerComponent>();
|
||||
|
||||
foreach (var toRemove in _toRemove)
|
||||
{
|
||||
if (toRemove.NodeGroup == null)
|
||||
|
|
@ -182,7 +187,11 @@ namespace Content.Server.NodeContainer.EntitySystems
|
|||
QueueRemakeGroup((BaseNodeGroup) node.NodeGroup);
|
||||
}
|
||||
|
||||
foreach (var compatible in GetCompatibleNodes(node))
|
||||
// GetCompatibleNodes will involve getting the transform & grid as most connection requirements are
|
||||
// based on position & anchored neighbours However, here more than one node could be attached to the
|
||||
// same parent. So there is probably a better way of doing this.
|
||||
|
||||
foreach (var compatible in GetCompatibleNodes(node, xformQuery, nodeQuery))
|
||||
{
|
||||
ClearReachableIfNecessary(compatible);
|
||||
|
||||
|
|
@ -303,14 +312,23 @@ namespace Content.Server.NodeContainer.EntitySystems
|
|||
return allNodes;
|
||||
}
|
||||
|
||||
private static IEnumerable<Node> GetCompatibleNodes(Node node)
|
||||
private IEnumerable<Node> GetCompatibleNodes(Node node, EntityQuery<TransformComponent> xformQuery, EntityQuery<NodeContainerComponent> nodeQuery)
|
||||
{
|
||||
foreach (var reachable in node.GetReachableNodes())
|
||||
var xform = xformQuery.GetComponent(node.Owner);
|
||||
_mapManager.TryGetGrid(xform.GridID, out var grid);
|
||||
|
||||
if (!node.Connectable(EntityManager, xform))
|
||||
yield break;
|
||||
|
||||
foreach (var reachable in node.GetReachableNodes(xform, nodeQuery, xformQuery, grid, EntityManager))
|
||||
{
|
||||
DebugTools.Assert(reachable != node, "GetReachableNodes() should not include self.");
|
||||
|
||||
if (reachable.Connectable && reachable.NodeGroupID == node.NodeGroupID)
|
||||
if (reachable.NodeGroupID == node.NodeGroupID
|
||||
&& reachable.Connectable(EntityManager, xformQuery.GetComponent(reachable.Owner)))
|
||||
{
|
||||
yield return reachable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,16 +12,18 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
[DataDefinition]
|
||||
public class AdjacentNode : Node
|
||||
{
|
||||
public override IEnumerable<Node> GetReachableNodes()
|
||||
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
if (!IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Anchored)
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
|
||||
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
foreach (var (_, node) in NodeHelpers.GetCardinalNeighborNodes(entMan, grid, gridIndex))
|
||||
foreach (var (_, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex))
|
||||
{
|
||||
if (node != this)
|
||||
yield return node;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Content.Server.NodeContainer.EntitySystems;
|
|||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
|
|
@ -36,7 +37,20 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
/// <summary>
|
||||
/// If this node should be considered for connection by other nodes.
|
||||
/// </summary>
|
||||
public bool Connectable => !Deleting && Anchored;
|
||||
public virtual bool Connectable(IEntityManager entMan, TransformComponent? xform = null)
|
||||
{
|
||||
if (Deleting)
|
||||
return false;
|
||||
|
||||
if (entMan.IsQueuedForDeletion(Owner))
|
||||
return false;
|
||||
|
||||
if (!NeedAnchored)
|
||||
return true;
|
||||
|
||||
xform ??= entMan.GetComponent<TransformComponent>(Owner);
|
||||
return xform.Anchored;
|
||||
}
|
||||
|
||||
protected bool Anchored => !NeedAnchored || IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Anchored;
|
||||
|
||||
|
|
@ -145,6 +159,10 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
/// of this asymmetric relation are made to manually update with <see cref="NodeGroupSystem.QueueReflood"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract IEnumerable<Node> GetReachableNodes();
|
||||
public abstract IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
|
@ -12,11 +12,11 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
/// </summary>
|
||||
public static class NodeHelpers
|
||||
{
|
||||
public static IEnumerable<Node> GetNodesInTile(IEntityManager entMan, IMapGrid grid, Vector2i coords)
|
||||
public static IEnumerable<Node> GetNodesInTile(EntityQuery<NodeContainerComponent> nodeQuery, IMapGrid grid, Vector2i coords)
|
||||
{
|
||||
foreach (var entityUid in grid.GetAnchoredEntities(coords))
|
||||
{
|
||||
if (!entMan.TryGetComponent(entityUid, out NodeContainerComponent? container))
|
||||
if (!nodeQuery.TryGetComponent(entityUid, out var container))
|
||||
continue;
|
||||
|
||||
foreach (var node in container.Nodes.Values)
|
||||
|
|
@ -27,14 +27,14 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
}
|
||||
|
||||
public static IEnumerable<(Direction dir, Node node)> GetCardinalNeighborNodes(
|
||||
IEntityManager entMan,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
IMapGrid grid,
|
||||
Vector2i coords,
|
||||
bool includeSameTile = true)
|
||||
{
|
||||
foreach (var (dir, entityUid) in GetCardinalNeighborCells(grid, coords, includeSameTile))
|
||||
{
|
||||
if (!entMan.TryGetComponent(entityUid, out NodeContainerComponent? container))
|
||||
if (!nodeQuery.TryGetComponent(entityUid, out var container))
|
||||
continue;
|
||||
|
||||
foreach (var node in container.Nodes.Values)
|
||||
|
|
@ -68,22 +68,5 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
foreach (var uid in grid.GetAnchoredEntities(coords + (-1, 0)))
|
||||
yield return (Direction.West, uid);
|
||||
}
|
||||
|
||||
public static Vector2i TileOffsetForDir(Direction dir)
|
||||
{
|
||||
return dir switch
|
||||
{
|
||||
Direction.Invalid => (0, 0),
|
||||
Direction.South => (0, -1),
|
||||
Direction.SouthEast => (1, -1),
|
||||
Direction.East => (1, 0),
|
||||
Direction.NorthEast => (1, 1),
|
||||
Direction.North => (0, 1),
|
||||
Direction.NorthWest => (-1, 1),
|
||||
Direction.West => (-1, 0),
|
||||
Direction.SouthWest => (-1, -1),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(dir), dir, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ using Content.Shared.Atmos;
|
|||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
|
@ -89,6 +90,11 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
[DataField("connectionsEnabled")]
|
||||
private bool _connectionsEnabled = true;
|
||||
|
||||
public override bool Connectable(IEntityManager entMan, TransformComponent? xform = null)
|
||||
{
|
||||
return _connectionsEnabled && base.Connectable(entMan, xform);
|
||||
}
|
||||
|
||||
[DataField("rotationsEnabled")]
|
||||
public bool RotationsEnabled { get; set; } = true;
|
||||
|
||||
|
|
@ -112,12 +118,6 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
}
|
||||
}
|
||||
|
||||
public void AssumeAir(GasMixture giver)
|
||||
{
|
||||
if(PipeNet != null)
|
||||
EntitySystem.Get<AtmosphereSystem>().Merge(PipeNet.Air, giver);
|
||||
}
|
||||
|
||||
[ViewVariables]
|
||||
[DataField("volume")]
|
||||
public float Volume { get; set; } = DefaultVolume;
|
||||
|
|
@ -150,25 +150,16 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
UpdateAppearance();
|
||||
}
|
||||
|
||||
public override IEnumerable<Node> GetReachableNodes()
|
||||
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
for (var i = 0; i < PipeDirectionHelpers.AllPipeDirections; i++)
|
||||
{
|
||||
var pipeDir = (PipeDirection) (1 << i);
|
||||
|
||||
if (!CurrentPipeDirection.HasDirection(pipeDir))
|
||||
continue;
|
||||
|
||||
foreach (var pipe in LinkableNodesInDirection(pipeDir))
|
||||
{
|
||||
yield return pipe;
|
||||
}
|
||||
}
|
||||
|
||||
if(_alwaysReachable != null)
|
||||
if (_alwaysReachable != null)
|
||||
{
|
||||
var remQ = new RemQueue<PipeNode>();
|
||||
foreach(var pipe in _alwaysReachable)
|
||||
foreach (var pipe in _alwaysReachable)
|
||||
{
|
||||
if (pipe.Deleting)
|
||||
{
|
||||
|
|
@ -177,64 +168,58 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
yield return pipe;
|
||||
}
|
||||
|
||||
foreach(var pipe in remQ)
|
||||
foreach (var pipe in remQ)
|
||||
{
|
||||
_alwaysReachable.Remove(pipe);
|
||||
}
|
||||
}
|
||||
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
var pos = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
for (var i = 0; i < PipeDirectionHelpers.PipeDirections; i++)
|
||||
{
|
||||
var pipeDir = (PipeDirection) (1 << i);
|
||||
|
||||
if (!CurrentPipeDirection.HasDirection(pipeDir))
|
||||
continue;
|
||||
|
||||
foreach (var pipe in LinkableNodesInDirection(pos, pipeDir, grid, nodeQuery))
|
||||
{
|
||||
yield return pipe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pipes that can connect to us from entities on the tile or adjacent in a direction.
|
||||
/// </summary>
|
||||
private IEnumerable<PipeNode> LinkableNodesInDirection(PipeDirection pipeDir)
|
||||
private IEnumerable<PipeNode> LinkableNodesInDirection(Vector2i pos, PipeDirection pipeDir, IMapGrid grid,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery)
|
||||
{
|
||||
if (!Anchored)
|
||||
yield break;
|
||||
|
||||
foreach (var pipe in PipesInDirection(pipeDir))
|
||||
foreach (var pipe in PipesInDirection(pos, pipeDir, grid, nodeQuery))
|
||||
{
|
||||
if (pipe.ConnectionsEnabled && pipe.CurrentPipeDirection.HasDirection(pipeDir.GetOpposite()))
|
||||
if (pipe.NodeGroupID == NodeGroupID
|
||||
&& pipe.CurrentPipeDirection.HasDirection(pipeDir.GetOpposite()))
|
||||
{
|
||||
yield return pipe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pipes from entities on the tile adjacent in a direction.
|
||||
/// </summary>
|
||||
protected IEnumerable<PipeNode> PipesInDirection(PipeDirection pipeDir)
|
||||
protected IEnumerable<PipeNode> PipesInDirection(Vector2i pos, PipeDirection pipeDir, IMapGrid grid,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery)
|
||||
{
|
||||
if (!IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Anchored)
|
||||
yield break;
|
||||
var offsetPos = pos.Offset(pipeDir.ToDirection());
|
||||
|
||||
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
|
||||
var position = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates;
|
||||
foreach (var entity in grid.GetInDir(position, pipeDir.ToDirection()))
|
||||
foreach (var entity in grid.GetAnchoredEntities(offsetPos))
|
||||
{
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<NodeContainerComponent>(entity, out var container))
|
||||
continue;
|
||||
|
||||
foreach (var node in container.Nodes.Values)
|
||||
{
|
||||
if (node is PipeNode pipe)
|
||||
yield return pipe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the pipes from entities on the same tile.
|
||||
/// </summary>
|
||||
protected IEnumerable<PipeNode> PipesInTile()
|
||||
{
|
||||
if (!IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Anchored)
|
||||
yield break;
|
||||
|
||||
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
|
||||
var position = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates;
|
||||
foreach (var entity in grid.GetLocal(position))
|
||||
{
|
||||
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<NodeContainerComponent>(entity, out var container))
|
||||
if (!nodeQuery.TryGetComponent(entity, out var container))
|
||||
continue;
|
||||
|
||||
foreach (var node in container.Nodes.Values)
|
||||
|
|
@ -271,6 +256,13 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
{
|
||||
ConnectedDirections = PipeDirection.None;
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var xform = entMan.GetComponent<TransformComponent>(Owner);
|
||||
if (!IoCManager.Resolve<IMapManager>().TryGetGrid(xform.GridID, out var grid))
|
||||
return;
|
||||
var pos = grid.WorldToTile(xform.WorldPosition);
|
||||
var query = entMan.GetEntityQuery<NodeContainerComponent>();
|
||||
|
||||
for (var i = 0; i < PipeDirectionHelpers.AllPipeDirections; i++)
|
||||
{
|
||||
var pipeDir = (PipeDirection) (1 << i);
|
||||
|
|
@ -278,9 +270,9 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
if (!CurrentPipeDirection.HasDirection(pipeDir))
|
||||
continue;
|
||||
|
||||
foreach (var pipe in LinkableNodesInDirection(pipeDir))
|
||||
foreach (var pipe in LinkableNodesInDirection(pos, pipeDir, grid, query))
|
||||
{
|
||||
if (pipe.Connectable && pipe.NodeGroupID == NodeGroupID)
|
||||
if (pipe.Connectable(entMan) && pipe.NodeGroupID == NodeGroupID)
|
||||
{
|
||||
ConnectedDirections |= pipeDir;
|
||||
break;
|
||||
|
|
@ -295,11 +287,18 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
/// </summary>
|
||||
private void UpdateAdjacentConnectedDirections()
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var xform = entMan.GetComponent<TransformComponent>(Owner);
|
||||
if (!IoCManager.Resolve<IMapManager>().TryGetGrid(xform.GridID, out var grid))
|
||||
return;
|
||||
var pos = grid.WorldToTile(xform.WorldPosition);
|
||||
var query = entMan.GetEntityQuery<NodeContainerComponent>();
|
||||
|
||||
for (var i = 0; i < PipeDirectionHelpers.PipeDirections; i++)
|
||||
{
|
||||
var pipeDir = (PipeDirection) (1 << i);
|
||||
|
||||
foreach (var pipe in LinkableNodesInDirection(pipeDir))
|
||||
foreach (var pipe in LinkableNodesInDirection(pos, pipeDir, grid, query))
|
||||
{
|
||||
pipe.UpdateConnectedDirections();
|
||||
pipe.UpdateAppearance();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.NodeContainer.Nodes
|
||||
|
|
@ -6,15 +8,24 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
[DataDefinition]
|
||||
public class PortPipeNode : PipeNode
|
||||
{
|
||||
public override IEnumerable<Node> GetReachableNodes()
|
||||
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
foreach (var node in PipesInTile())
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex))
|
||||
{
|
||||
if (node is PortablePipeNode)
|
||||
yield return node;
|
||||
}
|
||||
|
||||
foreach (var node in base.GetReachableNodes())
|
||||
foreach (var node in base.GetReachableNodes(xform, nodeQuery, xformQuery, grid, entMan))
|
||||
{
|
||||
yield return node;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.NodeContainer.Nodes
|
||||
|
|
@ -6,15 +8,24 @@ namespace Content.Server.NodeContainer.Nodes
|
|||
[DataDefinition]
|
||||
public class PortablePipeNode : PipeNode
|
||||
{
|
||||
public override IEnumerable<Node> GetReachableNodes()
|
||||
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
foreach (var node in PipesInTile())
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex))
|
||||
{
|
||||
if (node is PortPipeNode)
|
||||
yield return node;
|
||||
}
|
||||
|
||||
foreach (var node in base.GetReachableNodes())
|
||||
foreach (var node in base.GetReachableNodes(xform, nodeQuery, xformQuery, grid, entMan))
|
||||
{
|
||||
yield return node;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ namespace Content.Server.Nutrition.Components
|
|||
{
|
||||
[RegisterComponent]
|
||||
[Friend(typeof(DrinkSystem))]
|
||||
public class DrinkComponent : Component
|
||||
public sealed class DrinkComponent : Component
|
||||
{
|
||||
[DataField("solution")]
|
||||
public string SolutionName { get; set; } = DefaultSolutionName;
|
||||
|
|
@ -41,6 +41,12 @@ namespace Content.Server.Nutrition.Components
|
|||
[DataField("burstSound")]
|
||||
public SoundSpecifier BurstSound = new SoundPathSpecifier("/Audio/Effects/flash_bang.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes to drink this yourself.
|
||||
/// </summary>
|
||||
[DataField("delay")]
|
||||
public float Delay = 1;
|
||||
|
||||
/// <summary>
|
||||
/// This is how many seconds it takes to force feed someone this drink.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,12 @@ namespace Content.Server.Nutrition.Components
|
|||
[DataField("eatMessage")]
|
||||
public string EatMessage = "food-nom";
|
||||
|
||||
/// <summary>
|
||||
/// How long it takes to eat the food personally.
|
||||
/// </summary>
|
||||
[DataField("delay")]
|
||||
public float Delay = 1;
|
||||
|
||||
/// <summary>
|
||||
/// This is how many seconds it takes to force feed someone this food.
|
||||
/// Should probably be smaller for small items like pills.
|
||||
|
|
|
|||
|
|
@ -1,25 +1,22 @@
|
|||
using System.Threading;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.Components.SolutionManager;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Nutrition.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Throwing;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
|
|
@ -54,24 +51,10 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
SubscribeLocalEvent<DrinkComponent, ComponentInit>(OnDrinkInit);
|
||||
SubscribeLocalEvent<DrinkComponent, LandEvent>(HandleLand);
|
||||
SubscribeLocalEvent<DrinkComponent, UseInHandEvent>(OnUse);
|
||||
SubscribeLocalEvent<DrinkComponent, HandDeselectedEvent>(OnDrinkDeselected);
|
||||
SubscribeLocalEvent<DrinkComponent, AfterInteractEvent>(AfterInteract);
|
||||
SubscribeLocalEvent<DrinkComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<SharedBodyComponent, ForceDrinkEvent>(OnForceDrink);
|
||||
SubscribeLocalEvent<ForceDrinkCancelledEvent>(OnForceDrinkCancelled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the user is currently forcing someone do drink, this cancels the attempt if they swap hands or
|
||||
/// otherwise loose the item. Prevents force-feeding dual-wielding.
|
||||
/// </summary>
|
||||
private void OnDrinkDeselected(EntityUid uid, DrinkComponent component, HandDeselectedEvent args)
|
||||
{
|
||||
if (component.CancelToken != null)
|
||||
{
|
||||
component.CancelToken.Cancel();
|
||||
component.CancelToken = null;
|
||||
}
|
||||
SubscribeLocalEvent<SharedBodyComponent, DrinkEvent>(OnDrink);
|
||||
SubscribeLocalEvent<DrinkCancelledEvent>(OnDrinkCancelled);
|
||||
}
|
||||
|
||||
public bool IsEmpty(EntityUid uid, DrinkComponent? component = null)
|
||||
|
|
@ -85,9 +68,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
private void OnExamined(EntityUid uid, DrinkComponent component, ExaminedEvent args)
|
||||
{
|
||||
if (!component.Opened || !args.IsInDetailsRange)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var color = IsEmpty(uid, component) ? "gray" : "yellow";
|
||||
var openedText =
|
||||
|
|
@ -106,9 +87,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
component.Opened = opened;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, component.SolutionName, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (EntityManager.TryGetComponent<AppearanceComponent>(uid, out var appearance))
|
||||
{
|
||||
|
|
@ -125,7 +104,6 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
EntityManager.RemoveComponent<RefillableSolutionComponent>(uid);
|
||||
EntityManager.RemoveComponent<DrainableSolutionComponent>(uid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void AfterInteract(EntityUid uid, DrinkComponent component, AfterInteractEvent args)
|
||||
|
|
@ -133,22 +111,23 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
if (args.Handled || args.Target == null || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (args.User == args.Target)
|
||||
{
|
||||
args.Handled = TryUseDrink(uid, args.User);
|
||||
// CanInteract already checked CanInteract
|
||||
if (!_actionBlockerSystem.CanUse(args.User))
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.User == args.Target)
|
||||
args.Handled = TryUseDrink(uid, args.User, component);
|
||||
else
|
||||
args.Handled = TryForceDrink(uid, args.User, args.Target.Value, component);
|
||||
args.Handled = TryDrink(args.User, args.Target.Value, component);
|
||||
}
|
||||
|
||||
private void OnUse(EntityUid uid, DrinkComponent component, UseInHandEvent args)
|
||||
{
|
||||
if (args.Handled) return;
|
||||
|
||||
if (!args.User.InRangeUnobstructed(uid, popup: true))
|
||||
{
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!component.Opened)
|
||||
{
|
||||
//Do the opening stuff like playing the sounds.
|
||||
|
|
@ -158,13 +137,11 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
return;
|
||||
}
|
||||
|
||||
if (_solutionContainerSystem.DrainAvailable(uid) <= 0)
|
||||
{
|
||||
args.User.PopupMessage(Loc.GetString("drink-component-on-use-is-empty", ("owner", uid)));
|
||||
// CanUse already checked; trying to keep it consistent if we interact with ourselves.
|
||||
if (!_actionBlockerSystem.CanInteract(args.User))
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = TryUseDrink(uid, args.User, component);
|
||||
args.Handled = TryDrink(args.User, args.User, component);
|
||||
}
|
||||
|
||||
private void HandleLand(EntityUid uid, DrinkComponent component, LandEvent args)
|
||||
|
|
@ -219,94 +196,8 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
appearance.SetData(DrinkCanStateVisual.Opened, component.Opened);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to drink some of a drink. Returns true if any interaction took place, including generation of
|
||||
/// pop-up messages.
|
||||
/// </summary>
|
||||
private bool TryUseDrink(EntityUid uid, EntityUid userUid, DrinkComponent? drink = null)
|
||||
private bool TryDrink(EntityUid user, EntityUid target, DrinkComponent drink)
|
||||
{
|
||||
if (!Resolve(uid, ref drink))
|
||||
return false;
|
||||
|
||||
// if currently being used to force-feed, cancel that action.
|
||||
if (drink.CancelToken != null)
|
||||
{
|
||||
drink.CancelToken.Cancel();
|
||||
drink.CancelToken = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!drink.Opened)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open",
|
||||
("owner", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), uid, Filter.Entities(userUid));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(userUid, out SharedBodyComponent? body))
|
||||
return false;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetDrainableSolution((drink).Owner, out var drinkSolution) ||
|
||||
drinkSolution.DrainAvailable <= 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty",
|
||||
("entity", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), uid, Filter.Entities(userUid));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(userUid, out var stomachs, body))
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-cannot-drink"),
|
||||
userUid, Filter.Entities(userUid));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_foodSystem.IsMouthBlocked(userUid, userUid))
|
||||
return true;
|
||||
|
||||
var transferAmount = FixedPoint2.Min(drink.TransferAmount, drinkSolution.DrainAvailable);
|
||||
var drain = _solutionContainerSystem.Drain(uid, drinkSolution, transferAmount);
|
||||
var firstStomach = stomachs.FirstOrNull(
|
||||
stomach => _stomachSystem.CanTransferSolution((stomach.Comp).Owner, drain));
|
||||
|
||||
// All stomach are full or can't handle whatever solution we have.
|
||||
if (firstStomach == null)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-had-enough"),
|
||||
userUid, Filter.Entities(userUid));
|
||||
|
||||
if (EntityManager.HasComponent<RefillableSolutionComponent>(uid))
|
||||
{
|
||||
_spillableSystem.SpillAt(userUid, drain, "PuddleSmear");
|
||||
return true;
|
||||
}
|
||||
|
||||
_solutionContainerSystem.Refill(uid, drinkSolution, drain);
|
||||
return true;
|
||||
}
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(userUid), drink.UseSound.GetSound(), userUid,
|
||||
AudioParams.Default.WithVolume(-2f));
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-success-slurp"), userUid,
|
||||
Filter.Pvs(userUid));
|
||||
|
||||
drain.DoEntityReaction(userUid, ReactionMethod.Ingestion);
|
||||
_stomachSystem.TryTransferSolution((firstStomach.Value.Comp).Owner, drain, firstStomach.Value.Comp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to force someone else to drink some of a drink. Returns true if any interaction took place,
|
||||
/// including generation of pop-up messages.
|
||||
/// </summary>
|
||||
private bool TryForceDrink(EntityUid uid, EntityUid userUid, EntityUid targetUid,
|
||||
DrinkComponent? drink = null)
|
||||
{
|
||||
if (!Resolve(uid, ref drink))
|
||||
return false;
|
||||
|
||||
// cannot stack do-afters
|
||||
if (drink.CancelToken != null)
|
||||
{
|
||||
|
|
@ -315,69 +206,89 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!EntityManager.HasComponent<SharedBodyComponent>(targetUid))
|
||||
if (!EntityManager.HasComponent<SharedBodyComponent>(target))
|
||||
return false;
|
||||
|
||||
if (!drink.Opened)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-not-open",
|
||||
("owner", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), uid, Filter.Entities(userUid));
|
||||
("owner", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), drink.Owner, Filter.Entities(user));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_solutionContainerSystem.TryGetDrainableSolution(uid, out var drinkSolution) ||
|
||||
if (!_solutionContainerSystem.TryGetDrainableSolution(drink.Owner, out var drinkSolution) ||
|
||||
drinkSolution.DrainAvailable <= 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-is-empty",
|
||||
("entity", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), uid, Filter.Entities(userUid));
|
||||
("entity", EntityManager.GetComponent<MetaDataComponent>(drink.Owner).EntityName)), drink.Owner, Filter.Entities(user));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_foodSystem.IsMouthBlocked(targetUid, userUid))
|
||||
if (_foodSystem.IsMouthBlocked(target, user))
|
||||
return true;
|
||||
|
||||
EntityManager.TryGetComponent(userUid, out MetaDataComponent? meta);
|
||||
var userName = meta?.EntityName ?? string.Empty;
|
||||
if (!user.InRangeUnobstructed(drink.Owner, popup: true))
|
||||
return true;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)),
|
||||
userUid, Filter.Entities(targetUid));
|
||||
var forceDrink = user != target;
|
||||
|
||||
drink.CancelToken = new();
|
||||
_doAfterSystem.DoAfter(new DoAfterEventArgs(userUid, drink.ForceFeedDelay, drink.CancelToken.Token, targetUid)
|
||||
if (forceDrink)
|
||||
{
|
||||
EntityManager.TryGetComponent(user, out MetaDataComponent? meta);
|
||||
var userName = meta?.EntityName ?? string.Empty;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed", ("user", userName)),
|
||||
user, Filter.Entities(target));
|
||||
|
||||
// logging
|
||||
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to drink {ToPrettyString(drink.Owner):drink} {SolutionContainerSystem.ToPrettyString(drinkSolution)}");
|
||||
}
|
||||
|
||||
drink.CancelToken = new CancellationTokenSource();
|
||||
_doAfterSystem.DoAfter(new DoAfterEventArgs(user, forceDrink ? drink.ForceFeedDelay : drink.Delay, drink.CancelToken.Token, target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnTargetMove = true,
|
||||
MovementThreshold = 1.0f,
|
||||
TargetFinishedEvent = new ForceDrinkEvent(userUid, drink, drinkSolution),
|
||||
BroadcastCancelledEvent = new ForceDrinkCancelledEvent(drink),
|
||||
MovementThreshold = 0.01f,
|
||||
TargetFinishedEvent = new DrinkEvent(user, drink, drinkSolution),
|
||||
BroadcastCancelledEvent = new DrinkCancelledEvent(drink),
|
||||
NeedHand = true,
|
||||
});
|
||||
|
||||
// logging
|
||||
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(userUid):user} is forcing {ToPrettyString(targetUid):target} to drink {ToPrettyString(uid):drink} {SolutionContainerSystem.ToPrettyString(drinkSolution):solution}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at a victim when someone has force fed them a drink.
|
||||
/// </summary>
|
||||
private void OnForceDrink(EntityUid uid, SharedBodyComponent body, ForceDrinkEvent args)
|
||||
private void OnDrink(EntityUid uid, SharedBodyComponent body, DrinkEvent args)
|
||||
{
|
||||
if (args.Drink.Deleted)
|
||||
return;
|
||||
|
||||
args.Drink.CancelToken = null;
|
||||
var transferAmount = FixedPoint2.Min(args.Drink.TransferAmount, args.DrinkSolution.DrainAvailable);
|
||||
var drained = _solutionContainerSystem.Drain((args.Drink).Owner, args.DrinkSolution, transferAmount);
|
||||
var drained = _solutionContainerSystem.Drain(args.Drink.Owner, args.DrinkSolution, transferAmount);
|
||||
|
||||
var forceDrink = uid != args.User;
|
||||
|
||||
if (!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(uid, out var stomachs, body))
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-try-use-drink-cannot-drink-other"),
|
||||
_popupSystem.PopupEntity(
|
||||
forceDrink ?
|
||||
Loc.GetString("drink-component-try-use-drink-cannot-drink-other") :
|
||||
Loc.GetString("drink-component-try-use-drink-had-enough"),
|
||||
uid, Filter.Entities(args.User));
|
||||
|
||||
_spillableSystem.SpillAt(uid, drained, "PuddleSmear");
|
||||
if (EntityManager.HasComponent<RefillableSolutionComponent>(uid))
|
||||
{
|
||||
_spillableSystem.SpillAt(args.User, drained, "PuddleSmear");
|
||||
return;
|
||||
}
|
||||
|
||||
_solutionContainerSystem.Refill(uid, args.DrinkSolution, drained);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -394,25 +305,34 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
return;
|
||||
}
|
||||
|
||||
EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta);
|
||||
var targetName = targetMeta?.EntityName ?? string.Empty;
|
||||
if (forceDrink)
|
||||
{
|
||||
EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta);
|
||||
var targetName = targetMeta?.EntityName ?? string.Empty;
|
||||
|
||||
EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta);
|
||||
var userName = userMeta?.EntityName ?? string.Empty;
|
||||
EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta);
|
||||
var userName = userMeta?.EntityName ?? string.Empty;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed-success", ("user", userName)),
|
||||
uid, Filter.Entities(uid));
|
||||
_popupSystem.PopupEntity(
|
||||
Loc.GetString("drink-component-force-feed-success", ("user", userName)), uid, Filter.Entities(uid));
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("drink-component-force-feed-success-user", ("target", targetName)),
|
||||
args.User, Filter.Entities(args.User));
|
||||
_popupSystem.PopupEntity(
|
||||
Loc.GetString("drink-component-force-feed-success-user", ("target", targetName)),
|
||||
args.User, Filter.Entities(args.User));
|
||||
}
|
||||
else
|
||||
{
|
||||
_popupSystem.PopupEntity(
|
||||
Loc.GetString("drink-component-try-use-drink-success-slurp"), args.User, Filter.Pvs(args.User));
|
||||
}
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(uid), args.Drink.UseSound.GetSound(), uid, AudioParams.Default.WithVolume(-2f));
|
||||
|
||||
drained.DoEntityReaction(uid, ReactionMethod.Ingestion);
|
||||
_stomachSystem.TryTransferSolution((firstStomach.Value.Comp).Owner, drained, firstStomach.Value.Comp);
|
||||
_stomachSystem.TryTransferSolution(firstStomach.Value.Comp.Owner, drained, firstStomach.Value.Comp);
|
||||
}
|
||||
|
||||
private void OnForceDrinkCancelled(ForceDrinkCancelledEvent args)
|
||||
private static void OnDrinkCancelled(DrinkCancelledEvent args)
|
||||
{
|
||||
args.Drink.CancelToken = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
|
|
@ -22,7 +23,6 @@ using Robust.Shared.IoC;
|
|||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Item;
|
||||
|
||||
|
|
@ -31,7 +31,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
/// <summary>
|
||||
/// Handles feeding attempts both on yourself and on the target.
|
||||
/// </summary>
|
||||
internal class FoodSystem : EntitySystem
|
||||
public sealed class FoodSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly BodySystem _bodySystem = default!;
|
||||
|
|
@ -49,26 +49,12 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
|
||||
SubscribeLocalEvent<FoodComponent, UseInHandEvent>(OnUseFoodInHand);
|
||||
SubscribeLocalEvent<FoodComponent, AfterInteractEvent>(OnFeedFood);
|
||||
SubscribeLocalEvent<FoodComponent, HandDeselectedEvent>(OnFoodDeselected);
|
||||
SubscribeLocalEvent<FoodComponent, GetInteractionVerbsEvent>(AddEatVerb);
|
||||
SubscribeLocalEvent<SharedBodyComponent, ForceFeedEvent>(OnForceFeed);
|
||||
SubscribeLocalEvent<ForceFeedCancelledEvent>(OnForceFeedCancelled);
|
||||
SubscribeLocalEvent<SharedBodyComponent, FeedEvent>(OnFeed);
|
||||
SubscribeLocalEvent<ForceFeedCancelledEvent>(OnFeedCancelled);
|
||||
SubscribeLocalEvent<InventoryComponent, IngestionAttemptEvent>(OnInventoryIngestAttempt);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the user is currently force feeding someone, this cancels the attempt if they swap hands or otherwise
|
||||
/// loose the item. Prevents force-feeding dual-wielding.
|
||||
/// </summary>
|
||||
private void OnFoodDeselected(EntityUid uid, FoodComponent component, HandDeselectedEvent args)
|
||||
{
|
||||
if (component.CancelToken != null)
|
||||
{
|
||||
component.CancelToken.Cancel();
|
||||
component.CancelToken = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Eat item
|
||||
/// </summary>
|
||||
|
|
@ -77,7 +63,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
if (ev.Handled)
|
||||
return;
|
||||
|
||||
ev.Handled = TryUseFood(uid, ev.User);
|
||||
ev.Handled = TryFeed(ev.User, ev.User, foodComponent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -88,27 +74,15 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
if (args.Handled || args.Target == null || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (args.User == args.Target)
|
||||
{
|
||||
args.Handled = TryUseFood(uid, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = TryForceFeed(uid, args.User, args.Target.Value);
|
||||
args.Handled = TryFeed(args.User, args.Target.Value, foodComponent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to eat some food
|
||||
/// </summary>
|
||||
/// <param name="uid">Food entity.</param>
|
||||
/// <param name="user">Feeding initiator.</param>
|
||||
/// <returns>True if an interaction occurred (i.e., food was consumed, or a pop-up message was created)</returns>
|
||||
public bool TryUseFood(EntityUid uid, EntityUid user, FoodComponent? food = null)
|
||||
public bool TryFeed(EntityUid user, EntityUid target, FoodComponent food)
|
||||
{
|
||||
if (!Resolve(uid, ref food))
|
||||
if (!_actionBlockerSystem.CanInteract(user) || !_actionBlockerSystem.CanUse(user))
|
||||
return false;
|
||||
|
||||
// if currently being used to force-feed, cancel that action.
|
||||
// if currently being used to feed, cancel that action.
|
||||
if (food.CancelToken != null)
|
||||
{
|
||||
food.CancelToken.Cancel();
|
||||
|
|
@ -116,73 +90,134 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
return true;
|
||||
}
|
||||
|
||||
if (uid == user || //Suppresses self-eating
|
||||
EntityManager.TryGetComponent<MobStateComponent>(uid, out var mobState) && mobState.IsAlive()) // Suppresses eating alive mobs
|
||||
if (food.Owner == user || //Suppresses self-eating
|
||||
EntityManager.TryGetComponent<MobStateComponent>(food.Owner, out var mobState) && mobState.IsAlive()) // Suppresses eating alive mobs
|
||||
return false;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var solution))
|
||||
// Target can't be fed
|
||||
if (!EntityManager.HasComponent<SharedBodyComponent>(target))
|
||||
return false;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(food.Owner, food.SolutionName, out var foodSolution))
|
||||
return false;
|
||||
|
||||
if (food.UsesRemaining <= 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty", ("entity", uid)), user, Filter.Entities(user));
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty",
|
||||
("entity", food.Owner)), user, Filter.Entities(user));
|
||||
DeleteAndSpawnTrash(food, user);
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent(user, out SharedBodyComponent ? body) ||
|
||||
!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(user, out var stomachs, body))
|
||||
if (IsMouthBlocked(target, user))
|
||||
return false;
|
||||
|
||||
if (IsMouthBlocked(user, user))
|
||||
{
|
||||
if (!TryGetRequiredUtensils(user, food, out var utensils))
|
||||
return false;
|
||||
|
||||
if (!user.InRangeUnobstructed(food.Owner, popup: true))
|
||||
return true;
|
||||
|
||||
var forceFeed = user != target;
|
||||
food.CancelToken = new CancellationTokenSource();
|
||||
|
||||
if (forceFeed)
|
||||
{
|
||||
EntityManager.TryGetComponent(user, out MetaDataComponent? meta);
|
||||
var userName = meta?.EntityName ?? string.Empty;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)),
|
||||
user, Filter.Entities(target));
|
||||
|
||||
// logging
|
||||
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(food.Owner):food} {SolutionContainerSystem.ToPrettyString(foodSolution)}");
|
||||
}
|
||||
|
||||
var usedUtensils = new List<UtensilComponent>();
|
||||
_doAfterSystem.DoAfter(new DoAfterEventArgs(user, forceFeed ? food.ForceFeedDelay : food.Delay, food.CancelToken.Token, target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnTargetMove = true,
|
||||
MovementThreshold = 0.01f,
|
||||
TargetFinishedEvent = new FeedEvent(user, food, foodSolution, utensils),
|
||||
BroadcastCancelledEvent = new ForceFeedCancelledEvent(food),
|
||||
NeedHand = true,
|
||||
});
|
||||
|
||||
if (!TryGetRequiredUtensils(user, food, out var utensils))
|
||||
return true;
|
||||
return true;
|
||||
|
||||
if (!user.InRangeUnobstructed(uid, popup: true))
|
||||
return true;
|
||||
}
|
||||
|
||||
var transferAmount = food.TransferAmount != null ? FixedPoint2.Min((FixedPoint2) food.TransferAmount, solution.CurrentVolume) : solution.CurrentVolume;
|
||||
var split = _solutionContainerSystem.SplitSolution(uid, solution, transferAmount);
|
||||
private void OnFeed(EntityUid uid, SharedBodyComponent body, FeedEvent args)
|
||||
{
|
||||
if (args.Food.Deleted)
|
||||
return;
|
||||
|
||||
args.Food.CancelToken = null;
|
||||
|
||||
if (!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(uid, out var stomachs, body))
|
||||
return;
|
||||
|
||||
var transferAmount = args.Food.TransferAmount != null
|
||||
? FixedPoint2.Min((FixedPoint2) args.Food.TransferAmount, args.FoodSolution.CurrentVolume)
|
||||
: args.FoodSolution.CurrentVolume;
|
||||
|
||||
var split = _solutionContainerSystem.SplitSolution((args.Food).Owner, args.FoodSolution, transferAmount);
|
||||
var firstStomach = stomachs.FirstOrNull(
|
||||
stomach => _stomachSystem.CanTransferSolution((stomach.Comp).Owner, split));
|
||||
|
||||
var forceFeed = uid != args.User;
|
||||
|
||||
// No stomach so just popup a message that they can't eat.
|
||||
if (firstStomach == null)
|
||||
{
|
||||
_solutionContainerSystem.TryAddSolution(uid, solution, split);
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more"), user, Filter.Entities(user));
|
||||
return true;
|
||||
_solutionContainerSystem.TryAddSolution(uid, args.FoodSolution, split);
|
||||
_popupSystem.PopupEntity(
|
||||
forceFeed ?
|
||||
Loc.GetString("food-system-you-cannot-eat-any-more-other") :
|
||||
Loc.GetString("food-system-you-cannot-eat-any-more")
|
||||
, uid, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Account for partial transfer.
|
||||
split.DoEntityReaction(user, ReactionMethod.Ingestion);
|
||||
_stomachSystem.TryTransferSolution((firstStomach.Value.Comp).Owner, split, firstStomach.Value.Comp);
|
||||
split.DoEntityReaction(uid, ReactionMethod.Ingestion);
|
||||
_stomachSystem.TryTransferSolution(firstStomach.Value.Comp.Owner, split, firstStomach.Value.Comp);
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(user), food.UseSound.GetSound(), user, AudioParams.Default.WithVolume(-1f));
|
||||
_popupSystem.PopupEntity(Loc.GetString(food.EatMessage, ("food", food.Owner)), user, Filter.Entities(user));
|
||||
if (forceFeed)
|
||||
{
|
||||
EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta);
|
||||
var targetName = targetMeta?.EntityName ?? string.Empty;
|
||||
|
||||
EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta);
|
||||
var userName = userMeta?.EntityName ?? string.Empty;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName)),
|
||||
uid, Filter.Entities(uid));
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)),
|
||||
args.User, Filter.Entities(args.User));
|
||||
}
|
||||
else
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString(args.Food.EatMessage, ("food", args.Food.Owner)), args.User, Filter.Entities(args.User));
|
||||
}
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(uid), args.Food.UseSound.GetSound(), uid, AudioParams.Default.WithVolume(-1f));
|
||||
|
||||
// Try to break all used utensils
|
||||
foreach (var utensil in usedUtensils)
|
||||
foreach (var utensil in args.Utensils)
|
||||
{
|
||||
_utensilSystem.TryBreak((utensil).Owner, user);
|
||||
_utensilSystem.TryBreak((utensil).Owner, args.User);
|
||||
}
|
||||
|
||||
if (food.UsesRemaining > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (args.Food.UsesRemaining > 0)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(food.TrashPrototype))
|
||||
EntityManager.QueueDeleteEntity(food.Owner);
|
||||
if (string.IsNullOrEmpty(args.Food.TrashPrototype))
|
||||
EntityManager.QueueDeleteEntity(args.Food.Owner);
|
||||
else
|
||||
DeleteAndSpawnTrash(food, user);
|
||||
|
||||
return true;
|
||||
DeleteAndSpawnTrash(args.Food, args.User);
|
||||
}
|
||||
|
||||
private void DeleteAndSpawnTrash(FoodComponent component, EntityUid? user = null)
|
||||
|
|
@ -229,7 +264,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
{
|
||||
Act = () =>
|
||||
{
|
||||
TryUseFood(uid, ev.User, component);
|
||||
TryFeed(uid, ev.User, component);
|
||||
},
|
||||
Text = Loc.GetString("food-system-verb-eat"),
|
||||
Priority = -1
|
||||
|
|
@ -238,131 +273,12 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
ev.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to force feed a target. Returns true if any interaction occurred, including pop-up generation
|
||||
/// </summary>
|
||||
public bool TryForceFeed(EntityUid uid, EntityUid user, EntityUid target, FoodComponent? food = null)
|
||||
{
|
||||
if (!Resolve(uid, ref food))
|
||||
return false;
|
||||
|
||||
// if currently being used to force-feed, cancel that action.
|
||||
if (food.CancelToken != null)
|
||||
{
|
||||
food.CancelToken.Cancel();
|
||||
food.CancelToken = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EntityManager.HasComponent<SharedBodyComponent>(target))
|
||||
return false;
|
||||
|
||||
if (!_solutionContainerSystem.TryGetSolution(uid, food.SolutionName, out var foodSolution))
|
||||
return false;
|
||||
|
||||
if (food.UsesRemaining <= 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-try-use-food-is-empty",
|
||||
("entity", uid)), user, Filter.Entities(user));
|
||||
DeleteAndSpawnTrash(food, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (IsMouthBlocked(target, user))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryGetRequiredUtensils(user, food, out var utensils))
|
||||
return true;
|
||||
|
||||
EntityManager.TryGetComponent(user, out MetaDataComponent? meta);
|
||||
var userName = meta?.EntityName ?? string.Empty;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed", ("user", userName)),
|
||||
user, Filter.Entities(target));
|
||||
|
||||
food.CancelToken = new();
|
||||
_doAfterSystem.DoAfter(new DoAfterEventArgs(user, food.ForceFeedDelay, food.CancelToken.Token, target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnTargetMove = true,
|
||||
MovementThreshold = 1.0f,
|
||||
TargetFinishedEvent = new ForceFeedEvent(user, food, foodSolution, utensils),
|
||||
BroadcastCancelledEvent = new ForceFeedCancelledEvent(food)
|
||||
});
|
||||
|
||||
// logging
|
||||
_logSystem.Add(LogType.ForceFeed, LogImpact.Medium, $"{ToPrettyString(user):user} is forcing {ToPrettyString(target):target} to eat {ToPrettyString(uid):food} {SolutionContainerSystem.ToPrettyString(foodSolution):solution}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnForceFeed(EntityUid uid, SharedBodyComponent body, ForceFeedEvent args)
|
||||
{
|
||||
if (args.Food.Deleted)
|
||||
return;
|
||||
|
||||
args.Food.CancelToken = null;
|
||||
|
||||
if (!_bodySystem.TryGetComponentsOnMechanisms<StomachComponent>(uid, out var stomachs, body))
|
||||
return;
|
||||
|
||||
var transferAmount = args.Food.TransferAmount != null
|
||||
? FixedPoint2.Min((FixedPoint2) args.Food.TransferAmount, args.FoodSolution.CurrentVolume)
|
||||
: args.FoodSolution.CurrentVolume;
|
||||
|
||||
var split = _solutionContainerSystem.SplitSolution((args.Food).Owner, args.FoodSolution, transferAmount);
|
||||
var firstStomach = stomachs.FirstOrNull(
|
||||
stomach => _stomachSystem.CanTransferSolution((stomach.Comp).Owner, split));
|
||||
|
||||
if (firstStomach == null)
|
||||
{
|
||||
_solutionContainerSystem.TryAddSolution(uid, args.FoodSolution, split);
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more-other"), uid, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
|
||||
split.DoEntityReaction(uid, ReactionMethod.Ingestion);
|
||||
_stomachSystem.TryTransferSolution((firstStomach.Value.Comp).Owner, split, firstStomach.Value.Comp);
|
||||
|
||||
EntityManager.TryGetComponent(uid, out MetaDataComponent? targetMeta);
|
||||
var targetName = targetMeta?.EntityName ?? string.Empty;
|
||||
|
||||
EntityManager.TryGetComponent(args.User, out MetaDataComponent? userMeta);
|
||||
var userName = userMeta?.EntityName ?? string.Empty;
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success", ("user", userName)),
|
||||
uid, Filter.Entities(uid));
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("food-system-force-feed-success-user", ("target", targetName)),
|
||||
args.User, Filter.Entities(args.User));
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(uid), args.Food.UseSound.GetSound(), uid, AudioParams.Default.WithVolume(-1f));
|
||||
|
||||
// Try to break all used utensils
|
||||
foreach (var utensil in args.Utensils)
|
||||
{
|
||||
_utensilSystem.TryBreak((utensil).Owner, args.User);
|
||||
}
|
||||
|
||||
if (args.Food.UsesRemaining > 0)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(args.Food.TrashPrototype))
|
||||
EntityManager.QueueDeleteEntity((args.Food).Owner);
|
||||
else
|
||||
DeleteAndSpawnTrash(args.Food, args.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Force feeds someone remotely. Does not require utensils (well, not the normal type anyways).
|
||||
/// </summary>
|
||||
public void ProjectileForceFeed(EntityUid uid, EntityUid target, EntityUid? user, FoodComponent? food = null, BodyComponent? body = null)
|
||||
{
|
||||
// TODO: Combine with regular feeding because holy code duplication batman.
|
||||
if (!Resolve(uid, ref food, false) || !Resolve(target, ref body, false))
|
||||
return;
|
||||
|
||||
|
|
@ -398,7 +314,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
SoundSystem.Play(Filter.Pvs(target), food.UseSound.GetSound(), target, AudioParams.Default.WithVolume(-1f));
|
||||
|
||||
if (string.IsNullOrEmpty(food.TrashPrototype))
|
||||
EntityManager.QueueDeleteEntity(((IComponent) food).Owner);
|
||||
EntityManager.QueueDeleteEntity(food.Owner);
|
||||
else
|
||||
DeleteAndSpawnTrash(food);
|
||||
}
|
||||
|
|
@ -406,7 +322,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
private bool TryGetRequiredUtensils(EntityUid user, FoodComponent component,
|
||||
out List<UtensilComponent> utensils, HandsComponent? hands = null)
|
||||
{
|
||||
utensils = new();
|
||||
utensils = new List<UtensilComponent>();
|
||||
|
||||
if (component.Utensil != UtensilType.None)
|
||||
return true;
|
||||
|
|
@ -441,7 +357,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
return true;
|
||||
}
|
||||
|
||||
private void OnForceFeedCancelled(ForceFeedCancelledEvent args)
|
||||
private static void OnFeedCancelled(ForceFeedCancelledEvent args)
|
||||
{
|
||||
args.Food.CancelToken = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
|||
if (!user.InRangeUnobstructed(target, popup: true))
|
||||
return false;
|
||||
|
||||
return _foodSystem.TryUseFood(target, user);
|
||||
return _foodSystem.TryFeed(user, target, food);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -17,16 +17,16 @@ public sealed class IngestionAttemptEvent : CancellableEntityEventArgs
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised directed at the food after a successful force-feed do-after.
|
||||
/// Raised directed at the food after a successful feed do-after.
|
||||
/// </summary>
|
||||
public sealed class ForceFeedEvent : EntityEventArgs
|
||||
public sealed class FeedEvent : EntityEventArgs
|
||||
{
|
||||
public readonly EntityUid User;
|
||||
public readonly FoodComponent Food;
|
||||
public readonly Solution FoodSolution;
|
||||
public readonly List<UtensilComponent> Utensils;
|
||||
|
||||
public ForceFeedEvent(EntityUid user, FoodComponent food, Solution foodSolution, List<UtensilComponent> utensils)
|
||||
public FeedEvent(EntityUid user, FoodComponent food, Solution foodSolution, List<UtensilComponent> utensils)
|
||||
{
|
||||
User = user;
|
||||
Food = food;
|
||||
|
|
@ -51,13 +51,13 @@ public sealed class ForceFeedCancelledEvent : EntityEventArgs
|
|||
/// <summary>
|
||||
/// Raised directed at the drink after a successful force-drink do-after.
|
||||
/// </summary>
|
||||
public sealed class ForceDrinkEvent : EntityEventArgs
|
||||
public sealed class DrinkEvent : EntityEventArgs
|
||||
{
|
||||
public readonly EntityUid User;
|
||||
public readonly DrinkComponent Drink;
|
||||
public readonly Solution DrinkSolution;
|
||||
|
||||
public ForceDrinkEvent(EntityUid user, DrinkComponent drink, Solution drinkSolution)
|
||||
public DrinkEvent(EntityUid user, DrinkComponent drink, Solution drinkSolution)
|
||||
{
|
||||
User = user;
|
||||
Drink = drink;
|
||||
|
|
@ -68,11 +68,11 @@ public sealed class ForceDrinkEvent : EntityEventArgs
|
|||
/// <summary>
|
||||
/// Raised directed at the food after a failed force-dink do-after.
|
||||
/// </summary>
|
||||
public sealed class ForceDrinkCancelledEvent : EntityEventArgs
|
||||
public sealed class DrinkCancelledEvent : EntityEventArgs
|
||||
{
|
||||
public readonly DrinkComponent Drink;
|
||||
|
||||
public ForceDrinkCancelledEvent(DrinkComponent drink)
|
||||
public DrinkCancelledEvent(DrinkComponent drink)
|
||||
{
|
||||
Drink = drink;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ namespace Content.Server.Physics.Controllers
|
|||
|
||||
worldPos += Convey(direction, speed, frameTime, itemRelative);
|
||||
transform.WorldPosition = worldPos;
|
||||
|
||||
if (TryComp<PhysicsComponent>(entity, out var body))
|
||||
body.Awake = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
|
|
@ -13,20 +13,18 @@ namespace Content.Server.Power.Nodes
|
|||
[DataDefinition]
|
||||
public class CableDeviceNode : Node
|
||||
{
|
||||
public override IEnumerable<Node> GetReachableNodes()
|
||||
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
if (!Anchored)
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
// If we're in an invalid grid, such as grid 0, we cannot connect to anything.
|
||||
if(!IoCManager.Resolve<IMapManager>().TryGetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID, out var grid))
|
||||
yield break;
|
||||
|
||||
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
|
||||
foreach (var node in NodeHelpers.GetNodesInTile(entMan, grid, gridIndex))
|
||||
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, gridIndex))
|
||||
{
|
||||
if (node is CableNode)
|
||||
yield return node;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
|
@ -12,21 +12,23 @@ namespace Content.Server.Power.Nodes
|
|||
[DataDefinition]
|
||||
public class CableNode : Node
|
||||
{
|
||||
public override IEnumerable<Node> GetReachableNodes()
|
||||
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
if (!Anchored)
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
|
||||
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
// While we go over adjacent nodes, we build a list of blocked directions due to
|
||||
// incoming or outgoing wire terminals.
|
||||
var terminalDirs = 0;
|
||||
List<(Direction, Node)> nodeDirs = new();
|
||||
|
||||
foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(entMan, grid, gridIndex))
|
||||
foreach (var (dir, node) in NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex))
|
||||
{
|
||||
if (node is CableNode && node != this)
|
||||
{
|
||||
|
|
@ -44,11 +46,11 @@ namespace Content.Server.Power.Nodes
|
|||
if (dir == Direction.Invalid)
|
||||
{
|
||||
// On own tile, block direction it faces
|
||||
terminalDirs |= 1 << (int) IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(node.Owner).LocalRotation.GetCardinalDir();
|
||||
terminalDirs |= 1 << (int) xformQuery.GetComponent(node.Owner).LocalRotation.GetCardinalDir();
|
||||
}
|
||||
else
|
||||
{
|
||||
var terminalDir = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(node.Owner).LocalRotation.GetCardinalDir();
|
||||
var terminalDir = xformQuery.GetComponent(node.Owner).LocalRotation.GetCardinalDir();
|
||||
if (terminalDir.GetOpposite() == dir)
|
||||
{
|
||||
// Target tile has a terminal towards us, block the direction.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Power.Nodes
|
||||
|
|
@ -10,29 +11,27 @@ namespace Content.Server.Power.Nodes
|
|||
[DataDefinition]
|
||||
public class CableTerminalNode : CableDeviceNode
|
||||
{
|
||||
public override IEnumerable<Node> GetReachableNodes()
|
||||
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
if (!Anchored)
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
if (IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID == GridId.Invalid)
|
||||
yield break; // No funny nodes in spess.
|
||||
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
var dir = xform.LocalRotation.GetDir();
|
||||
var targetIdx = gridIndex.Offset(dir);
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
|
||||
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
|
||||
var dir = IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).LocalRotation.GetDir();
|
||||
var targetIdx = gridIndex + NodeHelpers.TileOffsetForDir(dir);
|
||||
|
||||
foreach (var node in NodeHelpers.GetNodesInTile(entMan, grid, targetIdx))
|
||||
foreach (var node in NodeHelpers.GetNodesInTile(nodeQuery, grid, targetIdx))
|
||||
{
|
||||
if (node is CableTerminalPortNode)
|
||||
yield return node;
|
||||
}
|
||||
|
||||
foreach (var node in base.GetReachableNodes())
|
||||
foreach (var node in base.GetReachableNodes(xform, nodeQuery, xformQuery, grid, entMan))
|
||||
{
|
||||
yield return node;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Collections.Generic;
|
||||
using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.Nodes;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
|
|
@ -10,19 +10,18 @@ namespace Content.Server.Power.Nodes
|
|||
[DataDefinition]
|
||||
public class CableTerminalPortNode : Node
|
||||
{
|
||||
public override IEnumerable<Node> GetReachableNodes()
|
||||
public override IEnumerable<Node> GetReachableNodes(TransformComponent xform,
|
||||
EntityQuery<NodeContainerComponent> nodeQuery,
|
||||
EntityQuery<TransformComponent> xformQuery,
|
||||
IMapGrid? grid,
|
||||
IEntityManager entMan)
|
||||
{
|
||||
if (!Anchored)
|
||||
if (!xform.Anchored || grid == null)
|
||||
yield break;
|
||||
|
||||
if (IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID == GridId.Invalid)
|
||||
yield break; // No funny nodes in spess.
|
||||
var gridIndex = grid.TileIndicesFor(xform.Coordinates);
|
||||
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var grid = IoCManager.Resolve<IMapManager>().GetGrid(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).GridID);
|
||||
var gridIndex = grid.TileIndicesFor(IoCManager.Resolve<IEntityManager>().GetComponent<TransformComponent>(Owner).Coordinates);
|
||||
|
||||
var nodes = NodeHelpers.GetCardinalNeighborNodes(entMan, grid, gridIndex, includeSameTile: false);
|
||||
var nodes = NodeHelpers.GetCardinalNeighborNodes(nodeQuery, grid, gridIndex, includeSameTile: false);
|
||||
foreach (var (_, node) in nodes)
|
||||
{
|
||||
if (node is CableTerminalNode)
|
||||
|
|
|
|||
21
Content.Server/Remotes/DoorRemoteComponent.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Robust.Shared.Analyzers;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
namespace Content.Server.Remotes
|
||||
{
|
||||
[RegisterComponent]
|
||||
[Friend(typeof(DoorRemoteSystem))]
|
||||
public class DoorRemoteComponent : Component
|
||||
{
|
||||
public override string Name => "DoorRemote";
|
||||
|
||||
public OperatingMode Mode = OperatingMode.OpenClose;
|
||||
|
||||
public enum OperatingMode : byte
|
||||
{
|
||||
OpenClose,
|
||||
ToggleBolts
|
||||
// ToggleEmergencyAccess
|
||||
}
|
||||
}
|
||||
}
|
||||
93
Content.Server/Remotes/DoorRemoteSystem.cs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Audio;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Server.Doors.Systems;
|
||||
using Content.Server.Doors.Components;
|
||||
|
||||
namespace Content.Server.Remotes
|
||||
{
|
||||
public sealed class DoorRemoteSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedDoorSystem _sharedDoorSystem = default!;
|
||||
[Dependency] private readonly DoorSystem _doorSystem = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DoorRemoteComponent, UseInHandEvent>(OnInHandActivation);
|
||||
SubscribeLocalEvent<DoorRemoteComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
}
|
||||
|
||||
public void OnInHandActivation(EntityUid user, DoorRemoteComponent component, UseInHandEvent args)
|
||||
{
|
||||
switch (component.Mode)
|
||||
{
|
||||
case DoorRemoteComponent.OperatingMode.OpenClose:
|
||||
component.Mode = DoorRemoteComponent.OperatingMode.ToggleBolts;
|
||||
_popupSystem.PopupEntity(Loc.GetString("door-remote-switch-state-toggle-bolts"), args.User, Filter.Entities(args.User));
|
||||
break;
|
||||
case DoorRemoteComponent.OperatingMode.ToggleBolts:
|
||||
component.Mode = DoorRemoteComponent.OperatingMode.OpenClose; // TODO: Swítch to ToggleEmergencyAcces when EA is implemented
|
||||
_popupSystem.PopupEntity(Loc.GetString("door-remote-switch-state-open-close"), args.User, Filter.Entities(args.User)); // TODO: See the above comment
|
||||
break;
|
||||
/*
|
||||
case DoorRemoteComponent.OperatingMode.ToggleEmergencyAccess:
|
||||
component.Mode = DoorRemoteComponent.OperatingMode.OpenClose;
|
||||
_popupSystem.PopupEntity(Loc.GetString("door-remote-switch-state-open-close"), args.User, Filter.Entities(args.User));
|
||||
break;
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, DoorRemoteComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled
|
||||
|| args.Target == null
|
||||
|| !TryComp<DoorComponent>(args.Target, out var doorComponent) // If it isn't a door we don't use it
|
||||
|| !HasComp<AccessReaderComponent>(args.Target) // Remotes do not work on doors without access requirements
|
||||
|| !TryComp<AirlockComponent>(args.Target, out var airlockComponent) // Remotes only work on airlocks
|
||||
|| !_interactionSystem.InRangeUnobstructed(args.User, doorComponent.Owner, -1f, CollisionGroup.Opaque))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (component.Mode == DoorRemoteComponent.OperatingMode.OpenClose)
|
||||
{
|
||||
_sharedDoorSystem.TryToggleDoor(doorComponent.Owner, user: args.Used);
|
||||
}
|
||||
|
||||
if (component.Mode == DoorRemoteComponent.OperatingMode.ToggleBolts
|
||||
&& airlockComponent.IsPowered())
|
||||
{
|
||||
if (_doorSystem.HasAccess(doorComponent.Owner, args.Used))
|
||||
{
|
||||
airlockComponent.SetBoltsWithAudio(!airlockComponent.IsBolted());
|
||||
}
|
||||
else
|
||||
{
|
||||
if (doorComponent.State != DoorState.Open)
|
||||
{
|
||||
_sharedDoorSystem.Deny(airlockComponent.Owner, user: args.User);
|
||||
}
|
||||
else if (doorComponent.DenySound != null)
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(args.Target.Value), doorComponent.DenySound.GetSound(), args.Target.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
|
|
@ -10,20 +9,20 @@ namespace Content.Shared.Chemistry.Components
|
|||
/// <summary>
|
||||
/// Shared class for injectors & syringes
|
||||
/// </summary>
|
||||
[NetworkedComponent()]
|
||||
public class SharedInjectorComponent : Component
|
||||
[NetworkedComponent, ComponentProtoName("Injector")]
|
||||
public abstract class SharedInjectorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Component data used for net updates. Used by client for item status ui
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
protected sealed class InjectorComponentState : ComponentState
|
||||
public sealed class InjectorComponentState : ComponentState
|
||||
{
|
||||
public FixedPoint2 CurrentVolume { get; }
|
||||
public FixedPoint2 TotalVolume { get; }
|
||||
public InjectorToggleMode CurrentMode { get; }
|
||||
|
||||
public InjectorComponentState(FixedPoint2 currentVolume, FixedPoint2 totalVolume, InjectorToggleMode currentMode)
|
||||
public InjectorComponentState(FixedPoint2 currentVolume, FixedPoint2 totalVolume, SharedInjectorComponent.InjectorToggleMode currentMode)
|
||||
{
|
||||
CurrentVolume = currentVolume;
|
||||
TotalVolume = totalVolume;
|
||||
|
|
@ -31,7 +30,7 @@ namespace Content.Shared.Chemistry.Components
|
|||
}
|
||||
}
|
||||
|
||||
public enum InjectorToggleMode
|
||||
public enum InjectorToggleMode : byte
|
||||
{
|
||||
Inject,
|
||||
Draw
|
||||
|
|
|
|||
|
|
@ -1,25 +1,21 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Shared.Crayon
|
||||
{
|
||||
[NetworkedComponent()]
|
||||
public class SharedCrayonComponent : Component
|
||||
[NetworkedComponent, ComponentProtoName("Crayon")]
|
||||
public abstract class SharedCrayonComponent : Component
|
||||
{
|
||||
public string SelectedState { get; set; } = string.Empty;
|
||||
|
||||
[DataField("color")]
|
||||
protected string _color = "white";
|
||||
[DataField("color")] public string _color = "white";
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum CrayonUiKey
|
||||
public enum CrayonUiKey : byte
|
||||
{
|
||||
Key,
|
||||
}
|
||||
|
|
|
|||
15
Content.Shared/GlobalUsings.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// There isn't really a 'default place' to put these,
|
||||
// so a file in the project top level directory it is
|
||||
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using Robust.Shared.Analyzers;
|
||||
global using Robust.Shared.Log;
|
||||
global using Robust.Shared.Localization;
|
||||
global using Robust.Shared.GameObjects;
|
||||
global using Robust.Shared.IoC;
|
||||
global using Robust.Shared.Maths;
|
||||
global using Robust.Shared.ViewVariables;
|
||||
global using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
|
||||
|
|
@ -30,6 +30,7 @@ namespace Content.Shared.Pulling
|
|||
public class SharedPullingStateManagementSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedJointSystem _jointSystem = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -116,7 +117,7 @@ namespace Content.Shared.Pulling
|
|||
pullable.Puller = puller.Owner;
|
||||
|
||||
// Joint startup
|
||||
var union = pullerPhysics.GetWorldAABB().Union(pullablePhysics.GetWorldAABB());
|
||||
var union = _physics.GetHardAABB(pullerPhysics).Union(_physics.GetHardAABB(pullablePhysics));
|
||||
var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f;
|
||||
|
||||
pullable.PullJoint = _jointSystem.CreateDistanceJoint(pullablePhysics.Owner, pullerPhysics.Owner, id:$"pull-joint-{pullablePhysics.Owner}");
|
||||
|
|
|
|||
|
|
@ -1,18 +1,29 @@
|
|||
using System;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Trigger
|
||||
{
|
||||
[NetSerializable]
|
||||
[Serializable]
|
||||
public enum TriggerVisuals
|
||||
[Serializable, NetSerializable]
|
||||
public enum ProximityTriggerVisuals : byte
|
||||
{
|
||||
Off,
|
||||
Inactive,
|
||||
Active,
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum ProximityTriggerVisualState : byte
|
||||
{
|
||||
State,
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum TriggerVisuals : byte
|
||||
{
|
||||
VisualState,
|
||||
}
|
||||
|
||||
[NetSerializable]
|
||||
[Serializable]
|
||||
public enum TriggerVisualState
|
||||
[Serializable, NetSerializable]
|
||||
public enum TriggerVisualState : byte
|
||||
{
|
||||
Primed,
|
||||
Unprimed,
|
||||
|
|
|
|||
|
|
@ -1,64 +1,4 @@
|
|||
Entries:
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- {message: Station will now start with a random rotation., type: Tweak}
|
||||
id: 483
|
||||
time: '2021-10-25T04:22:57.0000000+00:00'
|
||||
- author: mirrorcult
|
||||
changes:
|
||||
- {message: Walking over slippery items (usually) won't slip you anymore., type: Tweak}
|
||||
- {message: Turned-on magboots now stop you from slipping., type: Add}
|
||||
id: 484
|
||||
time: '2021-10-25T06:43:49.0000000+00:00'
|
||||
- author: 20kdc
|
||||
changes:
|
||||
- {message: Construction works on rotated grids, type: Fix}
|
||||
id: 485
|
||||
time: '2021-10-25T14:14:21.0000000+00:00'
|
||||
- author: Zumorica
|
||||
changes:
|
||||
- {message: Added electrocution., type: Add}
|
||||
- {message: Cutting cables without insulated gloves shocks you., type: Tweak}
|
||||
- {message: Grilles can now be electrified by placing a cable below them., type: Tweak}
|
||||
id: 486
|
||||
time: '2021-10-25T14:21:56.0000000+00:00'
|
||||
- author: ike709
|
||||
changes:
|
||||
- {message: Stacks can now be split in half by alt-clicking or into different amounts
|
||||
with the new Split verb., type: Add}
|
||||
id: 487
|
||||
time: '2021-10-25T14:36:04.0000000+00:00'
|
||||
- author: Zumorica
|
||||
changes:
|
||||
- {message: Added insulated gloves (and their cheap knockoffs) to the station.,
|
||||
type: Add}
|
||||
id: 488
|
||||
time: '2021-10-25T14:58:04.292881+00:00'
|
||||
- author: 20kdc
|
||||
changes:
|
||||
- {message: ChemMaster bottles are no longer 'empty', type: Fix}
|
||||
id: 489
|
||||
time: '2021-10-25T14:58:16.0000000+00:00'
|
||||
- author: 20kdc
|
||||
changes:
|
||||
- {message: 'Wall lights can be placed again, APCs place where they should', type: Fix}
|
||||
id: 490
|
||||
time: '2021-10-26T14:15:02.0000000+00:00'
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- {message: Construction recipes are now sorted alphabetically., type: Tweak}
|
||||
id: 491
|
||||
time: '2021-10-26T14:18:43.0000000+00:00'
|
||||
- author: Zumorica
|
||||
changes:
|
||||
- {message: Fixed not being able to construct or deconstruct tables., type: Fix}
|
||||
id: 492
|
||||
time: '2021-10-26T14:38:03.0000000+00:00'
|
||||
- author: Zumorica
|
||||
changes:
|
||||
- {message: Fix computer dupe bug., type: Fix}
|
||||
id: 493
|
||||
time: '2021-10-26T23:30:17.397968+00:00'
|
||||
- author: 20kdc
|
||||
changes:
|
||||
- {message: Morgue trays now extend to the proper position., type: Fix}
|
||||
|
|
@ -2934,7 +2874,7 @@ Entries:
|
|||
time: '2022-02-04T23:17:11.0000000+00:00'
|
||||
- author: Lamrr
|
||||
changes:
|
||||
- {message: 'The digital audio workstaiton [sic] has been relabelled.', type: Fix}
|
||||
- {message: The digital audio workstaiton (sic) has been relabelled., type: Fix}
|
||||
id: 981
|
||||
time: '2022-02-05T01:23:43.0000000+00:00'
|
||||
- author: Willhelm53
|
||||
|
|
@ -2953,3 +2893,65 @@ Entries:
|
|||
of just discarding your wrench.', type: Tweak}
|
||||
id: 983
|
||||
time: '2022-02-05T02:39:02.0000000+00:00'
|
||||
- author: walrusfromspace
|
||||
changes:
|
||||
- {message: Added door remotes to the heads' lockers, type: Add}
|
||||
id: 984
|
||||
time: '2022-02-05T13:33:24.0000000+00:00'
|
||||
- author: ElectroJr
|
||||
changes:
|
||||
- {message: Windoors should no longer crush people as they auto close., type: Fix}
|
||||
id: 985
|
||||
time: '2022-02-05T17:27:06.0000000+00:00'
|
||||
- author: Peptide90
|
||||
changes:
|
||||
- {message: Blast doors can't be opened with a crowbar anymore., type: Tweak}
|
||||
id: 986
|
||||
time: '2022-02-05T18:06:33.0000000+00:00'
|
||||
- author: router
|
||||
changes:
|
||||
- {message: Replaced all ingot mints to fix a typo in their stamping. All "Nanotransen"
|
||||
gold and silver bars have been melted down and reformed., type: Fix}
|
||||
- {message: Removed Herobrine., type: Remove}
|
||||
id: 987
|
||||
time: '2022-02-05T19:23:06.0000000+00:00'
|
||||
- author: Peptide90
|
||||
changes:
|
||||
- {message: Massive list of changes and fixes for NSS Pillar. See Github for list.,
|
||||
type: Fix}
|
||||
id: 988
|
||||
time: '2022-02-05T21:27:43.0000000+00:00'
|
||||
- author: Rane
|
||||
changes:
|
||||
- {message: Brig access is now the least privileged form of security access and
|
||||
given to lawyers. It may take a while for stations to update their airlock software
|
||||
to the latest version., type: Tweak}
|
||||
id: 989
|
||||
time: '2022-02-05T23:08:15.0000000+00:00'
|
||||
- author: moony
|
||||
changes:
|
||||
- {message: Hopefully fix pillar spawning., type: Fix}
|
||||
id: 990
|
||||
time: '2022-02-06T02:22:28.0000000+00:00'
|
||||
- author: mirrorcult
|
||||
changes:
|
||||
- {message: Directional windows no longer look weird when viewed while rotated.,
|
||||
type: Tweak}
|
||||
id: 991
|
||||
time: '2022-02-06T02:25:13.0000000+00:00'
|
||||
- author: Peptide90
|
||||
changes:
|
||||
- {message: 'NSS Pillar minor update! Check out changes to security, permabrig and
|
||||
salvage among other things.', type: Tweak}
|
||||
id: 992
|
||||
time: '2022-02-06T12:56:41.0000000+00:00'
|
||||
- author: lajolico
|
||||
changes:
|
||||
- {message: Lasers can now pass through glass airlocks, type: Add}
|
||||
id: 993
|
||||
time: '2022-02-06T13:48:31.0000000+00:00'
|
||||
- author: scaly-chimp
|
||||
changes:
|
||||
- {message: adds a portable flasher for security to lock down areas, type: Add}
|
||||
id: 994
|
||||
time: '2022-02-06T15:59:42.0000000+00:00'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
## Entity
|
||||
|
||||
plant-holder-component-empty-seed-packet-message = The packet seems to be empty. You throw it away.
|
||||
plant-holder-component-plant-success-message = You plant the {$seedName} {$seedNoun}.
|
||||
plant-holder-component-already-seeded-message = The {$name} already has seeds in it!
|
||||
plant-holder-component-remove-weeds-message = You remove the weeds from the {$name}.
|
||||
|
|
@ -32,4 +31,4 @@ plant-holder-component-toxins-high-warning = The [color=red]toxicity level alert
|
|||
plant-holder-component-light-improper-warning = The [color=yellow]improper light level alert[/color] is blinking.
|
||||
plant-holder-component-heat-improper-warning = The [color=orange]improper temperature level alert[/color] is blinking.
|
||||
plant-holder-component-pressure-improper-warning = The [color=lightblue]improper environment pressure alert[/color] is blinking.
|
||||
plant-holder-component-gas-missing-warning = The [color=cyan]improper gas environment alert[/color] is blinking.
|
||||
plant-holder-component-gas-missing-warning = The [color=cyan]improper gas environment alert[/color] is blinking.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
## Entity
|
||||
|
||||
seed-component-no-seeds-message = It doesn't seem to contain any seeds.
|
||||
seed-component-description = It has a picture of [color=yellow]{$seedName}[/color] on the front.
|
||||
seed-component-has-variety-tag = It's tagged as variety [color=lightgray]no. {$seedUid}[/color].
|
||||
seed-component-plant-yield-text = Plant Yield: [color=lightblue]{$seedYield}[/color]
|
||||
|
|
@ -9,4 +8,4 @@ seed-component-plant-potency-text = Plant Potency: [color=lightblue]{$seedPotenc
|
|||
botany-seed-packet-name = packet of {$seedName} {$seedNoun}
|
||||
botany-harvest-fail-message = You fail to harvest anything useful.
|
||||
botany-harvest-success-message = You harvest from the {$name}
|
||||
botany-mysterious-description-addon = On second thought, something about this one looks strange.
|
||||
botany-mysterious-description-addon = On second thought, something about this one looks strange.
|
||||
|
|
|
|||
3
Resources/Locale/en-US/door-remote/door-remote.ftl
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
door-remote-switch-state-open-close = You switch the remote to open and close doors
|
||||
door-remote-switch-state-toggle-bolts = You switch the remote to toggle bolts
|
||||
door-remote-switch-state-toggle-emergency-access = You switch the remote to toggle emergency access
|
||||
|
|
@ -25,6 +25,7 @@
|
|||
prob: 1
|
||||
- id: CigPackGreen
|
||||
prob: 0.50
|
||||
- id: DoorRemoteCargo
|
||||
|
||||
- type: entity
|
||||
id: LockerCaptainFilled
|
||||
|
|
@ -63,6 +64,7 @@
|
|||
- id: CigarGoldCase
|
||||
prob: 0.25
|
||||
- id: ClothingBeltSheathFilled
|
||||
- id: DoorRemoteCommand
|
||||
|
||||
- type: entity
|
||||
id: LockerHeadOfPersonnelFilled
|
||||
|
|
@ -94,6 +96,7 @@
|
|||
- id: CigarGoldCase
|
||||
prob: 0.10
|
||||
# Fuck the HoP they don't deserve fucking cigars.
|
||||
- id: DoorRemoteService
|
||||
|
||||
- type: entity
|
||||
id: LockerChiefEngineerFilled
|
||||
|
|
@ -114,6 +117,7 @@
|
|||
- id: ClothingHandsGlovesColorYellow
|
||||
- id: CigarCase
|
||||
prob: 0.15
|
||||
- id: DoorRemoteEngineering
|
||||
|
||||
- type: entity
|
||||
id: LockerChiefMedicalOfficerFilled
|
||||
|
|
@ -135,6 +139,7 @@
|
|||
- id: ClothingOuterHardsuitMedical
|
||||
- id: Hypospray
|
||||
- id: HandheldCrewMonitor
|
||||
- id: DoorRemoteMedical
|
||||
|
||||
- type: entity
|
||||
id: LockerResearchDirectorFilled
|
||||
|
|
@ -157,6 +162,7 @@
|
|||
prob: 1
|
||||
- id: PlushieSlime
|
||||
prob: 0.1
|
||||
- id: DoorRemoteResearch
|
||||
|
||||
- type: entity
|
||||
id: LockerHeadOfSecurityFilled
|
||||
|
|
@ -205,3 +211,4 @@
|
|||
prob: 1
|
||||
- id: CigarGoldCase
|
||||
prob: 0.50
|
||||
- id: DoorRemoteSecurity
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
path: /Audio/Effects/Fluids/splat.ogg
|
||||
recolor: true
|
||||
- type: Clickable
|
||||
- type: Evaporation
|
||||
- type: Slippery
|
||||
- type: Physics
|
||||
- type: Fixtures
|
||||
|
|
@ -67,7 +68,6 @@
|
|||
state: smear-0
|
||||
netsync: false
|
||||
- type: Puddle
|
||||
- type: Evaporation
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: PuddleVisualizer
|
||||
|
|
@ -84,7 +84,6 @@
|
|||
state: splatter-0
|
||||
netsync: false
|
||||
- type: Puddle
|
||||
- type: Evaporation
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: PuddleVisualizer
|
||||
|
|
@ -146,7 +145,6 @@
|
|||
state: writing-0
|
||||
netsync: false
|
||||
- type: Puddle
|
||||
- type: Evaporation
|
||||
evaporateTime: 10
|
||||
- type: Appearance
|
||||
visuals:
|
||||
|
|
|
|||
|
|
@ -7,9 +7,6 @@
|
|||
abstract: true
|
||||
components:
|
||||
- type: Food
|
||||
- type: ItemCooldown
|
||||
- type: UseDelay
|
||||
delay: 1.5
|
||||
|
||||
# This base type is used to cover all of the "obvious" things that should be doable to open-package food.
|
||||
# Practically this means injection.
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@
|
|||
interfaces:
|
||||
- key: enum.TransferAmountUiKey.Key
|
||||
type: TransferAmountBoundUserInterface
|
||||
- type: ItemCooldown
|
||||
- type: UseDelay
|
||||
delay: 1.0
|
||||
|
||||
- type: entity
|
||||
parent: DrinkBase
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@
|
|||
tags:
|
||||
- Trash
|
||||
- type: ItemCooldown
|
||||
- type: UseDelay
|
||||
delay: 1.0
|
||||
|
||||
- type: entity
|
||||
parent: DrinkCanBaseFull
|
||||
|
|
|
|||
|
|
@ -29,8 +29,6 @@
|
|||
- type: Spillable
|
||||
solution: drink
|
||||
- type: ItemCooldown
|
||||
- type: UseDelay
|
||||
delay: 1.0
|
||||
|
||||
- type: entity
|
||||
parent: DrinkBaseCup
|
||||
|
|
|
|||
137
Resources/Prototypes/Entities/Objects/Devices/door_remote.yml
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
- type: entity
|
||||
parent: BaseItem
|
||||
id: DoorRemoteDefault
|
||||
name: door remote
|
||||
description: A gadget which can open and bolt doors remotely
|
||||
abstract: true
|
||||
components:
|
||||
- type: Sprite
|
||||
sprite: Objects/Devices/door_remote.rsi
|
||||
netsync: false
|
||||
- type: Access
|
||||
- type: DoorRemote
|
||||
|
||||
- type: entity
|
||||
parent: DoorRemoteDefault
|
||||
id: DoorRemoteCommand
|
||||
name: command door remote
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: door_remotebase
|
||||
- state: door_remotelightscolour
|
||||
color: "#e6e600"
|
||||
- state: door_remotescreencolour
|
||||
color: "#9f9f00"
|
||||
- type: Access
|
||||
tags:
|
||||
- Captain
|
||||
- HeadOfPersonnel
|
||||
- Command
|
||||
|
||||
- type: entity
|
||||
parent: DoorRemoteDefault
|
||||
id: DoorRemoteSecurity
|
||||
name: security door remote
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: door_remotebase
|
||||
- state: door_remotelightscolour
|
||||
color: "#cb0000"
|
||||
- state: door_remotescreencolour
|
||||
color: "#830000"
|
||||
- type: Access
|
||||
tags:
|
||||
- HeadOfSecurity
|
||||
- Security
|
||||
|
||||
- type: entity
|
||||
parent: DoorRemoteDefault
|
||||
id: DoorRemoteService
|
||||
name: service door remote
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: door_remotebase
|
||||
- state: door_remotelightscolour
|
||||
color: "#58c800"
|
||||
- state: door_remotescreencolour
|
||||
color: "#3a7231"
|
||||
- type: Access
|
||||
tags:
|
||||
- HeadOfPersonnel
|
||||
- Service
|
||||
- Bar
|
||||
- Janitor
|
||||
- Theatre
|
||||
- Kitchen
|
||||
- Hydroponics
|
||||
|
||||
- type: entity
|
||||
parent: DoorRemoteDefault
|
||||
id: DoorRemoteResearch
|
||||
name: research door remote
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: door_remotebase
|
||||
- state: door_remotelightscolour
|
||||
color: "#a53aaa"
|
||||
- state: door_remotescreencolour
|
||||
color: "#652368"
|
||||
- type: Access
|
||||
tags:
|
||||
- ResearchDirector
|
||||
- Research
|
||||
|
||||
- type: entity
|
||||
parent: DoorRemoteDefault
|
||||
id: DoorRemoteCargo
|
||||
name: cargo door remote
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: door_remotebase
|
||||
- state: door_remotelightscolour
|
||||
color: "#b18644"
|
||||
- state: door_remotescreencolour
|
||||
color: "#5b4523"
|
||||
- type: Access
|
||||
tags:
|
||||
- Cargo
|
||||
- Quartermaster
|
||||
|
||||
- type: entity
|
||||
parent: DoorRemoteDefault
|
||||
id: DoorRemoteMedical
|
||||
name: medical door remote
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: door_remotebase
|
||||
- state: door_remotelightscolour
|
||||
color: "#68aed6"
|
||||
- state: door_remotescreencolour
|
||||
color: "#325f7a"
|
||||
- type: Access
|
||||
tags:
|
||||
- Medical
|
||||
- ChiefMedicalOfficer
|
||||
|
||||
- type: entity
|
||||
parent: DoorRemoteDefault
|
||||
id: DoorRemoteEngineering
|
||||
name: engineering door remote
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: door_remotebase
|
||||
- state: door_remotelightscolour
|
||||
color: "#ffa62b"
|
||||
- state: door_remotescreencolour
|
||||
color: "#bc5b00"
|
||||
- type: Access
|
||||
tags:
|
||||
- ChiefEngineer
|
||||
- Engineering
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
abstract: true
|
||||
parent: BaseItem
|
||||
id: IngotBase
|
||||
description: A heavy metal ingot stamped with the Nanotransen logo.
|
||||
description: A heavy metal ingot stamped with the Nanotrasen logo.
|
||||
components:
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
|
|
|
|||
|
|
@ -49,3 +49,53 @@
|
|||
size: 2
|
||||
sprite: Objects/Weapons/Melee/flash.rsi
|
||||
- type: ItemCooldown
|
||||
|
||||
- type: entity
|
||||
name: portable flasher
|
||||
parent: BaseStructure
|
||||
id: Portableflasher
|
||||
description: An ultrabright flashbulb with a proximity trigger, useful for making an area security-only.
|
||||
components:
|
||||
- type: SoundOnTrigger
|
||||
sound:
|
||||
path: /Audio/Weapons/flash.ogg
|
||||
- type: FlashOnTrigger
|
||||
repeating: true
|
||||
range: 3
|
||||
- type: TriggerOnProximity
|
||||
enabled: true
|
||||
cooldown: 5
|
||||
shape:
|
||||
!type:PhysShapeCircle
|
||||
radius: 2
|
||||
- type: Anchorable
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
sprite: Objects/Weapons/pflash.rsi
|
||||
layers:
|
||||
- state: "off"
|
||||
map: ["enum.ProximityTriggerVisualLayers.Base"]
|
||||
- type: InteractionOutline
|
||||
- type: Physics
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
- shape:
|
||||
!type:PhysShapeAabb
|
||||
bounds: "-0.15,-0.3,0.15,0.3"
|
||||
mask:
|
||||
- Impassable
|
||||
- VaultImpassable
|
||||
- SmallImpassable
|
||||
layer:
|
||||
- MobImpassable
|
||||
mass: 70
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: ProximityTriggerVisualizer
|
||||
animationState: flashing
|
||||
- type: PointLight
|
||||
energy: 2.0
|
||||
radius: 0
|
||||
softness: 0
|
||||
enabled: true
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
components:
|
||||
- type: Sprite
|
||||
sprite: Structures/Doors/Airlocks/Standard/medical.rsi
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: Airlock
|
||||
id: AirlockChemistry
|
||||
|
|
@ -86,6 +86,21 @@
|
|||
enabled: false
|
||||
- type: Sprite
|
||||
sprite: Structures/Doors/Airlocks/Glass/glass.rsi
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
- shape:
|
||||
!type:PhysShapeAabb
|
||||
bounds: "-0.49,-0.49,0.49,0.49" # don't want this colliding with walls or they won't close
|
||||
mass: 100
|
||||
mask:
|
||||
- Impassable
|
||||
layer: #removed opaque from the layer, allowing lasers to pass through glass airlocks
|
||||
- Impassable
|
||||
- MobImpassable
|
||||
- VaultImpassable
|
||||
- SmallImpassable
|
||||
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: AirlockGlass
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
bounds: "-0.49,-0.49,0.49,0.49" # don't want this colliding with walls or they won't close
|
||||
mass: 100
|
||||
mask:
|
||||
- MobImpassable
|
||||
- Impassable
|
||||
layer:
|
||||
- Opaque
|
||||
- Impassable
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
closeTimeTwo: 0.4
|
||||
openTimeOne: 0.4
|
||||
openTimeTwo: 0.4
|
||||
pryTime: -1
|
||||
crushDamage:
|
||||
types:
|
||||
Blunt: 25 # yowch
|
||||
|
|
|
|||
|
|
@ -71,10 +71,8 @@
|
|||
# needed so that windoors will close regardless of whether there are people in it; it doesn't crush after all
|
||||
safety: false
|
||||
- type: Door
|
||||
canCrush: false
|
||||
weldable: false
|
||||
crushDamage:
|
||||
types:
|
||||
Blunt: 10
|
||||
openSound:
|
||||
path: /Audio/Machines/windoor_open.ogg
|
||||
closeSound:
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
components:
|
||||
- type: AccessReader
|
||||
access: [["Medical"]]
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: WindoorSecure
|
||||
id: WindoorChemistryLocked
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
access: [["Security"]]
|
||||
- type: Wires
|
||||
LayoutId: WindoorSecurity
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: WindoorSecurityLocked
|
||||
id: WindoorBrigLocked
|
||||
|
|
@ -86,7 +86,15 @@
|
|||
components:
|
||||
- type: AccessReader
|
||||
access: [["Brig"]]
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: WindoorSecurityLocked
|
||||
id: WindoorArmoryLocked
|
||||
suffix: Armory, Locked
|
||||
components:
|
||||
- type: AccessReader
|
||||
access: [["Armory"]]
|
||||
|
||||
- type: entity
|
||||
parent: WindoorSecure
|
||||
id: WindoorEngineeringLocked
|
||||
|
|
@ -94,7 +102,7 @@
|
|||
components:
|
||||
- type: AccessReader
|
||||
access: [["Engineering"]]
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: WindoorSecure
|
||||
id: WindoorChapelLocked
|
||||
|
|
@ -102,7 +110,7 @@
|
|||
components:
|
||||
- type: AccessReader
|
||||
access: [["Chapel"]]
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: WindoorSecure
|
||||
id: WindoorJanitorLocked
|
||||
|
|
@ -110,11 +118,11 @@
|
|||
components:
|
||||
- type: AccessReader
|
||||
access: [["Janitor"]]
|
||||
|
||||
|
||||
- type: entity
|
||||
parent: WindoorSecure
|
||||
id: WindoorKitchenLocked
|
||||
suffix: Kitchen, Locked
|
||||
components:
|
||||
- type: AccessReader
|
||||
access: [["Kitchen"]]
|
||||
access: [["Kitchen"]]
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
state_open: qm_open
|
||||
state_closed: qm_door
|
||||
- type: AccessReader
|
||||
access: [["Quartermaster"]]
|
||||
access: [["Quartermaster"]]
|
||||
|
||||
# Command
|
||||
- type: entity
|
||||
|
|
@ -286,7 +286,7 @@
|
|||
state_open: warden_open
|
||||
state_closed: warden_door
|
||||
- type: AccessReader
|
||||
access: [["Brig"]]
|
||||
access: [["Armory"]]
|
||||
|
||||
# Security Officer
|
||||
- type: entity
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
prefixCreator: 'VG'
|
||||
mapPath: /Maps/packedstation.yml
|
||||
minPlayers: 15
|
||||
maxPlayers: 55
|
||||
conditions:
|
||||
- !type:HolidayMapCondition
|
||||
inverted: true
|
||||
|
|
@ -103,30 +104,29 @@
|
|||
!type:NanotrasenNameGenerator
|
||||
prefixCreator: '14'
|
||||
mapPath: /Maps/nss_pillar.yml
|
||||
minPlayers: 20
|
||||
votable: false
|
||||
minPlayers: 40
|
||||
overflowJobs:
|
||||
- Assistant
|
||||
availableJobs:
|
||||
Assistant: [ -1, -1 ]
|
||||
CargoTechnician: [ 4, 5 ]
|
||||
Bartender: [ 1, 1 ]
|
||||
Botanist: [ 2, 2 ]
|
||||
Chef: [ 2, 2 ]
|
||||
Clown: [ 1, 1 ]
|
||||
Janitor: [ 2, 3 ]
|
||||
Mime: [ 1, 1 ]
|
||||
CargoTechnician: [ 4, 6 ]
|
||||
Bartender: [ 2, 2 ]
|
||||
Botanist: [ 2, 4 ]
|
||||
Chef: [ 2, 4 ]
|
||||
Clown: [ 1, 2 ]
|
||||
Janitor: [ 2, 4 ]
|
||||
Mime: [ 1, 2 ]
|
||||
Captain: [ 1, 1 ]
|
||||
HeadOfPersonnel: [ 1, 1 ]
|
||||
ChiefEngineer: [ 1, 1 ]
|
||||
StationEngineer: [ 4, 6 ]
|
||||
StationEngineer: [ 6, 10 ]
|
||||
ChiefMedicalOfficer: [ 1, 1 ]
|
||||
MedicalDoctor: [ 4, 6 ]
|
||||
Chemist: [ 2, 2 ]
|
||||
MedicalDoctor: [ 6, 10 ]
|
||||
Chemist: [ 2, 3 ]
|
||||
ResearchDirector: [ 1, 1 ]
|
||||
Scientist: [ 3, 5 ]
|
||||
Scientist: [ 3, 8 ]
|
||||
HeadOfSecurity: [ 1, 1 ]
|
||||
SecurityOfficer: [ 5, 6 ]
|
||||
SecurityOfficer: [ 6, 10 ]
|
||||
Chaplain: [ 1, 1 ]
|
||||
Librarian: [ 1, 1 ]
|
||||
Lawyer: [ 1, 2 ]
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
supervisors: "the head of personnel"
|
||||
access:
|
||||
- Service
|
||||
- Brig
|
||||
- Maintenance
|
||||
|
||||
- type: startingGear
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
canBeAntag: false
|
||||
access:
|
||||
- Security
|
||||
- Brig
|
||||
- Maintenance
|
||||
- Service
|
||||
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
20
Resources/Textures/Objects/Devices/door_remote.rsi/meta.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Command remote copied from tgstation, rest are recolors",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "door_remotebase"
|
||||
},
|
||||
{
|
||||
"name": "door_remotelightscolour"
|
||||
},
|
||||
{
|
||||
"name": "door_remotescreencolour"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Resources/Textures/Objects/Weapons/pflash.rsi/flashing.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
30
Resources/Textures/Objects/Weapons/pflash.rsi/meta.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/f349b842c84f500399bd5673e5e34a6bc45b001a",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "off"
|
||||
},
|
||||
{
|
||||
"name": "on"
|
||||
},
|
||||
{
|
||||
"name": "on-unlit"
|
||||
},
|
||||
{
|
||||
"name": "flashing",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
Resources/Textures/Objects/Weapons/pflash.rsi/off.png
Normal file
|
After Width: | Height: | Size: 489 B |
BIN
Resources/Textures/Objects/Weapons/pflash.rsi/on-unlit.png
Normal file
|
After Width: | Height: | Size: 113 B |
BIN
Resources/Textures/Objects/Weapons/pflash.rsi/on.png
Normal file
|
After Width: | Height: | Size: 503 B |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 724 B |
|
|
@ -9,27 +9,27 @@
|
|||
"states": [
|
||||
{
|
||||
"name": "window",
|
||||
"directions": 8
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "reinforced_window",
|
||||
"directions": 8
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "tinted_window",
|
||||
"directions": 8
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "frosted_window",
|
||||
"directions": 8
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "plasma_window",
|
||||
"directions": 8
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "plasma_reinforced_window",
|
||||
"directions": 8
|
||||
"directions": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 781 B |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 635 B |