mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-11-05 18:38:43 +03:00
Merge remote-tracking branch 'upstream/master' into upstream-sync
# Conflicts: # Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml
This commit is contained in:
commit
336be19a71
237 changed files with 119229 additions and 95575 deletions
|
|
@ -5,16 +5,5 @@ namespace Content.Client.Clothing
|
|||
{
|
||||
public sealed class MagbootsSystem : SharedMagbootsSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<MagbootsComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
|
||||
}
|
||||
|
||||
private void OnRefreshMovespeed(EntityUid uid, MagbootsComponent component, RefreshMovementSpeedModifiersEvent args)
|
||||
{
|
||||
args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />
|
||||
<PropertyGroup>
|
||||
<!-- Work around https://github.com/dotnet/project-system/issues/4314 -->
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Nett" Version="0.15.0" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RobustToolbox\Lidgren.Network\Lidgren.Network.csproj" />
|
||||
|
|
|
|||
29
Content.Client/Disease/DiseaseMachineSystem.cs
Normal file
29
Content.Client/Disease/DiseaseMachineSystem.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using Robust.Client.GameObjects;
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Client.Disease
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls client-side visuals for the
|
||||
/// disease machines.
|
||||
/// </summary>
|
||||
public sealed class DiseaseMachineSystem : VisualizerSystem<DiseaseMachineVisualsComponent>
|
||||
{
|
||||
protected override void OnAppearanceChange(EntityUid uid, DiseaseMachineVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (TryComp(uid, out SpriteComponent? sprite)
|
||||
&& args.Component.TryGetData(DiseaseMachineVisuals.IsOn, out bool isOn)
|
||||
&& args.Component.TryGetData(DiseaseMachineVisuals.IsRunning, out bool isRunning))
|
||||
{
|
||||
var state = isRunning ? component.RunningState : component.IdleState;
|
||||
sprite.LayerSetVisible(DiseaseMachineVisualLayers.IsOn, isOn);
|
||||
sprite.LayerSetState(DiseaseMachineVisualLayers.IsRunning, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public enum DiseaseMachineVisualLayers : byte
|
||||
{
|
||||
IsOn,
|
||||
IsRunning
|
||||
}
|
||||
15
Content.Client/Disease/DiseaseMachineVisualsComponent.cs
Normal file
15
Content.Client/Disease/DiseaseMachineVisualsComponent.cs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
namespace Content.Client.Disease;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the idle and running state for machines to control
|
||||
/// playing animtions on the client.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseMachineVisualsComponent : Component
|
||||
{
|
||||
[DataField("idleState", required: true)]
|
||||
public string IdleState = default!;
|
||||
|
||||
[DataField("runningState", required: true)]
|
||||
public string RunningState = default!;
|
||||
}
|
||||
|
|
@ -37,6 +37,10 @@ namespace Content.Client.Entry
|
|||
"Healing",
|
||||
"Material",
|
||||
"RandomAppearance",
|
||||
"DiseaseProtection",
|
||||
"DiseaseDiagnoser",
|
||||
"DiseaseVaccine",
|
||||
"DiseaseVaccineCreator",
|
||||
"Mineable",
|
||||
"RangedMagazine",
|
||||
"Ammo",
|
||||
|
|
@ -47,12 +51,15 @@ namespace Content.Client.Entry
|
|||
"ResearchClient",
|
||||
"IdCardConsole",
|
||||
"ThermalRegulator",
|
||||
"DiseaseMachineRunning",
|
||||
"DiseaseMachine",
|
||||
"AtmosFixMarker",
|
||||
"CablePlacer",
|
||||
"Drink",
|
||||
"Food",
|
||||
"DeployableBarrier",
|
||||
"MagicMirror",
|
||||
"DiseaseSwab",
|
||||
"FloorTile",
|
||||
"RandomInsulation",
|
||||
"Electrified",
|
||||
|
|
@ -62,9 +69,9 @@ namespace Content.Client.Entry
|
|||
"Bloodstream",
|
||||
"TransformableContainer",
|
||||
"Mind",
|
||||
"DiseaseCarrier",
|
||||
"StorageFill",
|
||||
"Mop",
|
||||
"Bucket",
|
||||
"Absorbent",
|
||||
"CableVis",
|
||||
"BatterySelfRecharger",
|
||||
"Puddle",
|
||||
|
|
@ -122,6 +129,7 @@ namespace Content.Client.Entry
|
|||
"RCD",
|
||||
"RCDAmmo",
|
||||
"CursedEntityStorage",
|
||||
"DiseaseArtifact",
|
||||
"Radio",
|
||||
"GasArtifact",
|
||||
"SentienceTarget",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ using Content.Shared.Station;
|
|||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.State;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
|
|
@ -21,7 +22,6 @@ namespace Content.Client.GameTicking.Managers
|
|||
public sealed class ClientGameTicker : SharedGameTicker
|
||||
{
|
||||
[Dependency] private readonly IStateManager _stateManager = default!;
|
||||
|
||||
[ViewVariables] private bool _initialized;
|
||||
private Dictionary<StationId, Dictionary<string, int>> _jobsAvailable = new();
|
||||
private Dictionary<StationId, string> _stationNames = new();
|
||||
|
|
@ -29,6 +29,7 @@ namespace Content.Client.GameTicking.Managers
|
|||
[ViewVariables] public bool AreWeReady { get; private set; }
|
||||
[ViewVariables] public bool IsGameStarted { get; private set; }
|
||||
[ViewVariables] public string? LobbySong { get; private set; }
|
||||
[ViewVariables] public string? LobbyBackground { get; private set; }
|
||||
[ViewVariables] public bool DisallowedLateJoin { get; private set; }
|
||||
[ViewVariables] public string? ServerInfoBlob { get; private set; }
|
||||
[ViewVariables] public TimeSpan StartTime { get; private set; }
|
||||
|
|
@ -89,6 +90,7 @@ namespace Content.Client.GameTicking.Managers
|
|||
IsGameStarted = message.IsRoundStarted;
|
||||
AreWeReady = message.YouAreReady;
|
||||
LobbySong = message.LobbySong;
|
||||
LobbyBackground = message.LobbyBackground;
|
||||
Paused = message.Paused;
|
||||
if (IsGameStarted)
|
||||
Status.Clear();
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Robust.Client.AutoGenerated;
|
|||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Disease.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Damage;
|
||||
|
|
@ -35,7 +36,17 @@ namespace Content.Client.HealthAnalyzer.UI
|
|||
|
||||
text.Append($"{Loc.GetString("health-analyzer-window-entity-health-text", ("entityName", entityName))}\n");
|
||||
|
||||
text.Append($"{Loc.GetString("health-analyzer-window-entity-damage-total-text", ("amount", damageable.TotalDamage))}\n");
|
||||
/// Status Effects / Components
|
||||
if (entities.HasComponent<DiseasedComponent>(msg.TargetEntity))
|
||||
{
|
||||
text.Append($"{Loc.GetString("disease-scanner-diseased")}\n");
|
||||
}else
|
||||
{
|
||||
text.Append($"{Loc.GetString("disease-scanner-not-diseased")}\n");
|
||||
}
|
||||
|
||||
/// Damage
|
||||
text.Append($"\n{Loc.GetString("health-analyzer-window-entity-damage-total-text", ("amount", damageable.TotalDamage))}\n");
|
||||
|
||||
HashSet<string> shownTypes = new();
|
||||
|
||||
|
|
|
|||
32
Content.Client/Info/DevInfoBanner.cs
Normal file
32
Content.Client/Info/DevInfoBanner.cs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
using Content.Client.Changelog;
|
||||
using Content.Client.Credits;
|
||||
using Content.Client.Links;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Info
|
||||
{
|
||||
public sealed class DevInfoBanner : BoxContainer
|
||||
{
|
||||
public DevInfoBanner() {
|
||||
var buttons = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal
|
||||
};
|
||||
AddChild(buttons);
|
||||
|
||||
var uriOpener = IoCManager.Resolve<IUriOpener>();
|
||||
|
||||
var reportButton = new Button {Text = Loc.GetString("server-info-report-button")};
|
||||
reportButton.OnPressed += args => uriOpener.OpenUri(UILinks.BugReport);
|
||||
|
||||
var creditsButton = new Button {Text = Loc.GetString("server-info-credits-button")};
|
||||
creditsButton.OnPressed += args => new CreditsWindow().Open();
|
||||
buttons.AddChild(reportButton);
|
||||
buttons.AddChild(creditsButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
43
Content.Client/Info/LinkBanner.cs
Normal file
43
Content.Client/Info/LinkBanner.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
using Content.Client.Changelog;
|
||||
using Content.Client.Credits;
|
||||
using Content.Client.Links;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Info
|
||||
{
|
||||
public sealed class LinkBanner : BoxContainer
|
||||
{
|
||||
public LinkBanner()
|
||||
{
|
||||
var buttons = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal
|
||||
};
|
||||
AddChild(buttons);
|
||||
|
||||
var uriOpener = IoCManager.Resolve<IUriOpener>();
|
||||
|
||||
var rulesButton = new Button() {Text = Loc.GetString("server-info-rules-button")};
|
||||
rulesButton.OnPressed += args => new RulesAndInfoWindow().Open();
|
||||
|
||||
var discordButton = new Button {Text = Loc.GetString("server-info-discord-button")};
|
||||
discordButton.OnPressed += args => uriOpener.OpenUri(UILinks.Discord);
|
||||
|
||||
var websiteButton = new Button {Text = Loc.GetString("server-info-website-button")};
|
||||
websiteButton.OnPressed += args => uriOpener.OpenUri(UILinks.Website);
|
||||
|
||||
var wikiButton = new Button {Text = Loc.GetString("server-info-wiki-button")};
|
||||
wikiButton.OnPressed += args => uriOpener.OpenUri(UILinks.Wiki);
|
||||
var changelogButton = new ChangelogButton();
|
||||
buttons.AddChild(changelogButton);
|
||||
buttons.AddChild(rulesButton);
|
||||
buttons.AddChild(discordButton);
|
||||
buttons.AddChild(websiteButton);
|
||||
buttons.AddChild(wikiButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,48 +22,7 @@ namespace Content.Client.Info
|
|||
VerticalExpand = true
|
||||
};
|
||||
AddChild(_richTextLabel);
|
||||
|
||||
var buttons = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal
|
||||
};
|
||||
AddChild(buttons);
|
||||
|
||||
var uriOpener = IoCManager.Resolve<IUriOpener>();
|
||||
|
||||
var rulesButton = new Button() { Text = Loc.GetString("server-info-rules-button") };
|
||||
rulesButton.OnPressed += args => new RulesAndInfoWindow().Open();
|
||||
|
||||
var discordButton = new Button {Text = Loc.GetString("server-info-discord-button") };
|
||||
discordButton.OnPressed += args => uriOpener.OpenUri(UILinks.Discord);
|
||||
|
||||
var websiteButton = new Button {Text = Loc.GetString("server-info-website-button") };
|
||||
websiteButton.OnPressed += args => uriOpener.OpenUri(UILinks.Website);
|
||||
|
||||
var wikiButton = new Button {Text = Loc.GetString("server-info-wiki-button") };
|
||||
wikiButton.OnPressed += args => uriOpener.OpenUri(UILinks.Wiki);
|
||||
|
||||
var reportButton = new Button { Text = Loc.GetString("server-info-report-button") };
|
||||
reportButton.OnPressed += args => uriOpener.OpenUri(UILinks.BugReport);
|
||||
|
||||
var creditsButton = new Button { Text = Loc.GetString("server-info-credits-button") };
|
||||
creditsButton.OnPressed += args => new CreditsWindow().Open();
|
||||
|
||||
var changelogButton = new ChangelogButton
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
HorizontalAlignment = HAlignment.Right
|
||||
};
|
||||
|
||||
buttons.AddChild(rulesButton);
|
||||
buttons.AddChild(discordButton);
|
||||
buttons.AddChild(websiteButton);
|
||||
buttons.AddChild(wikiButton);
|
||||
buttons.AddChild(reportButton);
|
||||
buttons.AddChild(creditsButton);
|
||||
buttons.AddChild(changelogButton);
|
||||
}
|
||||
|
||||
public void SetInfoBlob(string markup)
|
||||
{
|
||||
_richTextLabel.SetMessage(FormattedMessage.FromMarkup(markup));
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ using Content.Client.LateJoin;
|
|||
using Content.Client.Lobby.UI;
|
||||
using Content.Client.Preferences;
|
||||
using Content.Client.Preferences.UI;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Voting;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Client;
|
||||
|
|
@ -22,6 +23,7 @@ using Robust.Shared.IoC;
|
|||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Client.Lobby
|
||||
|
|
@ -104,12 +106,10 @@ namespace Content.Client.Lobby
|
|||
_lobby.LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
|
||||
_lobby.OptionsButton.OnPressed += _ => new OptionsMenu().Open();
|
||||
|
||||
UpdatePlayerList();
|
||||
|
||||
_playerManager.PlayerListUpdated += PlayerManagerOnPlayerListUpdated;
|
||||
_gameTicker.InfoBlobUpdated += UpdateLobbyUi;
|
||||
_gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
|
||||
_gameTicker.LobbyReadyUpdated += LobbyReadyUpdated;
|
||||
_gameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated;
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +118,6 @@ namespace Content.Client.Lobby
|
|||
_playerManager.PlayerListUpdated -= PlayerManagerOnPlayerListUpdated;
|
||||
_gameTicker.InfoBlobUpdated -= UpdateLobbyUi;
|
||||
_gameTicker.LobbyStatusUpdated -= LobbyStatusUpdated;
|
||||
_gameTicker.LobbyReadyUpdated -= LobbyReadyUpdated;
|
||||
_gameTicker.LobbyLateJoinStatusUpdated -= LobbyLateJoinStatusUpdated;
|
||||
|
||||
_lobby?.Dispose();
|
||||
|
|
@ -176,14 +175,12 @@ namespace Content.Client.Lobby
|
|||
}
|
||||
}
|
||||
|
||||
UpdatePlayerList();
|
||||
}
|
||||
|
||||
private void LobbyReadyUpdated() => UpdatePlayerList();
|
||||
|
||||
private void LobbyStatusUpdated()
|
||||
{
|
||||
UpdatePlayerList();
|
||||
UpdateLobbyBackground();
|
||||
UpdateLobbyUi();
|
||||
}
|
||||
|
||||
|
|
@ -222,35 +219,18 @@ namespace Content.Client.Lobby
|
|||
}
|
||||
}
|
||||
|
||||
private void UpdatePlayerList()
|
||||
private void UpdateLobbyBackground()
|
||||
{
|
||||
if (_lobby == null) return;
|
||||
_lobby.OnlinePlayerList.Clear();
|
||||
var gameTicker = EntitySystem.Get<ClientGameTicker>();
|
||||
|
||||
foreach (var session in _playerManager.Sessions.OrderBy(s => s.Name))
|
||||
if (_gameTicker.LobbyBackground != null)
|
||||
{
|
||||
var readyState = string.Empty;
|
||||
// Don't show ready state if we're ingame
|
||||
if (!gameTicker.IsGameStarted)
|
||||
{
|
||||
LobbyPlayerStatus status;
|
||||
if (session.UserId == _playerManager.LocalPlayer?.UserId)
|
||||
status = gameTicker.AreWeReady ? LobbyPlayerStatus.Ready : LobbyPlayerStatus.NotReady;
|
||||
else
|
||||
gameTicker.Status.TryGetValue(session.UserId, out status);
|
||||
|
||||
readyState = status switch
|
||||
{
|
||||
LobbyPlayerStatus.NotReady => Loc.GetString("lobby-state-player-status-not-ready"),
|
||||
LobbyPlayerStatus.Ready => Loc.GetString("lobby-state-player-status-ready"),
|
||||
LobbyPlayerStatus.Observer => Loc.GetString("lobby-state-player-status-observer"),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
_lobby.OnlinePlayerList.AddItem(session.Name, readyState);
|
||||
_lobby.Background.Texture = _resourceCache.GetResource<TextureResource>(_gameTicker.LobbyBackground );
|
||||
}
|
||||
else
|
||||
{
|
||||
_lobby.Background.Texture = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void SetReady(bool newReady)
|
||||
|
|
@ -261,7 +241,6 @@ namespace Content.Client.Lobby
|
|||
}
|
||||
|
||||
_consoleHost.ExecuteCommand($"toggleready {newReady}");
|
||||
UpdatePlayerList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Linq;
|
|||
using Content.Client.HUD.UI;
|
||||
using Content.Client.Inventory;
|
||||
using Content.Client.Preferences;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.CharacterAppearance.Systems;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Inventory;
|
||||
|
|
@ -23,23 +24,20 @@ namespace Content.Client.Lobby.UI
|
|||
{
|
||||
public sealed class LobbyCharacterPreviewPanel : Control
|
||||
{
|
||||
private readonly IEntityManager _entMan;
|
||||
private readonly IClientPreferencesManager _preferencesManager;
|
||||
private readonly IPrototypeManager _prototypeManager;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
|
||||
private EntityUid? _previewDummy;
|
||||
private readonly Label _summaryLabel;
|
||||
private readonly BoxContainer _loaded;
|
||||
private readonly BoxContainer _viewBox;
|
||||
private readonly Label _unloaded;
|
||||
|
||||
public LobbyCharacterPreviewPanel(IEntityManager entityManager,
|
||||
IClientPreferencesManager preferencesManager,
|
||||
IPrototypeManager prototypeManager)
|
||||
public LobbyCharacterPreviewPanel()
|
||||
{
|
||||
_entMan = entityManager;
|
||||
_preferencesManager = preferencesManager;
|
||||
_prototypeManager = prototypeManager;
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
var header = new NanoHeading
|
||||
{
|
||||
Text = Loc.GetString("lobby-character-preview-panel-header")
|
||||
|
|
@ -57,9 +55,6 @@ namespace Content.Client.Lobby.UI
|
|||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
};
|
||||
|
||||
vBox.AddChild(header);
|
||||
|
||||
_unloaded = new Label { Text = Loc.GetString("lobby-character-preview-panel-unloaded-preferences-label") };
|
||||
|
||||
_loaded = new BoxContainer
|
||||
|
|
@ -67,17 +62,18 @@ namespace Content.Client.Lobby.UI
|
|||
Orientation = LayoutOrientation.Vertical,
|
||||
Visible = false
|
||||
};
|
||||
|
||||
_loaded.AddChild(CharacterSetupButton);
|
||||
_loaded.AddChild(_summaryLabel);
|
||||
|
||||
_viewBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal
|
||||
};
|
||||
var _vSpacer = new VSpacer();
|
||||
|
||||
_loaded.AddChild(_summaryLabel);
|
||||
_loaded.AddChild(_viewBox);
|
||||
_loaded.AddChild(_vSpacer);
|
||||
_loaded.AddChild(CharacterSetupButton);
|
||||
|
||||
vBox.AddChild(header);
|
||||
vBox.AddChild(_loaded);
|
||||
vBox.AddChild(_unloaded);
|
||||
AddChild(vBox);
|
||||
|
|
@ -95,7 +91,7 @@ namespace Content.Client.Lobby.UI
|
|||
_preferencesManager.OnServerDataLoaded -= UpdateUI;
|
||||
|
||||
if (!disposing) return;
|
||||
if (_previewDummy != null) _entMan.DeleteEntity(_previewDummy.Value);
|
||||
if (_previewDummy != null) _entityManager.DeleteEntity(_previewDummy.Value);
|
||||
_previewDummy = default;
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +99,7 @@ namespace Content.Client.Lobby.UI
|
|||
{
|
||||
return new()
|
||||
{
|
||||
Sprite = _entMan.GetComponent<ISpriteComponent>(entity),
|
||||
Sprite = _entityManager.GetComponent<ISpriteComponent>(entity),
|
||||
OverrideDirection = direction,
|
||||
Scale = (2, 2)
|
||||
};
|
||||
|
|
@ -126,7 +122,7 @@ namespace Content.Client.Lobby.UI
|
|||
}
|
||||
else
|
||||
{
|
||||
_previewDummy = _entMan.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
|
||||
_previewDummy = _entityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
|
||||
var viewSouth = MakeSpriteView(_previewDummy.Value, Direction.South);
|
||||
var viewNorth = MakeSpriteView(_previewDummy.Value, Direction.North);
|
||||
var viewWest = MakeSpriteView(_previewDummy.Value, Direction.West);
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<Control xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:maths="clr-namespace:Robust.Shared.Maths;assembly=Robust.Shared.Maths"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:parallax="clr-namespace:Content.Client.Parallax"
|
||||
xmlns:ui="clr-namespace:Content.Client.Voting.UI"
|
||||
xmlns:vote="clr-namespace:Content.Client.Voting.UI"
|
||||
xmlns:style="clr-namespace:Content.Client.Stylesheets"
|
||||
xmlns:hudUi="clr-namespace:Content.Client.HUD.UI"
|
||||
xmlns:chatUi="clr-namespace:Content.Client.Chat.UI"
|
||||
|
|
@ -13,29 +13,12 @@
|
|||
<Control>
|
||||
<!-- Parallax background -->
|
||||
<parallax:ParallaxControl />
|
||||
|
||||
<Control Margin="20 20 20 20">
|
||||
<PanelContainer StyleClasses="AngleRect" />
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Top row -->
|
||||
<BoxContainer Orientation="Horizontal" MinSize="0 40">
|
||||
<Label Margin="8 0 0 0" StyleClasses="LabelHeadingBigger" VAlign="Center" Text="{Loc 'ui-lobby-title'}" />
|
||||
<Label Name="ServerName" Access="Public" StyleClasses="LabelHeadingBigger" VAlign="Center" />
|
||||
<ui:VoteCallMenuButton Name="CallVoteButton" StyleClasses="ButtonBig" />
|
||||
<Button Name="OptionsButton" Access="Public" StyleClasses="ButtonBig" Text="{Loc 'ui-lobby-options-button'}" />
|
||||
<Button Name="LeaveButton" Access="Public" StyleClasses="ButtonBig" Text="{Loc 'ui-lobby-leave-button'}" />
|
||||
</BoxContainer>
|
||||
<!-- Gold line -->
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}"
|
||||
ContentMarginTopOverride="2" />
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
<!-- Middle section with the two vertical panels -->
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<!-- Left panel -->
|
||||
<BoxContainer Orientation="Vertical" Name="LeftPanelContainer" HorizontalExpand="True">
|
||||
<TextureRect Access="Public" Name = "Background" Stretch="Scale"/>
|
||||
<Control Margin="10 10 10 10" >
|
||||
<!-- Left Top Panel -->
|
||||
<PanelContainer StyleClasses="AngleRect" HorizontalAlignment="Left" Name = "LeftSideTop" VerticalExpand="True" VerticalAlignment="Top" >
|
||||
<BoxContainer Orientation="Vertical" HorizontalAlignment="Center" MaxWidth="620">
|
||||
<info:LinkBanner Name="LinkBanner" VerticalExpand="false" HorizontalAlignment="Center" Margin="3 3 3 3"/>
|
||||
<hudUi:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal" SeparationOverride="6" Margin="3 3 3 3">
|
||||
<cc:UICommandButton Command="observe" Name="ObserveButton" Access="Public" Text="{Loc 'ui-lobby-observe-button'}" StyleClasses="ButtonBig" WindowType="{x:Type lobbyUi:ObserveWarningWindow}"/>
|
||||
|
|
@ -46,41 +29,45 @@
|
|||
StyleClasses="LabelBig" HorizontalExpand="True" />
|
||||
<Button Name="ReadyButton" Access="Public" ToggleMode="True" Text="{Loc 'ui-lobby-ready-up-button'}"
|
||||
StyleClasses="ButtonBig" />
|
||||
</BoxContainer>
|
||||
</hudUi:StripeBack>
|
||||
<chatUi:ChatBox Name="Chat" Access="Public" VerticalExpand="True" Margin="3 3 3 3"/>
|
||||
</BoxContainer></hudUi:StripeBack>
|
||||
</BoxContainer>
|
||||
<!-- Gold line -->
|
||||
<PanelContainer MinSize="2 0">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}" />
|
||||
</PanelContainer.PanelOverride>
|
||||
</PanelContainer>
|
||||
<!-- Right panel -->
|
||||
<Control HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Player list -->
|
||||
<hudUi:NanoHeading Text="{Loc 'ui-lobby-online-players-block'}" />
|
||||
<lobbyUi:LobbyPlayerList Name="OnlinePlayerList"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
Margin="3 3 3 3" />
|
||||
<!-- Server info -->
|
||||
<hudUi:NanoHeading Text="{Loc 'ui-lobby-server-info-block'}" />
|
||||
<info:ServerInfo Name="ServerInfo"
|
||||
Access="Public"
|
||||
VerticalExpand="True"
|
||||
Margin="3 3 3 3"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Name="VoteContainer"
|
||||
Access="Public"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 8 8 0" />
|
||||
</Control>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
<!-- Left Bot Panel -->
|
||||
<BoxContainer Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Bottom" MaxWidth="620">
|
||||
<info:DevInfoBanner Name="DevInfoBanner" VerticalExpand="false" Margin="3 3 3 3"/>
|
||||
</BoxContainer>
|
||||
<!-- RightPanel Panel -->
|
||||
<PanelContainer Name="RightSide" StyleClasses="AngleRect" HorizontalAlignment= "Right" VerticalExpand="True" VerticalAlignment="Stretch">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Top row -->
|
||||
<BoxContainer Orientation="Horizontal" MinSize="0 40" Name="HeaderContainer" Access="Public">
|
||||
<Label Margin="8 0 0 0" StyleClasses="LabelHeadingBigger" VAlign="Center" Text="{Loc 'ui-lobby-title'}" />
|
||||
<Label Name="ServerName" Access="Public" StyleClasses="LabelHeadingBigger" VAlign="Center" />
|
||||
</BoxContainer>
|
||||
<!-- Gold line -->
|
||||
<ui:HLine Color="{x:Static style:StyleNano.NanoGold}" Thickness="2"/>
|
||||
<ui:HSpacer Spacing="10"/>
|
||||
<!-- Voting bar -->
|
||||
<BoxContainer Orientation="Horizontal" MinSize="0 40" Name="VoteContainer" Access="Public" HorizontalAlignment="Right">
|
||||
<cc:CommandButton Name = "AHelpButton" Command="openahelp" Access="Public" Text = "{Loc 'ui-lobby-ahelp-button'}" StyleClasses="ButtonBig"/>
|
||||
<vote:VoteCallMenuButton Name="CallVoteButton" StyleClasses="ButtonBig" />
|
||||
<Button Name="OptionsButton" Access="Public" StyleClasses="ButtonBig" Text="{Loc 'ui-lobby-options-button'}" />
|
||||
<Button Name="LeaveButton" Access="Public" StyleClasses="ButtonBig" Text="{Loc 'ui-lobby-leave-button'}" />
|
||||
</BoxContainer>
|
||||
<ui:HSpacer Spacing="10"/>
|
||||
<!-- Server info -->
|
||||
<hudUi:NanoHeading Text="{Loc 'ui-lobby-server-info-block'}" />
|
||||
<info:ServerInfo Name="ServerInfo" Access="Public" MinSize="0 30" VerticalExpand="false" Margin="3 3 3 3" MaxWidth="400"/>
|
||||
<ui:HSpacer Spacing="5"/>
|
||||
<lobbyUi:LobbyCharacterPreviewPanel Name="CharacterPreview" Access="Public"></lobbyUi:LobbyCharacterPreviewPanel>
|
||||
<ui:HSpacer Spacing="5"/>
|
||||
<BoxContainer MinHeight="10"/>
|
||||
<!-- Gold line -->
|
||||
<ui:HLine Color="{x:Static style:StyleNano.NanoGold}" Thickness="2" Access="Public"/>
|
||||
<ui:HSpacer Spacing="10"/>
|
||||
<chatUi:ChatBox Name="Chat" Access="Public" VerticalExpand="True" Margin="3 3 3 3" MinHeight="50"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</Control>
|
||||
</Control>
|
||||
</Control>
|
||||
|
|
|
|||
|
|
@ -17,108 +17,12 @@ namespace Content.Client.Lobby.UI
|
|||
[GenerateTypedNameReferences]
|
||||
internal sealed partial class LobbyGui : Control
|
||||
{
|
||||
public LobbyCharacterPreviewPanel CharacterPreview { get; }
|
||||
|
||||
public LobbyGui(IEntityManager entityManager,
|
||||
IClientPreferencesManager preferencesManager)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
ServerName.HorizontalExpand = true;
|
||||
ServerName.HorizontalAlignment = HAlignment.Center;
|
||||
|
||||
CharacterPreview = new LobbyCharacterPreviewPanel(
|
||||
entityManager,
|
||||
preferencesManager,
|
||||
IoCManager.Resolve<IPrototypeManager>())
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Left
|
||||
};
|
||||
|
||||
LeftPanelContainer.AddChild(CharacterPreview);
|
||||
CharacterPreview.SetPositionFirst();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LobbyPlayerList : Control
|
||||
{
|
||||
private readonly ScrollContainer _scroll;
|
||||
private readonly BoxContainer _vBox;
|
||||
|
||||
public LobbyPlayerList()
|
||||
{
|
||||
var panel = new PanelContainer()
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat { BackgroundColor = Color.FromHex("#202028") },
|
||||
};
|
||||
_vBox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Vertical
|
||||
};
|
||||
_scroll = new ScrollContainer();
|
||||
_scroll.AddChild(_vBox);
|
||||
panel.AddChild(_scroll);
|
||||
AddChild(panel);
|
||||
}
|
||||
|
||||
// Adds a row
|
||||
public void AddItem(string name, string status)
|
||||
{
|
||||
var hbox = new BoxContainer
|
||||
{
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
HorizontalExpand = true,
|
||||
};
|
||||
|
||||
// Player Name
|
||||
hbox.AddChild(new PanelContainer()
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#373744"),
|
||||
ContentMarginBottomOverride = 2,
|
||||
ContentMarginLeftOverride = 4,
|
||||
ContentMarginRightOverride = 4,
|
||||
ContentMarginTopOverride = 2
|
||||
},
|
||||
Children =
|
||||
{
|
||||
new Label
|
||||
{
|
||||
Text = name
|
||||
}
|
||||
},
|
||||
HorizontalExpand = true
|
||||
});
|
||||
// Status
|
||||
hbox.AddChild(new PanelContainer()
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#373744"),
|
||||
ContentMarginBottomOverride = 2,
|
||||
ContentMarginLeftOverride = 4,
|
||||
ContentMarginRightOverride = 4,
|
||||
ContentMarginTopOverride = 2
|
||||
},
|
||||
Children =
|
||||
{
|
||||
new Label
|
||||
{
|
||||
Text = status
|
||||
}
|
||||
},
|
||||
HorizontalExpand = true,
|
||||
SizeFlagsStretchRatio = 0.2f,
|
||||
});
|
||||
|
||||
_vBox.AddChild(hbox);
|
||||
}
|
||||
|
||||
// Deletes all rows
|
||||
public void Clear()
|
||||
{
|
||||
_vBox.RemoveAllChildren();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
46
Content.Client/UserInterface/Controls/HLine.cs
Normal file
46
Content.Client/UserInterface/Controls/HLine.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
public sealed class HLine : Container
|
||||
{
|
||||
public Color? Color
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_line.PanelOverride is StyleBoxFlat styleBox) return styleBox.BackgroundColor;
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_line.PanelOverride is StyleBoxFlat styleBox) styleBox.BackgroundColor = value!.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public float? Thickness {
|
||||
get
|
||||
{
|
||||
if (_line.PanelOverride is StyleBoxFlat styleBox) return styleBox.ContentMarginTopOverride;
|
||||
return null;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (_line.PanelOverride is StyleBoxFlat styleBox) styleBox.ContentMarginTopOverride = value!.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly PanelContainer _line;
|
||||
|
||||
public HLine()
|
||||
{
|
||||
_line = new PanelContainer();
|
||||
_line.PanelOverride = new StyleBoxFlat();
|
||||
_line.PanelOverride.ContentMarginTopOverride = Thickness;
|
||||
AddChild(_line);
|
||||
}
|
||||
|
||||
}
|
||||
21
Content.Client/UserInterface/Controls/HSpacer.cs
Normal file
21
Content.Client/UserInterface/Controls/HSpacer.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
using TerraFX.Interop.Windows;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
public sealed class HSpacer : Control
|
||||
{
|
||||
public float Spacing { get => MinHeight; set => MinHeight = value; }
|
||||
public HSpacer()
|
||||
{
|
||||
MinHeight = Spacing;
|
||||
}
|
||||
public HSpacer(float height = 5)
|
||||
{
|
||||
Spacing = height;
|
||||
MinHeight = height;
|
||||
}
|
||||
}
|
||||
20
Content.Client/UserInterface/Controls/VSpacer.cs
Normal file
20
Content.Client/UserInterface/Controls/VSpacer.cs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Maths;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
public sealed class VSpacer : Control
|
||||
{
|
||||
public float Spacing{ get => MinWidth; set => MinWidth = value; }
|
||||
public VSpacer()
|
||||
{
|
||||
MinWidth = Spacing;
|
||||
}
|
||||
public VSpacer(float width = 5)
|
||||
{
|
||||
Spacing = width;
|
||||
MinWidth = width;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,9 +10,9 @@
|
|||
</PropertyGroup>
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Administration.Logs;
|
||||
|
|
@ -10,6 +8,9 @@ using Content.Server.MoMMI;
|
|||
using Content.Server.Players;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.Radio.EntitySystems;
|
||||
using Content.Server.Disease;
|
||||
using Content.Server.Disease.Components;
|
||||
using Content.Shared.Disease.Components;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
|
|
@ -22,10 +23,6 @@ using Robust.Server.Player;
|
|||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Players;
|
||||
|
|
@ -208,6 +205,11 @@ namespace Content.Server.Chat.Managers
|
|||
return;
|
||||
}
|
||||
|
||||
if (_entManager.HasComponent<DiseasedComponent>(source) && _entManager.TryGetComponent<DiseaseCarrierComponent>(source,out var carrier))
|
||||
{
|
||||
EntitySystem.Get<DiseaseSystem>().SneezeCough(source, _random.Pick(carrier.Diseases), string.Empty);
|
||||
}
|
||||
|
||||
if (MessageCharacterLimit(source, message))
|
||||
{
|
||||
return;
|
||||
|
|
|
|||
33
Content.Server/Chemistry/ReagentEffects/ChemCauseDisease.cs
Normal file
33
Content.Server/Chemistry/ReagentEffects/ChemCauseDisease.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Server.Disease;
|
||||
using Content.Shared.Disease;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Chemistry.ReagentEffects
|
||||
{
|
||||
/// <summary>
|
||||
/// Default metabolism for medicine reagents.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class ChemCauseDisease : ReagentEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Chance it has each tick to cause disease, between 0 and 1
|
||||
/// </summary>
|
||||
[DataField("causeChance")]
|
||||
public float CauseChance = 0.15f;
|
||||
|
||||
/// <summary>
|
||||
/// The disease to add.
|
||||
/// </summary>
|
||||
[DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>), required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string Disease = default!;
|
||||
|
||||
public override void Effect(ReagentEffectArgs args)
|
||||
{
|
||||
EntitySystem.Get<DiseaseSystem>().TryAddDisease(args.SolutionEntity, Disease);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Content.Server/Chemistry/ReagentEffects/ChemCureDisease.cs
Normal file
25
Content.Server/Chemistry/ReagentEffects/ChemCureDisease.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Server.Disease;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Chemistry.ReagentEffects
|
||||
{
|
||||
/// <summary>
|
||||
/// Default metabolism for medicine reagents.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class ChemCureDisease : ReagentEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Chance it has each tick to cure a disease, between 0 and 1
|
||||
/// </summary>
|
||||
[DataField("cureChance")]
|
||||
public float CureChance = 0.15f;
|
||||
|
||||
public override void Effect(ReagentEffectArgs args)
|
||||
{
|
||||
var ev = new CureDiseaseAttemptEvent(CureChance);
|
||||
args.EntityManager.EventBus.RaiseLocalEvent(args.SolutionEntity, ev, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.FixedPoint;
|
||||
using JetBrains.Annotations;
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ namespace Content.Server.Clothing
|
|||
|
||||
SubscribeLocalEvent<MagbootsComponent, GetVerbsEvent<ActivationVerb>>(AddToggleVerb);
|
||||
SubscribeLocalEvent<MagbootsComponent, SlipAttemptEvent>(OnSlipAttempt);
|
||||
SubscribeLocalEvent<MagbootsComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
|
||||
SubscribeLocalEvent<MagbootsComponent, GotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<MagbootsComponent, GotUnequippedEvent>(OnGotUnequipped);
|
||||
}
|
||||
|
|
@ -63,11 +62,6 @@ namespace Content.Server.Clothing
|
|||
}
|
||||
}
|
||||
|
||||
private void OnRefreshMovespeed(EntityUid uid, MagbootsComponent component, RefreshMovementSpeedModifiersEvent args)
|
||||
{
|
||||
args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
|
||||
}
|
||||
|
||||
private void AddToggleVerb(EntityUid uid, MagbootsComponent component, GetVerbsEvent<ActivationVerb> args)
|
||||
{
|
||||
if (!args.CanAccess || !args.CanInteract)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
</PropertyGroup>
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Content.Server.Database\Content.Server.Database.csproj" />
|
||||
|
|
|
|||
36
Content.Server/Disease/Components/DiseaseCarrierComponent.cs
Normal file
36
Content.Server/Disease/Components/DiseaseCarrierComponent.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using System.Linq;
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// Allows the enity to be infected with diseases.
|
||||
/// Please use only on mobs.
|
||||
/// </summary>
|
||||
public sealed class DiseaseCarrierComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the CURRENT diseases on the carrier
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<DiseasePrototype> Diseases = new();
|
||||
/// <summary>
|
||||
/// The carrier's resistance to disease
|
||||
/// </summary>
|
||||
[DataField("diseaseResist")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float DiseaseResist = 0f;
|
||||
/// <summary>
|
||||
/// Diseases the carrier has had, used for immunity.
|
||||
/// <summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<DiseasePrototype> PastDiseases = new();
|
||||
/// <summary>
|
||||
/// All the diseases the carrier has or has had.
|
||||
/// Checked against when trying to add a disease
|
||||
/// <summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public List<DiseasePrototype> AllDiseases => PastDiseases.Concat(Diseases).ToList();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// To give the disease diagnosing machine specific behavior
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseDiagnoserComponent : Component
|
||||
{}
|
||||
}
|
||||
31
Content.Server/Disease/Components/DiseaseMachineComponent.cs
Normal file
31
Content.Server/Disease/Components/DiseaseMachineComponent.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Content.Shared.Disease;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// For shared behavior between both disease machines
|
||||
/// </summary>
|
||||
public sealed class DiseaseMachineComponent : Component
|
||||
{
|
||||
[DataField("delay")]
|
||||
public float Delay = 5f;
|
||||
/// <summary>
|
||||
/// How much time we've accumulated processing
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public float Accumulator = 0f;
|
||||
/// <summary>
|
||||
/// The disease prototype currently being diagnosed
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public DiseasePrototype? Disease;
|
||||
/// <summary>
|
||||
/// What the machine will spawn
|
||||
/// </summary>
|
||||
[DataField("machineOutput", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
|
||||
public string MachineOutput = string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For EntityQuery to keep track of which machines are running
|
||||
/// <summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseMachineRunningComponent : Component
|
||||
{}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Value added to clothing to give its wearer
|
||||
/// protection against infection from diseases
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseProtectionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Float value between 0 and 1, will be subtracted
|
||||
/// from the infection chance (which is base 0.7)
|
||||
/// Reference guide is a full biosuit w/gloves & mask
|
||||
/// should add up to exactly 0.7
|
||||
/// </summary>
|
||||
[DataField("protection")]
|
||||
public float Protection = 0.1f;
|
||||
/// <summary>
|
||||
/// Is the component currently being worn and affecting someone's disease
|
||||
/// resistance? Making the unequip check not totally CBT
|
||||
/// </summary>
|
||||
public bool IsActive = false;
|
||||
}
|
||||
}
|
||||
33
Content.Server/Disease/Components/DiseaseSwabComponent.cs
Normal file
33
Content.Server/Disease/Components/DiseaseSwabComponent.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using System.Threading;
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
/// <summary>
|
||||
/// For mouth swabs used to collect and process
|
||||
/// disease samples.
|
||||
/// </summary>
|
||||
public sealed class DiseaseSwabComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How long it takes to swab someone.
|
||||
/// </summary>
|
||||
[DataField("swabDelay")]
|
||||
[ViewVariables]
|
||||
public float SwabDelay = 2f;
|
||||
/// <summary>
|
||||
/// If this swab has been used
|
||||
/// </summary>
|
||||
public bool Used = false;
|
||||
/// <summary>
|
||||
/// Token for interrupting swabbing do after.
|
||||
/// </summary>
|
||||
public CancellationTokenSource? CancelToken;
|
||||
/// <summary>
|
||||
/// The disease prototype currently on the swab
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public DiseasePrototype? Disease;
|
||||
}
|
||||
}
|
||||
33
Content.Server/Disease/Components/DiseaseVaccineComponent.cs
Normal file
33
Content.Server/Disease/Components/DiseaseVaccineComponent.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using System.Threading;
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For disease vaccines
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseVaccineComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How long it takes to inject someone
|
||||
/// </summary>
|
||||
[DataField("injectDelay")]
|
||||
[ViewVariables]
|
||||
public float InjectDelay = 2f;
|
||||
/// <summary>
|
||||
/// If this vaccine has been used
|
||||
/// </summary>
|
||||
public bool Used = false;
|
||||
/// <summary>
|
||||
/// Token for interrupting injection do after.
|
||||
/// </summary>
|
||||
public CancellationTokenSource? CancelToken;
|
||||
|
||||
/// <summary>
|
||||
/// The disease prototype currently on the vaccine
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DiseasePrototype? Disease;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
namespace Content.Server.Disease.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Controls disease machine behavior specific to the
|
||||
/// vaccine creating machine
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseVaccineCreatorComponent : Component
|
||||
{}
|
||||
}
|
||||
33
Content.Server/Disease/Cures/DiseaseBedrestCure.cs
Normal file
33
Content.Server/Disease/Cures/DiseaseBedrestCure.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using Content.Shared.Disease;
|
||||
using Content.Server.Buckle.Components;
|
||||
|
||||
namespace Content.Server.Disease.Cures
|
||||
{
|
||||
/// <summary>
|
||||
/// Cures the disease after a certain amount of time
|
||||
/// strapped.
|
||||
/// </summary>
|
||||
/// TODO: Revisit after bed pr merged
|
||||
public sealed class DiseaseBedrestCure : DiseaseCure
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Ticker = 0;
|
||||
[DataField("maxLength", required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MaxLength = 60;
|
||||
|
||||
public override bool Cure(DiseaseEffectArgs args)
|
||||
{
|
||||
if (!args.EntityManager.TryGetComponent<BuckleComponent>(args.DiseasedEntity, out var buckle))
|
||||
return false;
|
||||
if (buckle.Buckled)
|
||||
Ticker++;
|
||||
return Ticker >= MaxLength;
|
||||
}
|
||||
|
||||
public override string CureText()
|
||||
{
|
||||
return (Loc.GetString("diagnoser-cure-bedrest", ("time", MaxLength)));
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Content.Server/Disease/Cures/DiseaseBodyTemperatureCure.cs
Normal file
34
Content.Server/Disease/Cures/DiseaseBodyTemperatureCure.cs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
using Content.Server.Temperature.Components;
|
||||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Cures
|
||||
{
|
||||
/// <summary>
|
||||
/// Cures the disease if temperature is within certain bounds.
|
||||
/// </summary>
|
||||
public sealed class DiseaseBodyTemperatureCure : DiseaseCure
|
||||
{
|
||||
[DataField("min")]
|
||||
public float Min = 0;
|
||||
|
||||
[DataField("max")]
|
||||
public float Max = float.MaxValue;
|
||||
public override bool Cure(DiseaseEffectArgs args)
|
||||
{
|
||||
if (!args.EntityManager.TryGetComponent(args.DiseasedEntity, out TemperatureComponent temp))
|
||||
return false;
|
||||
|
||||
return temp.CurrentTemperature > Min && temp.CurrentTemperature < float.MaxValue;
|
||||
}
|
||||
|
||||
public override string CureText()
|
||||
{
|
||||
if (Min == 0)
|
||||
return Loc.GetString("diagnoser-cure-temp-max", ("max", Math.Round(Max)));
|
||||
if (Max == float.MaxValue)
|
||||
return Loc.GetString("diagnoser-cure-temp-min", ("min", Math.Round(Min)));
|
||||
|
||||
return Loc.GetString("diagnoser-cure-temp-both", ("max", Math.Round(Max)), ("min", Math.Round(Min)));
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Content.Server/Disease/Cures/DiseaseJustWaitCure.cs
Normal file
31
Content.Server/Disease/Cures/DiseaseJustWaitCure.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Content.Shared.Disease;
|
||||
|
||||
namespace Content.Server.Disease.Cures
|
||||
{
|
||||
/// <summary>
|
||||
/// Automatically removes the disease after a
|
||||
/// certain amount of time.
|
||||
/// </summary>
|
||||
public sealed class DiseaseJustWaitCure : DiseaseCure
|
||||
{
|
||||
/// <summary>
|
||||
/// All of these are in seconds
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Ticker = 0;
|
||||
[DataField("maxLength", required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int MaxLength = 150;
|
||||
|
||||
public override bool Cure(DiseaseEffectArgs args)
|
||||
{
|
||||
Ticker++;
|
||||
return Ticker >= MaxLength;
|
||||
}
|
||||
|
||||
public override string CureText()
|
||||
{
|
||||
return Loc.GetString("diagnoser-cure-wait", ("time", MaxLength));
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Content.Server/Disease/Cures/DiseaseReagentCure.cs
Normal file
38
Content.Server/Disease/Cures/DiseaseReagentCure.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using Content.Shared.Disease;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Server.Body.Components;
|
||||
|
||||
namespace Content.Server.Disease.Cures
|
||||
{
|
||||
/// <summary>
|
||||
/// Cures the disease if a certain amount of reagent
|
||||
/// is in the host's chemstream.
|
||||
/// </summary>
|
||||
public sealed class DiseaseReagentCure : DiseaseCure
|
||||
{
|
||||
[DataField("min")]
|
||||
public FixedPoint2 Min = 5;
|
||||
[DataField("reagent")]
|
||||
public string? Reagent;
|
||||
|
||||
public override bool Cure(DiseaseEffectArgs args)
|
||||
{
|
||||
if (!args.EntityManager.TryGetComponent<BloodstreamComponent>(args.DiseasedEntity, out var bloodstream))
|
||||
return false;
|
||||
|
||||
var quant = FixedPoint2.Zero;
|
||||
if (Reagent != null && bloodstream.ChemicalSolution.ContainsReagent(Reagent))
|
||||
{
|
||||
quant = bloodstream.ChemicalSolution.GetReagentQuantity(Reagent);
|
||||
}
|
||||
return quant >= Min;
|
||||
}
|
||||
|
||||
public override string CureText()
|
||||
{
|
||||
if (Reagent == null)
|
||||
return string.Empty;
|
||||
return (Loc.GetString("diagnoser-cure-reagent", ("units", Min), ("reagent", Reagent)));
|
||||
}
|
||||
}
|
||||
}
|
||||
415
Content.Server/Disease/DiseaseDiagnosisSystem.cs
Normal file
415
Content.Server/Disease/DiseaseDiagnosisSystem.cs
Normal file
|
|
@ -0,0 +1,415 @@
|
|||
using System.Threading;
|
||||
using Content.Server.Disease.Components;
|
||||
using Content.Shared.Disease;
|
||||
using Content.Shared.Disease.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Content.Server.Paper;
|
||||
using Content.Server.Tools.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Disease
|
||||
{
|
||||
/// Everything that's about disease diangosis and machines is in here
|
||||
|
||||
public sealed class DiseaseDiagnosisSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<DiseaseSwabComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<DiseaseSwabComponent, ExaminedEvent>(OnExamined);
|
||||
SubscribeLocalEvent<DiseaseDiagnoserComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
|
||||
SubscribeLocalEvent<DiseaseVaccineCreatorComponent, AfterInteractUsingEvent>(OnAfterInteractUsingVaccine);
|
||||
/// Visuals
|
||||
SubscribeLocalEvent<DiseaseMachineComponent, PowerChangedEvent>(OnPowerChanged);
|
||||
/// Private Events
|
||||
SubscribeLocalEvent<DiseaseDiagnoserComponent, DiseaseMachineFinishedEvent>(OnDiagnoserFinished);
|
||||
SubscribeLocalEvent<DiseaseVaccineCreatorComponent, DiseaseMachineFinishedEvent>(OnVaccinatorFinished);
|
||||
SubscribeLocalEvent<TargetSwabSuccessfulEvent>(OnTargetSwabSuccessful);
|
||||
SubscribeLocalEvent<SwabCancelledEvent>(OnSwabCancelled);
|
||||
}
|
||||
|
||||
private Queue<EntityUid> AddQueue = new();
|
||||
private Queue<EntityUid> RemoveQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// This handles running disease machines
|
||||
/// to handle their delay and visuals.
|
||||
/// </summary>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
foreach (var uid in AddQueue)
|
||||
EnsureComp<DiseaseMachineRunningComponent>(uid);
|
||||
|
||||
AddQueue.Clear();
|
||||
foreach (var uid in RemoveQueue)
|
||||
RemComp<DiseaseMachineRunningComponent>(uid);
|
||||
|
||||
RemoveQueue.Clear();
|
||||
|
||||
foreach (var (runningComp, diseaseMachine) in EntityQuery<DiseaseMachineRunningComponent, DiseaseMachineComponent>(false))
|
||||
{
|
||||
if (diseaseMachine.Accumulator < diseaseMachine.Delay)
|
||||
{
|
||||
diseaseMachine.Accumulator += frameTime;
|
||||
return;
|
||||
}
|
||||
|
||||
diseaseMachine.Accumulator = 0;
|
||||
var ev = new DiseaseMachineFinishedEvent(diseaseMachine);
|
||||
RaiseLocalEvent(diseaseMachine.Owner, ev, false);
|
||||
RemoveQueue.Enqueue(diseaseMachine.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Event Handlers
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// This handles using swabs on other people
|
||||
/// and checks that the swab isn't already used
|
||||
/// and the other person's mouth is accessible
|
||||
/// and then adds a random disease from that person
|
||||
/// to the swab if they have any
|
||||
/// </summary>
|
||||
private void OnAfterInteract(EntityUid uid, DiseaseSwabComponent swab, AfterInteractEvent args)
|
||||
{
|
||||
if (swab.CancelToken != null)
|
||||
{
|
||||
swab.CancelToken.Cancel();
|
||||
swab.CancelToken = null;
|
||||
return;
|
||||
}
|
||||
if (args.Target == null || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (!TryComp<DiseaseCarrierComponent>(args.Target, out var carrier))
|
||||
return;
|
||||
|
||||
if (swab.Used)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("swab-already-used"), args.User, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_inventorySystem.TryGetSlotEntity(args.Target.Value, "mask", out var maskUid) &&
|
||||
EntityManager.TryGetComponent<IngestionBlockerComponent>(maskUid, out var blocker) &&
|
||||
blocker.Enabled)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("swab-mask-blocked", ("target", args.Target), ("mask", maskUid)), args.User, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
|
||||
swab.CancelToken = new CancellationTokenSource();
|
||||
_doAfterSystem.DoAfter(new DoAfterEventArgs(args.User, swab.SwabDelay, swab.CancelToken.Token, target: args.Target)
|
||||
{
|
||||
BroadcastFinishedEvent = new TargetSwabSuccessfulEvent(args.User, args.Target, swab, carrier),
|
||||
BroadcastCancelledEvent = new SwabCancelledEvent(swab),
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnStun = true,
|
||||
NeedHand = true
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This handles the disease diagnoser machine up
|
||||
/// until it's turned on. It has some slight
|
||||
/// differences in checks from the vaccinator.
|
||||
/// </summary>
|
||||
private void OnAfterInteractUsing(EntityUid uid, DiseaseDiagnoserComponent component, AfterInteractUsingEvent args)
|
||||
{
|
||||
var machine = Comp<DiseaseMachineComponent>(uid);
|
||||
if (args.Handled || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (HasComp<DiseaseMachineRunningComponent>(uid) || TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|
||||
return;
|
||||
|
||||
if (!HasComp<HandsComponent>(args.User) || HasComp<ToolComponent>(args.Used)) // Don't want to accidentally breach wrenching or whatever
|
||||
return;
|
||||
|
||||
if (!TryComp<DiseaseSwabComponent>(args.Used, out var swab))
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("diagnoser-cant-use-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
_popupSystem.PopupEntity(Loc.GetString("diagnoser-insert-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
|
||||
|
||||
|
||||
machine.Disease = swab.Disease;
|
||||
EntityManager.DeleteEntity(args.Used);
|
||||
|
||||
AddQueue.Enqueue(uid);
|
||||
UpdateAppearance(uid, true, true);
|
||||
SoundSystem.Play(Filter.Pvs(uid), "/Audio/Machines/diagnoser_printing.ogg", uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This handles the vaccinator machine up
|
||||
/// until it's turned on. It has some slight
|
||||
/// differences in checks from the diagnoser.
|
||||
/// </summary>
|
||||
private void OnAfterInteractUsingVaccine(EntityUid uid, DiseaseVaccineCreatorComponent component, AfterInteractUsingEvent args)
|
||||
{
|
||||
if (args.Handled || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (HasComp<DiseaseMachineRunningComponent>(uid) || TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|
||||
return;
|
||||
|
||||
if (!HasComp<HandsComponent>(args.User) || HasComp<ToolComponent>(args.Used)) //This check ensures tools don't break without yaml ordering jank
|
||||
return;
|
||||
|
||||
if (!TryComp<DiseaseSwabComponent>(args.Used, out var swab) || swab.Disease == null || !swab.Disease.Infectious)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("diagnoser-cant-use-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
_popupSystem.PopupEntity(Loc.GetString("diagnoser-insert-swab", ("machine", uid), ("swab", args.Used)), uid, Filter.Entities(args.User));
|
||||
var machine = Comp<DiseaseMachineComponent>(uid);
|
||||
machine.Disease = swab.Disease;
|
||||
EntityManager.DeleteEntity(args.Used);
|
||||
|
||||
AddQueue.Enqueue(uid);
|
||||
UpdateAppearance(uid, true, true);
|
||||
SoundSystem.Play(Filter.Pvs(uid), "/Audio/Machines/vaccinator_running.ogg", uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This handles swab examination text
|
||||
/// so you can tell if they are used or not.
|
||||
/// </summary>
|
||||
private void OnExamined(EntityUid uid, DiseaseSwabComponent swab, ExaminedEvent args)
|
||||
{
|
||||
if (args.IsInDetailsRange)
|
||||
{
|
||||
if (swab.Used)
|
||||
args.PushMarkup(Loc.GetString("swab-used"));
|
||||
else
|
||||
args.PushMarkup(Loc.GetString("swab-unused"));
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Helper functions
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// This assembles a disease report
|
||||
/// With its basic details and
|
||||
/// specific cures (i.e. not spaceacillin).
|
||||
/// The cure resist field tells you how
|
||||
/// effective spaceacillin etc will be.
|
||||
/// </summary>
|
||||
private FormattedMessage AssembleDiseaseReport(DiseasePrototype disease)
|
||||
{
|
||||
FormattedMessage report = new();
|
||||
report.AddMarkup(Loc.GetString("diagnoser-disease-report-name", ("disease", disease.Name)));
|
||||
report.PushNewline();
|
||||
|
||||
if (disease.Infectious)
|
||||
{
|
||||
report.AddMarkup(Loc.GetString("diagnoser-disease-report-infectious"));
|
||||
report.PushNewline();
|
||||
} else
|
||||
{
|
||||
report.AddMarkup(Loc.GetString("diagnoser-disease-report-not-infectious"));
|
||||
report.PushNewline();
|
||||
}
|
||||
string cureResistLine = string.Empty;
|
||||
cureResistLine += disease.CureResist switch
|
||||
{
|
||||
< 0f => Loc.GetString("diagnoser-disease-report-cureresist-none"),
|
||||
<= 0.05f => Loc.GetString("diagnoser-disease-report-cureresist-low"),
|
||||
<= 0.14f => Loc.GetString("diagnoser-disease-report-cureresist-medium"),
|
||||
_ => Loc.GetString("diagnoser-disease-report-cureresist-high")
|
||||
};
|
||||
report.AddMarkup(cureResistLine);
|
||||
report.PushNewline();
|
||||
|
||||
/// Add Cures
|
||||
if (disease.Cures.Count == 0)
|
||||
{
|
||||
report.AddMarkup(Loc.GetString("diagnoser-no-cures"));
|
||||
}
|
||||
else
|
||||
{
|
||||
report.PushNewline();
|
||||
report.AddMarkup(Loc.GetString("diagnoser-cure-has"));
|
||||
report.PushNewline();
|
||||
|
||||
foreach (var cure in disease.Cures)
|
||||
{
|
||||
report.AddMarkup(cure.CureText());
|
||||
report.PushNewline();
|
||||
}
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
///
|
||||
/// Appearance stuff
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// Appearance helper function to
|
||||
/// set the component's power and running states.
|
||||
/// </summary>
|
||||
private void UpdateAppearance(EntityUid uid, bool isOn, bool isRunning)
|
||||
{
|
||||
if (!TryComp<AppearanceComponent>(uid, out var appearance))
|
||||
return;
|
||||
|
||||
appearance.SetData(DiseaseMachineVisuals.IsOn, isOn);
|
||||
appearance.SetData(DiseaseMachineVisuals.IsRunning, isRunning);
|
||||
}
|
||||
/// <summary>
|
||||
/// Makes sure the machine is visually off/on.
|
||||
/// </summary>
|
||||
private void OnPowerChanged(EntityUid uid, DiseaseMachineComponent component, PowerChangedEvent args)
|
||||
{
|
||||
UpdateAppearance(uid, args.Powered, false);
|
||||
}
|
||||
///
|
||||
/// Private events
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// Copies a disease prototype to the swab
|
||||
/// after the doafter completes.
|
||||
/// </summary>
|
||||
private void OnTargetSwabSuccessful(TargetSwabSuccessfulEvent args)
|
||||
{
|
||||
if (args.Target == null)
|
||||
return;
|
||||
|
||||
args.Swab.Used = true;
|
||||
_popupSystem.PopupEntity(Loc.GetString("swab-swabbed", ("target", args.Target)), args.Target.Value, Filter.Entities(args.User));
|
||||
|
||||
if (args.Swab.Disease != null || args.Carrier.Diseases.Count == 0)
|
||||
return;
|
||||
|
||||
args.Swab.Disease = _random.Pick(args.Carrier.Diseases);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the swab doafter if needed.
|
||||
/// </summary>
|
||||
private static void OnSwabCancelled(SwabCancelledEvent args)
|
||||
{
|
||||
args.Swab.CancelToken = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints a diagnostic report with its findings.
|
||||
/// Also cancels the animation.
|
||||
/// </summary>
|
||||
private void OnDiagnoserFinished(EntityUid uid, DiseaseDiagnoserComponent component, DiseaseMachineFinishedEvent args)
|
||||
{
|
||||
var power = Comp<ApcPowerReceiverComponent>(uid);
|
||||
UpdateAppearance(uid, power.Powered, false);
|
||||
// spawn a piece of paper.
|
||||
var printed = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates);
|
||||
|
||||
if (!TryComp<PaperComponent>(printed, out var paper))
|
||||
return;
|
||||
|
||||
var reportTitle = string.Empty;
|
||||
FormattedMessage contents = new();
|
||||
if (args.Machine.Disease != null)
|
||||
{
|
||||
reportTitle = Loc.GetString("diagnoser-disease-report", ("disease", args.Machine.Disease.Name));
|
||||
contents = AssembleDiseaseReport(args.Machine.Disease);
|
||||
} else
|
||||
{
|
||||
reportTitle = Loc.GetString("diagnoser-disease-report-none");
|
||||
contents.AddMarkup(Loc.GetString("diagnoser-disease-report-none-contents"));
|
||||
}
|
||||
MetaData(printed).EntityName = reportTitle;
|
||||
|
||||
paper.SetContent(contents.ToMarkup());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints a vaccine that will vaccinate
|
||||
/// against the disease on the inserted swab.
|
||||
/// <summary>
|
||||
private void OnVaccinatorFinished(EntityUid uid, DiseaseVaccineCreatorComponent component, DiseaseMachineFinishedEvent args)
|
||||
{
|
||||
var power = Comp<ApcPowerReceiverComponent>(uid);
|
||||
UpdateAppearance(uid, power.Powered, false);
|
||||
|
||||
// spawn a vaccine
|
||||
var vaxx = EntityManager.SpawnEntity(args.Machine.MachineOutput, Transform(uid).Coordinates);
|
||||
|
||||
if (!TryComp<DiseaseVaccineComponent>(vaxx, out var vaxxComp))
|
||||
return;
|
||||
|
||||
vaxxComp.Disease = args.Machine.Disease;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the mouth-swabbing doafter
|
||||
/// </summary>
|
||||
private sealed class SwabCancelledEvent : EntityEventArgs
|
||||
{
|
||||
public readonly DiseaseSwabComponent Swab;
|
||||
public SwabCancelledEvent(DiseaseSwabComponent swab)
|
||||
{
|
||||
Swab = swab;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires if the doafter for swabbing someone's mouth succeeds
|
||||
/// </summary>
|
||||
private sealed class TargetSwabSuccessfulEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid User { get; }
|
||||
public EntityUid? Target { get; }
|
||||
public DiseaseSwabComponent Swab { get; }
|
||||
|
||||
public DiseaseCarrierComponent Carrier { get; }
|
||||
|
||||
public TargetSwabSuccessfulEvent(EntityUid user, EntityUid? target, DiseaseSwabComponent swab, DiseaseCarrierComponent carrier)
|
||||
{
|
||||
User = user;
|
||||
Target = target;
|
||||
Swab = swab;
|
||||
Carrier = carrier;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fires when a disease machine is done
|
||||
/// with its production delay and ready to
|
||||
/// create a report or vaccine
|
||||
/// </summary>
|
||||
private sealed class DiseaseMachineFinishedEvent : EntityEventArgs
|
||||
{
|
||||
public DiseaseMachineComponent Machine {get;}
|
||||
public DiseaseMachineFinishedEvent(DiseaseMachineComponent machine)
|
||||
{
|
||||
Machine = machine;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
456
Content.Server/Disease/DiseaseSystem.cs
Normal file
456
Content.Server/Disease/DiseaseSystem.cs
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
using System.Threading;
|
||||
using Content.Shared.Disease;
|
||||
using Content.Shared.Disease.Components;
|
||||
using Content.Server.Disease.Components;
|
||||
using Content.Server.Clothing.Components;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.DoAfter;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Server.Nutrition.EntitySystems;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Disease
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Handles disease propagation & curing
|
||||
/// </summary>
|
||||
public sealed class DiseaseSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ISerializationManager _serializationManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<DiseaseCarrierComponent, CureDiseaseAttemptEvent>(OnTryCureDisease);
|
||||
SubscribeLocalEvent<DiseasedComponent, InteractHandEvent>(OnInteractDiseasedHand);
|
||||
SubscribeLocalEvent<DiseasedComponent, InteractUsingEvent>(OnInteractDiseasedUsing);
|
||||
SubscribeLocalEvent<DiseaseProtectionComponent, GotEquippedEvent>(OnEquipped);
|
||||
SubscribeLocalEvent<DiseaseProtectionComponent, GotUnequippedEvent>(OnUnequipped);
|
||||
SubscribeLocalEvent<DiseaseVaccineComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<DiseaseVaccineComponent, ExaminedEvent>(OnExamined);
|
||||
// Private events stuff
|
||||
SubscribeLocalEvent<TargetVaxxSuccessfulEvent>(OnTargetVaxxSuccessful);
|
||||
SubscribeLocalEvent<VaxxCancelledEvent>(OnVaxxCancelled);
|
||||
}
|
||||
|
||||
private Queue<EntityUid> AddQueue = new();
|
||||
private Queue<(DiseaseCarrierComponent carrier, DiseasePrototype disease)> CureQueue = new();
|
||||
|
||||
/// <summary>
|
||||
/// First, adds or removes diseased component from the queues and clears them.
|
||||
/// Then, iterates over every diseased component to check for their effects
|
||||
/// and cures
|
||||
/// </summary>
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var entity in AddQueue)
|
||||
{
|
||||
EnsureComp<DiseasedComponent>(entity);
|
||||
}
|
||||
|
||||
AddQueue.Clear();
|
||||
|
||||
foreach (var tuple in CureQueue)
|
||||
{
|
||||
if (tuple.carrier.Diseases.Count == 1) //This is reliable unlike testing Count == 0 right after removal for reasons I don't quite get
|
||||
RemComp<DiseasedComponent>(tuple.carrier.Owner);
|
||||
tuple.carrier.PastDiseases.Add(tuple.disease);
|
||||
tuple.carrier.Diseases.Remove(tuple.disease);
|
||||
}
|
||||
CureQueue.Clear();
|
||||
|
||||
foreach (var (_, carrierComp, mobState) in EntityQuery<DiseasedComponent, DiseaseCarrierComponent, MobStateComponent>())
|
||||
{
|
||||
DebugTools.Assert(carrierComp.Diseases.Count > 0);
|
||||
|
||||
if (mobState.IsDead())
|
||||
{
|
||||
if (_random.Prob(0.005f * frameTime)) //Mean time to remove is 200 seconds per disease
|
||||
CureDisease(carrierComp, _random.Pick(carrierComp.Diseases));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var disease in carrierComp.Diseases)
|
||||
{
|
||||
var args = new DiseaseEffectArgs(carrierComp.Owner, disease, EntityManager);
|
||||
disease.Accumulator += frameTime;
|
||||
if (disease.Accumulator >= disease.TickTime)
|
||||
{
|
||||
disease.Accumulator -= disease.TickTime;
|
||||
foreach (var cure in disease.Cures)
|
||||
{
|
||||
if (cure.Cure(args))
|
||||
CureDisease(carrierComp, disease);
|
||||
}
|
||||
foreach (var effect in disease.Effects)
|
||||
{
|
||||
if (_random.Prob(effect.Probability))
|
||||
effect.Effect(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Event Handlers
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// Used when something is trying to cure ANY disease on the target,
|
||||
/// not for special disease interactions. Randomly
|
||||
/// tries to cure every disease on the target.
|
||||
/// </summary>
|
||||
private void OnTryCureDisease(EntityUid uid, DiseaseCarrierComponent component, CureDiseaseAttemptEvent args)
|
||||
{
|
||||
foreach (var disease in component.Diseases)
|
||||
{
|
||||
var cureProb = ((args.CureChance / component.Diseases.Count) - disease.CureResist);
|
||||
if (cureProb < 0)
|
||||
return;
|
||||
if (cureProb > 1)
|
||||
{
|
||||
CureDisease(component, disease);
|
||||
return;
|
||||
}
|
||||
if (_random.Prob(cureProb))
|
||||
{
|
||||
CureDisease(component, disease);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a component with disease protection
|
||||
/// is equipped so it can be added to the person's
|
||||
/// total disease resistance
|
||||
/// </summary>
|
||||
private void OnEquipped(EntityUid uid, DiseaseProtectionComponent component, GotEquippedEvent args)
|
||||
{
|
||||
// This only works on clothing
|
||||
if (!TryComp<ClothingComponent>(uid, out var clothing))
|
||||
return;
|
||||
// Is the clothing in its actual slot?
|
||||
if (!clothing.SlotFlags.HasFlag(args.SlotFlags))
|
||||
return;
|
||||
// Give the user the component's disease resist
|
||||
if(TryComp<DiseaseCarrierComponent>(args.Equipee, out var carrier))
|
||||
carrier.DiseaseResist += component.Protection;
|
||||
// Set the component to active to the unequip check isn't CBT
|
||||
component.IsActive = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a component with disease protection
|
||||
/// is unequipped so it can be removed from the person's
|
||||
/// total disease resistance
|
||||
/// </summary>
|
||||
private void OnUnequipped(EntityUid uid, DiseaseProtectionComponent component, GotUnequippedEvent args)
|
||||
{
|
||||
// Only undo the resistance if it was affecting the user
|
||||
if (!component.IsActive)
|
||||
return;
|
||||
if(TryComp<DiseaseCarrierComponent>(args.Equipee, out var carrier))
|
||||
carrier.DiseaseResist -= component.Protection;
|
||||
component.IsActive = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when it's already decided a disease will be cured
|
||||
/// so it can be safely queued up to be removed from the target
|
||||
/// and added to past disease history (for immunity)
|
||||
/// </summary>
|
||||
private void CureDisease(DiseaseCarrierComponent carrier, DiseasePrototype disease)
|
||||
{
|
||||
var CureTuple = (carrier, disease);
|
||||
CureQueue.Enqueue(CureTuple);
|
||||
_popupSystem.PopupEntity(Loc.GetString("disease-cured"), carrier.Owner, Filter.Entities(carrier.Owner));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when someone interacts with a diseased person with an empty hand
|
||||
/// to check if they get infected
|
||||
/// </summary>
|
||||
private void OnInteractDiseasedHand(EntityUid uid, DiseasedComponent component, InteractHandEvent args)
|
||||
{
|
||||
InteractWithDiseased(args.Target, args.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when someone interacts with a diseased person with any object
|
||||
/// to check if they get infected
|
||||
/// </summary>
|
||||
private void OnInteractDiseasedUsing(EntityUid uid, DiseasedComponent component, InteractUsingEvent args)
|
||||
{
|
||||
InteractWithDiseased(args.Target, args.User);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a vaccine is used on someone
|
||||
/// to handle the vaccination doafter
|
||||
/// </summary>
|
||||
private void OnAfterInteract(EntityUid uid, DiseaseVaccineComponent vaxx, AfterInteractEvent args)
|
||||
{
|
||||
if (vaxx.CancelToken != null)
|
||||
{
|
||||
vaxx.CancelToken.Cancel();
|
||||
vaxx.CancelToken = null;
|
||||
return;
|
||||
}
|
||||
if (args.Target == null)
|
||||
return;
|
||||
|
||||
if (!args.CanReach)
|
||||
return;
|
||||
|
||||
if (vaxx.CancelToken != null)
|
||||
return;
|
||||
|
||||
if (!TryComp<DiseaseCarrierComponent>(args.Target, out var carrier))
|
||||
return;
|
||||
|
||||
if (vaxx.Used)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("vaxx-already-used"), args.User, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
|
||||
vaxx.CancelToken = new CancellationTokenSource();
|
||||
_doAfterSystem.DoAfter(new DoAfterEventArgs(args.User, vaxx.InjectDelay, vaxx.CancelToken.Token, target: args.Target)
|
||||
{
|
||||
BroadcastFinishedEvent = new TargetVaxxSuccessfulEvent(args.User, args.Target, vaxx, carrier),
|
||||
BroadcastCancelledEvent = new VaxxCancelledEvent(vaxx),
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnStun = true,
|
||||
NeedHand = true
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a vaccine is examined.
|
||||
/// Currently doesn't do much because
|
||||
/// vaccines don't have unique art with a seperate
|
||||
/// state visualizer.
|
||||
/// </summary>
|
||||
private void OnExamined(EntityUid uid, DiseaseVaccineComponent vaxx, ExaminedEvent args)
|
||||
{
|
||||
if (args.IsInDetailsRange)
|
||||
{
|
||||
if (vaxx.Used)
|
||||
args.PushMarkup(Loc.GetString("vaxx-used"));
|
||||
else
|
||||
args.PushMarkup(Loc.GetString("vaxx-unused"));
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Helper functions
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// Tries to infect anyone that
|
||||
/// interacts with a diseased person or body
|
||||
/// </summary>
|
||||
private void InteractWithDiseased(EntityUid diseased, EntityUid target, DiseaseCarrierComponent? diseasedCarrier = null)
|
||||
{
|
||||
if (!Resolve(diseased, ref diseasedCarrier, false) ||
|
||||
diseasedCarrier.Diseases.Count == 0 ||
|
||||
!TryComp<DiseaseCarrierComponent>(target, out var carrier))
|
||||
return;
|
||||
|
||||
var disease = _random.Pick(diseasedCarrier.Diseases);
|
||||
TryInfect(carrier, disease, 0.4f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a disease to a target
|
||||
/// if it's not already in their current
|
||||
/// or past diseases. If you want this
|
||||
/// to not be guaranteed you are looking
|
||||
/// for TryInfect.
|
||||
/// </summary>
|
||||
public void TryAddDisease(EntityUid host, DiseasePrototype addedDisease, DiseaseCarrierComponent? target = null)
|
||||
{
|
||||
if (!Resolve(host, ref target, false))
|
||||
return;
|
||||
|
||||
foreach (var disease in target.AllDiseases)
|
||||
{
|
||||
if (disease.ID == addedDisease?.ID) //ID because of the way protoypes work
|
||||
return;
|
||||
}
|
||||
|
||||
var freshDisease = _serializationManager.CreateCopy(addedDisease);
|
||||
|
||||
if (freshDisease == null) return;
|
||||
|
||||
target.Diseases.Add(freshDisease);
|
||||
AddQueue.Enqueue(target.Owner);
|
||||
}
|
||||
|
||||
public void TryAddDisease(EntityUid host, string? addedDisease, DiseaseCarrierComponent? target = null)
|
||||
{
|
||||
if (addedDisease == null || !_prototypeManager.TryIndex<DiseasePrototype>(addedDisease, out var added))
|
||||
return;
|
||||
|
||||
TryAddDisease(host, added, target);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pits the infection chance against the
|
||||
/// person's disease resistance and
|
||||
/// rolls the dice to see if they get
|
||||
/// the disease.
|
||||
/// </summary>
|
||||
public void TryInfect(DiseaseCarrierComponent carrier, DiseasePrototype? disease, float chance = 0.7f)
|
||||
{
|
||||
if(disease is not { Infectious: true })
|
||||
return;
|
||||
var infectionChance = chance - carrier.DiseaseResist;
|
||||
if (infectionChance <= 0)
|
||||
return;
|
||||
if (_random.Prob(infectionChance))
|
||||
TryAddDisease(carrier.Owner, disease, carrier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Plays a sneeze/cough popup if applicable
|
||||
/// and then tries to infect anyone in range
|
||||
/// if the snougher is not wearing a mask.
|
||||
/// </summary>
|
||||
public void SneezeCough(EntityUid uid, DiseasePrototype? disease, string snoughMessage, bool airTransmit = true, TransformComponent? xform = null)
|
||||
{
|
||||
if (!Resolve(uid, ref xform)) return;
|
||||
|
||||
if (!string.IsNullOrEmpty(snoughMessage))
|
||||
_popupSystem.PopupEntity(Loc.GetString(snoughMessage, ("person", uid)), uid, Filter.Pvs(uid));
|
||||
|
||||
if (disease is not { Infectious: true } || !airTransmit)
|
||||
return;
|
||||
|
||||
if (_inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) &&
|
||||
EntityManager.TryGetComponent<IngestionBlockerComponent>(maskUid, out var blocker) &&
|
||||
blocker.Enabled)
|
||||
return;
|
||||
|
||||
var carrierQuery = GetEntityQuery<DiseaseCarrierComponent>();
|
||||
|
||||
foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, xform.WorldPosition, 2f))
|
||||
{
|
||||
if (!carrierQuery.TryGetComponent(entity, out var carrier) ||
|
||||
!_interactionSystem.InRangeUnobstructed(uid, entity)) continue;
|
||||
|
||||
TryInfect(carrier, disease, 0.3f);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a disease to the carrier's
|
||||
/// past diseases to give them immunity
|
||||
/// IF they don't already have the disease.
|
||||
/// </summary>
|
||||
public void Vaccinate(DiseaseCarrierComponent carrier, DiseasePrototype disease)
|
||||
{
|
||||
foreach (var currentDisease in carrier.Diseases)
|
||||
{
|
||||
if (currentDisease.ID == disease.ID) //ID because of the way protoypes work
|
||||
return;
|
||||
}
|
||||
carrier.PastDiseases.Add(disease);
|
||||
}
|
||||
|
||||
///
|
||||
/// Private Events Stuff
|
||||
///
|
||||
|
||||
/// <summary>
|
||||
/// Injects the vaccine into the target
|
||||
/// if the doafter is completed
|
||||
/// </summary>
|
||||
private void OnTargetVaxxSuccessful(TargetVaxxSuccessfulEvent args)
|
||||
{
|
||||
if (args.Vaxx.Disease == null)
|
||||
return;
|
||||
Vaccinate(args.Carrier, args.Vaxx.Disease);
|
||||
EntityManager.DeleteEntity(args.Vaxx.Owner);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the vaccine doafter
|
||||
/// </summary>
|
||||
private static void OnVaxxCancelled(VaxxCancelledEvent args)
|
||||
{
|
||||
args.Vaxx.CancelToken = null;
|
||||
}
|
||||
/// These two are standard doafter stuff you can ignore
|
||||
private sealed class VaxxCancelledEvent : EntityEventArgs
|
||||
{
|
||||
public readonly DiseaseVaccineComponent Vaxx;
|
||||
public VaxxCancelledEvent(DiseaseVaccineComponent vaxx)
|
||||
{
|
||||
Vaxx = vaxx;
|
||||
}
|
||||
}
|
||||
private sealed class TargetVaxxSuccessfulEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid User { get; }
|
||||
public EntityUid? Target { get; }
|
||||
public DiseaseVaccineComponent Vaxx { get; }
|
||||
public DiseaseCarrierComponent Carrier { get; }
|
||||
public TargetVaxxSuccessfulEvent(EntityUid user, EntityUid? target, DiseaseVaccineComponent vaxx, DiseaseCarrierComponent carrier)
|
||||
{
|
||||
User = user;
|
||||
Target = target;
|
||||
Vaxx = vaxx;
|
||||
Carrier = carrier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This event is fired by chems
|
||||
/// and other brute-force rather than
|
||||
/// specific cures. It will roll the dice to attempt
|
||||
/// to cure each disease on the target
|
||||
/// </summary>
|
||||
public sealed class CureDiseaseAttemptEvent : EntityEventArgs
|
||||
{
|
||||
public float CureChance { get; }
|
||||
public CureDiseaseAttemptEvent(float cureChance)
|
||||
{
|
||||
CureChance = cureChance;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Controls whether the snough is a sneeze, cough
|
||||
/// or neither. If none, will not create
|
||||
/// a popup. Mostly used for talking
|
||||
/// </summary>
|
||||
public enum SneezeCoughType
|
||||
{
|
||||
Sneeze,
|
||||
Cough,
|
||||
None
|
||||
}
|
||||
}
|
||||
46
Content.Server/Disease/Effects/DiseaseAdjustReagent.cs
Normal file
46
Content.Server/Disease/Effects/DiseaseAdjustReagent.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
using JetBrains.Annotations;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Shared.Disease;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Disease.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds or removes reagents from the
|
||||
/// host's chemstream.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class DiseaseAdjustReagent : DiseaseEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The reagent ID to add or remove.
|
||||
/// </summary>
|
||||
[DataField("reagent", customTypeSerializer:typeof(PrototypeIdSerializer<ReagentPrototype>))]
|
||||
public string? Reagent = null;
|
||||
|
||||
[DataField("amount", required: true)]
|
||||
public FixedPoint2 Amount = default!;
|
||||
|
||||
public override void Effect(DiseaseEffectArgs args)
|
||||
{
|
||||
if (!args.EntityManager.TryGetComponent<BloodstreamComponent>(args.DiseasedEntity, out var bloodstream))
|
||||
return;
|
||||
|
||||
var stream = bloodstream.ChemicalSolution;
|
||||
if (stream != null)
|
||||
{
|
||||
var solutionSys = args.EntityManager.EntitySysManager.GetEntitySystem<SolutionContainerSystem>();
|
||||
if (Reagent != null)
|
||||
{
|
||||
if (Amount < 0 && stream.ContainsReagent(Reagent))
|
||||
solutionSys.TryRemoveReagent(args.DiseasedEntity, stream, Reagent, -Amount);
|
||||
if (Amount > 0)
|
||||
solutionSys.TryAddReagent(args.DiseasedEntity, stream, Reagent, Amount, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
67
Content.Server/Disease/Effects/DiseaseGenericStatusEffect.cs
Normal file
67
Content.Server/Disease/Effects/DiseaseGenericStatusEffect.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
using Content.Shared.Disease;
|
||||
using Content.Shared.StatusEffect;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Disease.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a generic status effect to the entity.
|
||||
/// Differs from the chem version in its defaults
|
||||
/// to better facilitate adding components that
|
||||
/// last the length of the disease.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class DiseaseGenericStatusEffect : DiseaseEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// The status effect key
|
||||
/// Prevents other components from being with the same key
|
||||
/// </summary>
|
||||
[DataField("key", required: true)]
|
||||
public string Key = default!;
|
||||
/// <summary>
|
||||
/// The component to add
|
||||
/// </summary>
|
||||
[DataField("component")]
|
||||
public string Component = String.Empty;
|
||||
|
||||
[DataField("time")]
|
||||
public float Time = 1.01f; /// I'm afraid if this was exact the key could get stolen by another thing
|
||||
|
||||
/// <remarks>
|
||||
/// true - refresh status effect time, false - accumulate status effect time
|
||||
/// </remarks>
|
||||
[DataField("refresh")]
|
||||
public bool Refresh = false;
|
||||
|
||||
/// <summary>
|
||||
/// Should this effect add the status effect, remove time from it, or set its cooldown?
|
||||
/// </summary>
|
||||
[DataField("type")]
|
||||
public StatusEffectDiseaseType Type = StatusEffectDiseaseType.Add;
|
||||
|
||||
public override void Effect(DiseaseEffectArgs args)
|
||||
{
|
||||
var statusSys = EntitySystem.Get<StatusEffectsSystem>();
|
||||
if (Type == StatusEffectDiseaseType.Add && Component != String.Empty)
|
||||
{
|
||||
statusSys.TryAddStatusEffect(args.DiseasedEntity, Key, TimeSpan.FromSeconds(Time), Refresh, Component);
|
||||
}
|
||||
else if (Type == StatusEffectDiseaseType.Remove)
|
||||
{
|
||||
statusSys.TryRemoveTime(args.DiseasedEntity, Key, TimeSpan.FromSeconds(Time));
|
||||
}
|
||||
else if (Type == StatusEffectDiseaseType.Set)
|
||||
{
|
||||
statusSys.TrySetTime(args.DiseasedEntity, Key, TimeSpan.FromSeconds(Time));
|
||||
}
|
||||
}
|
||||
}
|
||||
/// See status effects for how these work
|
||||
public enum StatusEffectDiseaseType
|
||||
{
|
||||
Add,
|
||||
Remove,
|
||||
Set
|
||||
}
|
||||
}
|
||||
21
Content.Server/Disease/Effects/DiseaseHealthChange.cs
Normal file
21
Content.Server/Disease/Effects/DiseaseHealthChange.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Content.Shared.Disease;
|
||||
using Content.Shared.Damage;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Disease.Effects
|
||||
{
|
||||
/// <summary>
|
||||
/// Deals or heals damage to the host
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class DiseaseHealthChange : DiseaseEffect
|
||||
{
|
||||
[DataField("damage", required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DamageSpecifier Damage = default!;
|
||||
public override void Effect(DiseaseEffectArgs args)
|
||||
{
|
||||
EntitySystem.Get<DamageableSystem>().TryChangeDamage(args.DiseasedEntity, Damage, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Content.Server/Disease/Effects/DiseasePopUp.cs
Normal file
38
Content.Server/Disease/Effects/DiseasePopUp.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using Content.Shared.Disease;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Player;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Disease.Effects
|
||||
{
|
||||
[UsedImplicitly]
|
||||
/// <summary>
|
||||
/// Plays a popup on the host's transform.
|
||||
/// Supports passing the host's entity metadata
|
||||
/// in PVS ones with {$person}
|
||||
/// </summary>
|
||||
public sealed class DiseasePopUp : DiseaseEffect
|
||||
{
|
||||
[DataField("message")]
|
||||
public string Message = "disease-sick-generic";
|
||||
|
||||
[DataField("type")]
|
||||
public PopupType Type = PopupType.Local;
|
||||
public override void Effect(DiseaseEffectArgs args)
|
||||
{
|
||||
var popupSys = EntitySystem.Get<SharedPopupSystem>();
|
||||
|
||||
if (Type == PopupType.Local)
|
||||
popupSys.PopupEntity(Loc.GetString(Message), args.DiseasedEntity, Filter.Entities(args.DiseasedEntity));
|
||||
else if (Type == PopupType.Pvs)
|
||||
popupSys.PopupEntity(Loc.GetString(Message, ("person", args.DiseasedEntity)), args.DiseasedEntity, Filter.Pvs(args.DiseasedEntity));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum PopupType
|
||||
{
|
||||
Pvs,
|
||||
Local
|
||||
}
|
||||
}
|
||||
29
Content.Server/Disease/Effects/DiseaseSnough.cs
Normal file
29
Content.Server/Disease/Effects/DiseaseSnough.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using Content.Shared.Disease;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Disease
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes the diseased sneeze or cough
|
||||
/// or neither.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class DiseaseSnough : DiseaseEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// Message to play when snoughing
|
||||
/// </summary>
|
||||
[DataField("snoughMessage")]
|
||||
public string SnoughMessage = "disease-sneeze";
|
||||
/// <summary>
|
||||
/// Whether to spread the disease throught he air
|
||||
/// </summary>
|
||||
[DataField("airTransmit")]
|
||||
public bool AirTransmit = true;
|
||||
|
||||
public override void Effect(DiseaseEffectArgs args)
|
||||
{
|
||||
EntitySystem.Get<DiseaseSystem>().SneezeCough(args.DiseasedEntity, args.Disease, SnoughMessage, AirTransmit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System.Linq;
|
||||
using Content.Shared.Drone;
|
||||
using Content.Server.Drone.Components;
|
||||
using Content.Shared.Actions;
|
||||
|
|
@ -88,9 +89,16 @@ namespace Content.Server.Drone
|
|||
{
|
||||
var body = Comp<SharedBodyComponent>(uid); //There's no way something can have a mobstate but not a body...
|
||||
|
||||
foreach (var item in drone.ToolUids)
|
||||
foreach (var item in drone.ToolUids.Select((value, i) => ( value, i )))
|
||||
{
|
||||
EntityManager.DeleteEntity(item);
|
||||
if (_tagSystem.HasTag(item.value, "Drone"))
|
||||
{
|
||||
RemComp<UnremoveableComponent>(item.value);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntityManager.DeleteEntity(item.value);
|
||||
}
|
||||
}
|
||||
body.Gib();
|
||||
EntityManager.DeleteEntity(uid);
|
||||
|
|
@ -111,12 +119,12 @@ namespace Content.Server.Drone
|
|||
if (TryComp<HandsComponent>(uid, out var hands) && hands.Count >= drone.Tools.Count)
|
||||
{
|
||||
foreach (var entry in drone.Tools)
|
||||
{
|
||||
var item = EntityManager.SpawnEntity(entry.PrototypeId, spawnCoord);
|
||||
AddComp<UnremoveableComponent>(item);
|
||||
hands.PutInHand(item);
|
||||
drone.ToolUids.Add(item);
|
||||
}
|
||||
{
|
||||
var item = EntityManager.SpawnEntity(entry.PrototypeId, spawnCoord);
|
||||
AddComp<UnremoveableComponent>(item);
|
||||
hands.PutInHand(item);
|
||||
drone.ToolUids.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (TryComp<ActionsComponent>(uid, out var actions) && TryComp<UnpoweredFlashlightComponent>(uid, out var flashlight))
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ namespace Content.Server.Entry
|
|||
"ClientEntitySpawner",
|
||||
"CharacterInfo",
|
||||
"ItemCabinetVisuals",
|
||||
"DiseaseMachineVisuals",
|
||||
"HandheldGPS",
|
||||
"PotencyVisuals"
|
||||
};
|
||||
|
|
|
|||
54
Content.Server/Fluids/Components/AbsorbentComponent.cs
Normal file
54
Content.Server/Fluids/Components/AbsorbentComponent.cs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Sound;
|
||||
|
||||
namespace Content.Server.Fluids.Components;
|
||||
|
||||
/// <summary>
|
||||
/// For entities that can clean up puddles
|
||||
/// </summary>
|
||||
[RegisterComponent, Friend(typeof(MoppingSystem))]
|
||||
public sealed class AbsorbentComponent : Component
|
||||
{
|
||||
public const string SolutionName = "absorbed";
|
||||
|
||||
[DataField("pickupAmount")]
|
||||
public FixedPoint2 PickupAmount = FixedPoint2.New(10);
|
||||
|
||||
/// <summary>
|
||||
/// When using this tool on an empty floor tile, leave this much reagent as a new puddle.
|
||||
/// </summary>
|
||||
[DataField("residueAmount")]
|
||||
public FixedPoint2 ResidueAmount = FixedPoint2.New(10); // Should be higher than MopLowerLimit
|
||||
|
||||
/// <summary>
|
||||
/// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this amount.
|
||||
/// </summary>
|
||||
[DataField("mopLowerLimit")]
|
||||
public FixedPoint2 MopLowerLimit = FixedPoint2.New(5);
|
||||
|
||||
[DataField("pickupSound")]
|
||||
public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg");
|
||||
|
||||
[DataField("transferSound")]
|
||||
public SoundSpecifier TransferSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier for the do_after delay for how quickly the mopping happens.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("mopSpeed")] public float MopSpeed = 1;
|
||||
|
||||
/// <summary>
|
||||
/// How many entities can this tool interact with at once?
|
||||
/// </summary>
|
||||
[DataField("maxEntities")]
|
||||
public int MaxInteractingEntities = 1;
|
||||
|
||||
/// <summary>
|
||||
/// What entities is this tool interacting with right now?
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public HashSet<EntityUid> InteractingEntities = new();
|
||||
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Server.Fluids.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Can a mop click on this entity and dump its fluids
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class BucketComponent : Component, IInteractUsing
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
|
||||
public const string SolutionName = "bucket";
|
||||
|
||||
private List<EntityUid> _currentlyUsing = new();
|
||||
|
||||
public FixedPoint2 MaxVolume
|
||||
{
|
||||
get =>
|
||||
EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
|
||||
? solution.MaxVolume
|
||||
: FixedPoint2.Zero;
|
||||
set
|
||||
{
|
||||
if (EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution))
|
||||
{
|
||||
solution.MaxVolume = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public FixedPoint2 CurrentVolume => EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution)
|
||||
? solution.CurrentVolume
|
||||
: FixedPoint2.Zero;
|
||||
|
||||
[DataField("sound")]
|
||||
private SoundSpecifier _sound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg");
|
||||
|
||||
|
||||
async Task<bool> IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs)
|
||||
{
|
||||
var solutionsSys = EntitySystem.Get<SolutionContainerSystem>();
|
||||
if (!solutionsSys.TryGetSolution(Owner, SolutionName, out var contents) ||
|
||||
_currentlyUsing.Contains(eventArgs.Using) ||
|
||||
!_entMan.TryGetComponent(eventArgs.Using, out MopComponent? mopComponent) ||
|
||||
mopComponent.Mopping)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CurrentVolume <= 0)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("bucket-component-bucket-is-empty-message"));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
_currentlyUsing.Add(eventArgs.Using);
|
||||
|
||||
// IMO let em move while doing it.
|
||||
var doAfterArgs = new DoAfterEventArgs(eventArgs.User, 1.0f, target: eventArgs.Target)
|
||||
{
|
||||
BreakOnStun = true,
|
||||
BreakOnDamage = true,
|
||||
};
|
||||
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterArgs);
|
||||
|
||||
_currentlyUsing.Remove(eventArgs.Using);
|
||||
|
||||
if (result == DoAfterStatus.Cancelled || _entMan.Deleted(Owner) || mopComponent.Deleted ||
|
||||
CurrentVolume <= 0 || !EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(Owner, mopComponent.Owner))
|
||||
return false;
|
||||
|
||||
//Checks if the mop is empty
|
||||
if(mopComponent.CurrentVolume == 0)
|
||||
{
|
||||
// Transfers up to half the mop's available capacity to the mop
|
||||
// Takes the lower of the mop's available volume and the bucket's current volume.
|
||||
var transferAmount = FixedPoint2.Min(0.5*mopComponent.AvailableVolume, CurrentVolume);
|
||||
if (transferAmount == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var mopContents = mopComponent.MopSolution;
|
||||
|
||||
if (mopContents == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Transfer solution from the bucket to the mop
|
||||
// Owner is the bucket being interacted with. contents is the Solution contained by said bucket.
|
||||
var solution = solutionsSys.SplitSolution(Owner, contents, transferAmount);
|
||||
if (!solutionsSys.TryAddSolution(mopComponent.Owner, mopComponent.MopSolution, solution))
|
||||
{
|
||||
return false; //if the attempt fails
|
||||
}
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("bucket-component-mop-is-now-wet-message"));
|
||||
|
||||
}
|
||||
else //if mop is not empty
|
||||
{
|
||||
//Transfer the mop solution to the bucket
|
||||
|
||||
if (mopComponent.MopSolution == null)
|
||||
return false;
|
||||
|
||||
var solutionFromMop = solutionsSys.SplitSolution(mopComponent.Owner, mopComponent.MopSolution, mopComponent.CurrentVolume);
|
||||
EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution);
|
||||
if (!solutionsSys.TryAddSolution(Owner, solution, solutionFromMop))
|
||||
{
|
||||
return false; //if the attempt fails
|
||||
}
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("bucket-component-mop-is-now-dry-message"));
|
||||
|
||||
}
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(Owner), _sound.GetSound(), Owner);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
using System.Threading.Tasks;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Helpers;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
namespace Content.Server.Fluids.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// For cleaning up puddles
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class MopComponent : Component, IAfterInteract
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entities = default!;
|
||||
|
||||
public const string SolutionName = "mop";
|
||||
|
||||
/// <summary>
|
||||
/// Used to prevent do_after spam if we're currently mopping.
|
||||
/// </summary>
|
||||
public bool Mopping { get; private set; }
|
||||
|
||||
// MopSolution Object stores whatever solution the mop has absorbed.
|
||||
public Solution? MopSolution
|
||||
{
|
||||
get
|
||||
{
|
||||
EntitySystem.Get<SolutionContainerSystem>().TryGetSolution(Owner, SolutionName, out var solution);
|
||||
return solution;
|
||||
}
|
||||
}
|
||||
|
||||
// MaxVolume is the Maximum volume the mop can absorb (however, this is defined in janitor.yml)
|
||||
public FixedPoint2 MaxVolume
|
||||
{
|
||||
get => MopSolution?.MaxVolume ?? FixedPoint2.Zero;
|
||||
set
|
||||
{
|
||||
var solution = MopSolution;
|
||||
if (solution != null)
|
||||
{
|
||||
solution.MaxVolume = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CurrentVolume is the volume the mop has absorbed.
|
||||
public FixedPoint2 CurrentVolume => MopSolution?.CurrentVolume ?? FixedPoint2.Zero;
|
||||
|
||||
// AvailableVolume is the remaining volume capacity of the mop.
|
||||
public FixedPoint2 AvailableVolume => MopSolution?.AvailableVolume ?? FixedPoint2.Zero;
|
||||
|
||||
// Currently there's a separate amount for pickup and dropoff so
|
||||
// Picking up a puddle requires multiple clicks
|
||||
// Dumping in a bucket requires 1 click
|
||||
// Long-term you'd probably use a cooldown and start the pickup once we have some form of global cooldown
|
||||
[DataField("pickup_amount")]
|
||||
public FixedPoint2 PickupAmount { get; } = FixedPoint2.New(10);
|
||||
|
||||
/// <summary>
|
||||
/// When using the mop on an empty floor tile, leave this much reagent as a new puddle.
|
||||
/// </summary>
|
||||
[DataField("residueAmount")]
|
||||
public FixedPoint2 ResidueAmount { get; } = FixedPoint2.New(10); // Should be higher than MopLowerLimit
|
||||
|
||||
/// <summary>
|
||||
/// To leave behind a wet floor, the mop will be unable to take from puddles with a volume less than this amount.
|
||||
/// </summary>
|
||||
[DataField("mopLowerLimit")]
|
||||
public FixedPoint2 MopLowerLimit { get; } = FixedPoint2.New(5);
|
||||
|
||||
[DataField("pickup_sound")]
|
||||
private SoundSpecifier _pickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier for the do_after delay for how fast the mop works.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
[DataField("speed")] private float _mopSpeed = 1;
|
||||
|
||||
async Task<bool> IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs)
|
||||
{
|
||||
/*
|
||||
* Functionality:
|
||||
* Essentially if we click on an empty tile spill our contents there
|
||||
* Otherwise, try to mop up the puddle (if it is a puddle).
|
||||
* It will try to destroy solution on the mop to do so, and if it is successful
|
||||
* will spill some of the mop's solution onto the puddle which will evaporate eventually.
|
||||
*/
|
||||
var solutionSystem = EntitySystem.Get<SolutionContainerSystem>();
|
||||
var spillableSystem = EntitySystem.Get<SpillableSystem>();
|
||||
|
||||
if (!eventArgs.CanReach ||
|
||||
!solutionSystem.TryGetSolution(Owner, SolutionName, out var contents ) ||
|
||||
Mopping)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (eventArgs.Target is not {Valid: true} target)
|
||||
{
|
||||
// Drop the liquid on the mop on to the ground
|
||||
var solution = solutionSystem.SplitSolution(Owner, contents, FixedPoint2.Min(ResidueAmount, CurrentVolume));
|
||||
spillableSystem.SpillAt(solution, eventArgs.ClickLocation, "PuddleSmear");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!_entities.TryGetComponent(target, out PuddleComponent? puddleComponent) ||
|
||||
!solutionSystem.TryGetSolution((puddleComponent).Owner, puddleComponent.SolutionName, out var puddleSolution))
|
||||
return false;
|
||||
|
||||
// if the puddle is too small for the mop to effectively take any more solution
|
||||
if (puddleSolution.TotalVolume <= MopLowerLimit)
|
||||
{
|
||||
// Transfers solution from the mop to the puddle
|
||||
solutionSystem.TryAddSolution(target, puddleSolution, solutionSystem.SplitSolution(Owner, contents, FixedPoint2.Min(ResidueAmount,CurrentVolume)));
|
||||
return true;
|
||||
}
|
||||
|
||||
// if the mop is full
|
||||
if(AvailableVolume <= 0)
|
||||
{
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("mop-component-mop-is-full-message"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mopping duration (aka delay) should scale with PickupAmount and not puddle volume, because we are picking up a constant volume of solution with each click.
|
||||
var doAfterArgs = new DoAfterEventArgs(eventArgs.User, _mopSpeed * PickupAmount.Float() / 10.0f,
|
||||
target: target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnDamage = true,
|
||||
};
|
||||
Mopping = true;
|
||||
var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterArgs);
|
||||
Mopping = false;
|
||||
|
||||
if (result == DoAfterStatus.Cancelled ||
|
||||
_entities.Deleted(Owner) ||
|
||||
puddleComponent.Deleted)
|
||||
return false;
|
||||
|
||||
// The volume the mop will take from the puddle
|
||||
FixedPoint2 transferAmount;
|
||||
// does the puddle actually have reagents? it might not if its a weird cosmetic entity.
|
||||
if (puddleSolution.TotalVolume == 0)
|
||||
transferAmount = FixedPoint2.Min(PickupAmount, AvailableVolume);
|
||||
else
|
||||
{
|
||||
transferAmount = FixedPoint2.Min(PickupAmount, puddleSolution.TotalVolume, AvailableVolume);
|
||||
|
||||
if ((puddleSolution.TotalVolume - transferAmount) < MopLowerLimit) // If the transferAmount would bring the puddle below the MopLowerLimit
|
||||
transferAmount = puddleSolution.TotalVolume - MopLowerLimit; // Then the transferAmount should bring the puddle down to the MopLowerLimit exactly
|
||||
}
|
||||
|
||||
// Transfers solution from the puddle to the mop
|
||||
solutionSystem.TryAddSolution(Owner, contents, solutionSystem.SplitSolution(target, puddleSolution, transferAmount));
|
||||
|
||||
SoundSystem.Play(Filter.Pvs(Owner), _pickupSound.GetSound(), Owner);
|
||||
|
||||
// if the mop became full after that puddle, let the player know.
|
||||
if(AvailableVolume <= 0)
|
||||
Owner.PopupMessage(eventArgs.User, Loc.GetString("mop-component-mop-is-now-full-message"));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
361
Content.Server/Fluids/EntitySystems/MoppingSystem.cs
Normal file
361
Content.Server/Fluids/EntitySystems/MoppingSystem.cs
Normal file
|
|
@ -0,0 +1,361 @@
|
|||
using Content.Server.Chemistry.Components.SolutionManager;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Sound;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Map;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class MoppingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SpillableSystem _spillableSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
|
||||
|
||||
const string puddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<AbsorbentComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<TransferCancelledEvent>(OnTransferCancelled);
|
||||
SubscribeLocalEvent<TransferCompleteEvent>(OnTransferComplete);
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach) // if user cannot reach the target
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Handled) // if the event was already handled
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorbedSolution);
|
||||
|
||||
if (absorbedSolution is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var toolAvailableVolume = absorbedSolution.AvailableVolume;
|
||||
var toolCurrentVolume = absorbedSolution.CurrentVolume;
|
||||
|
||||
// For adding liquid to an empty floor tile
|
||||
if (args.Target is null) // if a tile is clicked
|
||||
{
|
||||
ReleaseToFloor(args.ClickLocation, component, absorbedSolution);
|
||||
args.Handled = true;
|
||||
args.User.PopupMessage(args.User, Loc.GetString("mopping-system-release-to-floor"));
|
||||
return;
|
||||
}
|
||||
else if (args.Target is not null)
|
||||
{
|
||||
// Handle our do_after logic
|
||||
HandleDoAfter(args.User, args.Used, args.Target.Value, component, toolCurrentVolume, toolAvailableVolume);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
private void ReleaseToFloor(EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution? absorbedSolution)
|
||||
{
|
||||
if ((_mapManager.TryGetGrid(clickLocation.GetGridId(EntityManager), out var mapGrid)) // needs valid grid
|
||||
&& absorbedSolution is not null) // needs a solution to place on the tile
|
||||
{
|
||||
TileRef tile = mapGrid.GetTileRef(clickLocation);
|
||||
|
||||
// Drop some of the absorbed liquid onto the ground
|
||||
var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorbedSolution.CurrentVolume); // The release amount specified on the absorbent component, or the amount currently absorbed (whichever is less).
|
||||
var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorbedSolution, releaseAmount); // Remove releaseAmount of solution from the absorbent component
|
||||
_spillableSystem.SpillAt(tile, releasedSolution, puddlePrototypeId); // And spill it onto the tile.
|
||||
}
|
||||
}
|
||||
|
||||
// Handles logic for our different types of valid target.
|
||||
// Checks for conditions that would prevent a doAfter from starting.
|
||||
private void HandleDoAfter(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, FixedPoint2 currentVolume, FixedPoint2 availableVolume)
|
||||
{
|
||||
// Below variables will be set within this function depending on what kind of target was clicked.
|
||||
// They will be passed to the OnTransferComplete if the doAfter succeeds.
|
||||
|
||||
EntityUid donor;
|
||||
EntityUid acceptor;
|
||||
string donorSolutionName;
|
||||
string acceptorSolutionName;
|
||||
|
||||
FixedPoint2 transferAmount;
|
||||
|
||||
var delay = 1.0f; //default do_after delay in seconds.
|
||||
string msg;
|
||||
SoundSpecifier sfx;
|
||||
|
||||
// For our purposes, if our target has a PuddleComponent, treat it as a puddle above all else.
|
||||
if (TryComp<PuddleComponent>(target, out var puddle))
|
||||
{
|
||||
// These return conditions will abort BEFORE the do_after is called:
|
||||
if(!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) // puddle Solution is null
|
||||
|| (puddleSolution.TotalVolume <= 0)) // puddle is completely empty
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (availableVolume < 0) // mop is completely full
|
||||
{
|
||||
msg = "mopping-system-tool-full";
|
||||
user.PopupMessage(user, Loc.GetString(msg, ("used", used))); // play message now because we are aborting.
|
||||
return;
|
||||
}
|
||||
// adding to puddles
|
||||
else if (puddleSolution.TotalVolume < component.MopLowerLimit // if the puddle is too small for the tool to effectively absorb any more solution from it
|
||||
&& currentVolume > 0) // tool needs a solution to dilute the puddle with.
|
||||
{
|
||||
// Dilutes the puddle with some solution from the tool
|
||||
transferAmount = FixedPoint2.Max(component.ResidueAmount, currentVolume);
|
||||
TryTransfer(used, target, "absorbed", puddle.SolutionName, transferAmount); // Complete the transfer right away, with no doAfter.
|
||||
|
||||
sfx = component.TransferSound;
|
||||
SoundSystem.Play(Filter.Pvs(user), sfx.GetSound(), used); // Give instant feedback for diluting puddle, so that it's clear that the player is adding to the puddle (as opposed to other behaviours, which have a doAfter).
|
||||
|
||||
msg = "mopping-system-puddle-diluted";
|
||||
user.PopupMessage(user, Loc.GetString(msg)); // play message now because we are aborting.
|
||||
|
||||
return; // Do not begin a doAfter.
|
||||
}
|
||||
else
|
||||
{
|
||||
// Taking from puddles:
|
||||
|
||||
// Determine transferAmount:
|
||||
transferAmount = FixedPoint2.Min(component.PickupAmount, puddleSolution.TotalVolume, availableVolume);
|
||||
|
||||
// TODO: consider onelining this with the above, using additional args on Min()?
|
||||
if ((puddleSolution.TotalVolume - transferAmount) < component.MopLowerLimit) // If the transferAmount would bring the puddle below the MopLowerLimit
|
||||
{
|
||||
transferAmount = puddleSolution.TotalVolume - component.MopLowerLimit; // Then the transferAmount should bring the puddle down to the MopLowerLimit exactly
|
||||
}
|
||||
|
||||
donor = target; // the puddle Uid
|
||||
donorSolutionName = puddle.SolutionName;
|
||||
|
||||
acceptor = used; // the mop/tool Uid
|
||||
acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent
|
||||
|
||||
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
|
||||
delay = (component.PickupAmount.Float() / 10.0f) * component.MopSpeed; // Delay should scale with PickupAmount, which represents the maximum we can pick up per click.
|
||||
msg = "mopping-system-puddle-success";
|
||||
sfx = component.PickupSound;
|
||||
|
||||
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
|
||||
}
|
||||
}
|
||||
else if ((TryComp<RefillableSolutionComponent>(target, out var refillable)) // We can put solution from the tool into the target
|
||||
&& (currentVolume > 0)) // And the tool is wet
|
||||
{
|
||||
// These return conditions will abort BEFORE the do_after is called:
|
||||
if (!_solutionSystem.TryGetRefillableSolution(target, out var refillableSolution)) // refillable Solution is null
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (refillableSolution.AvailableVolume <= 0) // target container is full (liquid destination)
|
||||
{
|
||||
msg = "mopping-system-target-container-full";
|
||||
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting.
|
||||
return;
|
||||
}
|
||||
else if (refillableSolution.MaxVolume <= FixedPoint2.New(20)) // target container is too small (e.g. syringe)
|
||||
{
|
||||
msg = "mopping-system-target-container-too-small";
|
||||
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting.
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Determine transferAmount
|
||||
if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.)
|
||||
&& !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop
|
||||
{
|
||||
delay = 5.0f; // Should take much longer if you don't have a wringer
|
||||
|
||||
if ((currentVolume / (currentVolume + availableVolume) ) > 0.25) // mop is more than one-quarter full
|
||||
{
|
||||
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume * 0.6); // squeeze up to 60% of the solution from the mop.
|
||||
msg = "mopping-system-hand-squeeze-little-wet";
|
||||
|
||||
if ((currentVolume / (currentVolume + availableVolume) ) > 0.5) // if the mop is more than half full
|
||||
msg = "mopping-system-hand-squeeze-still-wet"; // overwrites the above
|
||||
|
||||
}
|
||||
else // mop is less than one-quarter full
|
||||
{
|
||||
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); // squeeze remainder of solution from the mop.
|
||||
msg = "mopping-system-hand-squeeze-dry";
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); //Transfer all liquid from the tool to the container, but only if it will fit.
|
||||
msg = "mopping-system-refillable-success";
|
||||
delay = 1.0f;
|
||||
}
|
||||
|
||||
donor = used; // the mop/tool Uid
|
||||
donorSolutionName = "absorbed"; // by definition on AbsorbentComponent
|
||||
|
||||
acceptor = target; // the refillable container's Uid
|
||||
acceptorSolutionName = refillable.Solution;
|
||||
|
||||
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
|
||||
|
||||
sfx = component.TransferSound;
|
||||
|
||||
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
|
||||
}
|
||||
}
|
||||
else if (TryComp<DrainableSolutionComponent>(target, out var drainable) // We can take solution from the target
|
||||
&& currentVolume <= 0 ) // tool is dry
|
||||
{
|
||||
// These return conditions will abort BEFORE the do_after is called:
|
||||
if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (drainableSolution.CurrentVolume <= 0) // target container is empty (liquid source)
|
||||
{
|
||||
msg = "mopping-system-target-container-empty";
|
||||
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are returning.
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Determine transferAmount
|
||||
transferAmount = FixedPoint2.Min(availableVolume * 0.5, drainableSolution.CurrentVolume); // Let's transfer up to to half the tool's available capacity to the tool.
|
||||
|
||||
donor = target; // the drainable container's Uid
|
||||
donorSolutionName = drainable.Solution;
|
||||
|
||||
acceptor = used; // the mop/tool Uid
|
||||
acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent
|
||||
|
||||
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
|
||||
// default delay is fine for this case.
|
||||
msg = "mopping-system-drainable-success";
|
||||
sfx = component.TransferSound;
|
||||
|
||||
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, EntityUid donor, EntityUid acceptor,
|
||||
AbsorbentComponent component, string donorSolutionName, string acceptorSolutionName,
|
||||
FixedPoint2 transferAmount, float delay, string msg, SoundSpecifier sfx)
|
||||
{
|
||||
var doAfterArgs = new DoAfterEventArgs(user, delay, target: target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.2f,
|
||||
BroadcastCancelledEvent = new TransferCancelledEvent()
|
||||
{
|
||||
Target = target,
|
||||
Component = component // (the AbsorbentComponent)
|
||||
},
|
||||
BroadcastFinishedEvent = new TransferCompleteEvent()
|
||||
{
|
||||
User = user,
|
||||
Tool = used,
|
||||
Target = target,
|
||||
Donor = donor,
|
||||
Acceptor = acceptor,
|
||||
Component = component,
|
||||
DonorSolutionName = donorSolutionName,
|
||||
AcceptorSolutionName = acceptorSolutionName,
|
||||
Message = msg,
|
||||
Sound = sfx,
|
||||
TransferAmount = transferAmount
|
||||
}
|
||||
};
|
||||
|
||||
// Can't interact with too many entities at once.
|
||||
if (component.MaxInteractingEntities < component.InteractingEntities.Count + 1)
|
||||
return;
|
||||
|
||||
// Can't interact with the same container multiple times at once.
|
||||
if (!component.InteractingEntities.Add(target))
|
||||
return;
|
||||
|
||||
var result = _doAfterSystem.WaitDoAfter(doAfterArgs);
|
||||
}
|
||||
|
||||
private void OnTransferComplete(TransferCompleteEvent ev)
|
||||
{
|
||||
SoundSystem.Play(Filter.Pvs(ev.User), ev.Sound.GetSound(), ev.Tool); // Play the After SFX
|
||||
|
||||
ev.User.PopupMessage(ev.User, Loc.GetString(ev.Message, ("target", ev.Target), ("used", ev.Tool))); // Play the After popup message
|
||||
|
||||
TryTransfer(ev.Donor, ev.Acceptor, ev.DonorSolutionName, ev.AcceptorSolutionName, ev.TransferAmount);
|
||||
|
||||
ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target.
|
||||
return;
|
||||
}
|
||||
|
||||
private void OnTransferCancelled(TransferCancelledEvent ev)
|
||||
{
|
||||
if (!ev.Component.Deleted)
|
||||
ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private void TryTransfer(EntityUid donor, EntityUid acceptor, string donorSolutionName, string acceptorSolutionName, FixedPoint2 transferAmount)
|
||||
{
|
||||
if (_solutionSystem.TryGetSolution(donor, donorSolutionName, out var donorSolution) // If the donor solution is valid
|
||||
&& _solutionSystem.TryGetSolution(acceptor, acceptorSolutionName, out var acceptorSolution)) // And the acceptor solution is valid
|
||||
{
|
||||
var solutionToTransfer = _solutionSystem.SplitSolution(donor, donorSolution, transferAmount); // Split a portion of the donor solution
|
||||
_solutionSystem.TryAddSolution(acceptor, acceptorSolution, solutionToTransfer); // And add it to the acceptor solution
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public sealed class TransferCompleteEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid User;
|
||||
public EntityUid Tool;
|
||||
public EntityUid Target;
|
||||
public EntityUid Donor;
|
||||
public EntityUid Acceptor;
|
||||
public AbsorbentComponent Component { get; init; } = default!;
|
||||
public string DonorSolutionName = "";
|
||||
public string AcceptorSolutionName = "";
|
||||
public string Message = "";
|
||||
public SoundSpecifier Sound { get; init; } = default!;
|
||||
public FixedPoint2 TransferAmount;
|
||||
|
||||
}
|
||||
|
||||
public sealed class TransferCancelledEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Target;
|
||||
public AbsorbentComponent Component { get; init; } = default!;
|
||||
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ namespace Content.Server.GameTicking
|
|||
private TickerLobbyStatusEvent GetStatusMsg(IPlayerSession session)
|
||||
{
|
||||
_playersInLobby.TryGetValue(session, out var status);
|
||||
return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbySong, status == LobbyPlayerStatus.Ready, _roundStartTime, Paused);
|
||||
return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbySong, LobbyBackground,status == LobbyPlayerStatus.Ready, _roundStartTime, Paused);
|
||||
}
|
||||
|
||||
private void SendStatusToAll()
|
||||
|
|
|
|||
33
Content.Server/GameTicking/GameTicker.LobbyBackground.cs
Normal file
33
Content.Server/GameTicking/GameTicker.LobbyBackground.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Shared.Audio;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.ViewVariables;
|
||||
|
||||
|
||||
namespace Content.Server.GameTicking;
|
||||
|
||||
public sealed partial class GameTicker
|
||||
{
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
|
||||
[ViewVariables]
|
||||
public string? LobbyBackground { get; private set; }
|
||||
|
||||
[ViewVariables]
|
||||
private List<ResourcePath>? _lobbyBackgrounds;
|
||||
|
||||
private static readonly string[] WhitelistedBackgroundExtensions = new string[] {"png", "jpg", "jpeg"};
|
||||
|
||||
private const string LobbyScreenPath = "/Textures/LobbyScreens";
|
||||
|
||||
private void InitializeLobbyBackground()
|
||||
{
|
||||
_lobbyBackgrounds = _resourceManager.ContentFindFiles(new ResourcePath(LobbyScreenPath)).ToList();
|
||||
_lobbyBackgrounds = _lobbyBackgrounds.FindAll(path => WhitelistedBackgroundExtensions.Contains((path.Extension)));
|
||||
LobbyBackground = _lobbyBackgrounds.Any() ? _robustRandom.Pick(_lobbyBackgrounds).ToString() : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -50,6 +50,7 @@ namespace Content.Server.GameTicking
|
|||
InitializeCVars();
|
||||
InitializePlayer();
|
||||
InitializeLobbyMusic();
|
||||
InitializeLobbyBackground();
|
||||
InitializeGamePreset();
|
||||
InitializeJobController();
|
||||
InitializeUpdates();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
using System.Threading;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.MedicalScanner;
|
||||
using Content.Shared.Disease;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Medical.Components
|
||||
{
|
||||
|
|
@ -23,5 +25,12 @@ namespace Content.Server.Medical.Components
|
|||
/// </summary>
|
||||
public CancellationTokenSource? CancelToken;
|
||||
public BoundUserInterface? UserInterface => Owner.GetUIOrNull(HealthAnalyzerUiKey.Key);
|
||||
|
||||
/// <summary>
|
||||
/// The disease this will give people.
|
||||
/// </summary>
|
||||
[DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string? Disease;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
using System.Threading;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Medical.Components;
|
||||
using Content.Server.Disease;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
using static Content.Shared.MedicalScanner.SharedHealthAnalyzerComponent;
|
||||
|
||||
namespace Content.Server.Medical
|
||||
{
|
||||
public sealed class HealthAnalyzerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DiseaseSystem _disease = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
|
|
@ -64,6 +69,22 @@ namespace Content.Server.Medical
|
|||
{
|
||||
args.Component.CancelToken = null;
|
||||
UpdateScannedUser(args.Component.Owner, args.User, args.Target, args.Component);
|
||||
// Below is for the traitor item
|
||||
// Piggybacking off another component's doafter is complete CBT so I gave up
|
||||
// and put it on the same component
|
||||
if (string.IsNullOrEmpty(args.Component.Disease) || args.Target == null)
|
||||
return;
|
||||
|
||||
_disease.TryAddDisease(args.Target.Value, args.Component.Disease);
|
||||
|
||||
if (args.User == args.Target)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("disease-scanner-gave-self", ("disease", args.Component.Disease)),
|
||||
args.User, Filter.Entities(args.User));
|
||||
return;
|
||||
}
|
||||
_popupSystem.PopupEntity(Loc.GetString("disease-scanner-gave-other", ("target", args.Target), ("disease", args.Component.Disease)),
|
||||
args.User, Filter.Entities(args.User));
|
||||
}
|
||||
|
||||
private void OpenUserInterface(EntityUid user, HealthAnalyzerComponent healthAnalyzer)
|
||||
|
|
|
|||
|
|
@ -59,10 +59,9 @@ namespace Content.Server.Medical
|
|||
|
||||
private void OnActivated(EntityUid uid, MedicalScannerComponent scannerComponent, ActivateInWorldEvent args)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(args.User, out var actor) || !IsPowered(scannerComponent))
|
||||
if (!IsPowered(scannerComponent))
|
||||
return;
|
||||
|
||||
scannerComponent.UserInterface?.Toggle(actor.PlayerSession);
|
||||
UpdateUserInterface(uid, scannerComponent);
|
||||
}
|
||||
|
||||
|
|
|
|||
64
Content.Server/StationEvents/Events/DiseaseOutbreak.cs
Normal file
64
Content.Server/StationEvents/Events/DiseaseOutbreak.cs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
using System.Linq;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Disease.Components;
|
||||
using Content.Server.Disease;
|
||||
using Content.Shared.Disease;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.StationEvents.Events;
|
||||
/// <summary>
|
||||
/// Infects a couple people
|
||||
/// with a random disease that isn't super deadly
|
||||
/// </summary>
|
||||
public sealed class DiseaseOutbreak : StationEvent
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Disease prototypes I decided were not too deadly for a random event
|
||||
/// </summary>
|
||||
public readonly IReadOnlyList<string> NotTooSeriousDiseases = new[]
|
||||
{
|
||||
"SpaceCold",
|
||||
"VanAusdallsRobovirus",
|
||||
"VentCough",
|
||||
"AMIV"
|
||||
};
|
||||
public override string Name => "DiseaseOutbreak";
|
||||
public override float Weight => WeightNormal;
|
||||
protected override float EndAfter => 1.0f;
|
||||
/// <summary>
|
||||
/// Finds 2-5 random entities that can host diseases
|
||||
/// and gives them a randomly selected disease.
|
||||
/// They all get the same disease.
|
||||
/// </summary>
|
||||
public override void Startup()
|
||||
{
|
||||
base.Startup();
|
||||
|
||||
var targetList = _entityManager.EntityQuery<DiseaseCarrierComponent>().ToList();
|
||||
_random.Shuffle(targetList);
|
||||
|
||||
var toInfect = _random.Next(2, 5);
|
||||
|
||||
var diseaseName = _random.Pick(NotTooSeriousDiseases);
|
||||
|
||||
if (!_prototypeManager.TryIndex(diseaseName, out DiseasePrototype? disease))
|
||||
return;
|
||||
|
||||
var diseaseSystem = EntitySystem.Get<DiseaseSystem>();
|
||||
|
||||
foreach (var target in targetList)
|
||||
{
|
||||
if (toInfect-- == 0)
|
||||
break;
|
||||
|
||||
diseaseSystem.TryAddDisease(target.Owner, disease, target);
|
||||
}
|
||||
_chatManager.DispatchStationAnnouncement(Loc.GetString("station-event-disease-outbreak-announcement"));
|
||||
}
|
||||
}
|
||||
|
|
@ -42,7 +42,7 @@ public sealed class VentClog : StationEvent
|
|||
public readonly IReadOnlyList<string> SafeishVentChemicals = new[]
|
||||
{
|
||||
"Water", "Iron", "Oxygen", "Tritium", "Plasma", "SulfuricAcid", "Blood", "SpaceDrugs", "SpaceCleaner", "Flour",
|
||||
"Nutriment", "Sugar", "SpaceLube", "Ethanol", "Mercury", "Ephedrine", "WeldingFuel"
|
||||
"Nutriment", "Sugar", "SpaceLube", "Ethanol", "Mercury", "Ephedrine", "WeldingFuel", "VentCrud"
|
||||
};
|
||||
|
||||
public override void Startup()
|
||||
|
|
|
|||
|
|
@ -33,6 +33,22 @@ namespace Content.Server.UserInterface
|
|||
[DataField("verbText")]
|
||||
public string VerbText = "ui-verb-toggle-open";
|
||||
|
||||
/// <summary>
|
||||
/// Whether you need a hand to operate this UI. The hand does not need to be free, you just need to have one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should probably be true for most machines & computers, but there will still be UIs that represent a
|
||||
/// more generic interaction / configuration that might not require hands.
|
||||
/// </remarks>
|
||||
[DataField("requireHands")]
|
||||
public bool RequireHands = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether spectators (non-admin ghosts) should be allowed to view this UI.
|
||||
/// </summary>
|
||||
[DataField("allowSpectator")]
|
||||
public bool AllowSpectator = true;
|
||||
|
||||
/// <summary>
|
||||
/// The client channel currently using the object, or null if there's none/not single user.
|
||||
/// NOTE: DO NOT DIRECTLY SET, USE ActivatableUISystem.SetCurrentSingleUser
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Verbs;
|
||||
|
|
@ -28,12 +27,25 @@ namespace Content.Server.UserInterface
|
|||
// *THIS IS A BLATANT WORKAROUND!* RATIONALE: Microwaves need it
|
||||
SubscribeLocalEvent<ActivatableUIComponent, EntParentChangedMessage>(OnParentChanged);
|
||||
SubscribeLocalEvent<ActivatableUIComponent, BoundUIClosedEvent>(OnUIClose);
|
||||
SubscribeLocalEvent<BoundUserInterfaceMessageAttempt>(OnBoundInterfaceInteractAttempt);
|
||||
|
||||
SubscribeLocalEvent<ActivatableUIComponent, GetVerbsEvent<ActivationVerb>>(AddOpenUiVerb);
|
||||
|
||||
SubscribeLocalEvent<ServerUserInterfaceComponent, OpenUiActionEvent>(OnActionPerform);
|
||||
}
|
||||
|
||||
private void OnBoundInterfaceInteractAttempt(BoundUserInterfaceMessageAttempt ev)
|
||||
{
|
||||
if (!TryComp(ev.Target, out ActivatableUIComponent? comp))
|
||||
return;
|
||||
|
||||
if (!comp.RequireHands)
|
||||
return;
|
||||
|
||||
if (!TryComp(ev.Sender.AttachedEntity, out SharedHandsComponent? hands) || hands.Hands.Count == 0)
|
||||
ev.Cancel();
|
||||
}
|
||||
|
||||
private void OnActionPerform(EntityUid uid, ServerUserInterfaceComponent component, OpenUiActionEvent args)
|
||||
{
|
||||
if (args.Handled || args.Key == null)
|
||||
|
|
@ -57,7 +69,7 @@ namespace Content.Server.UserInterface
|
|||
if (component.InHandsOnly && args.Using != uid)
|
||||
return;
|
||||
|
||||
if (!args.CanInteract && !HasComp<GhostComponent>(args.User))
|
||||
if (!args.CanInteract && (!component.AllowSpectator || !HasComp<GhostComponent>(args.User)))
|
||||
return;
|
||||
|
||||
ActivationVerb verb = new();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
using Content.Shared.Disease;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||
/// <summary>
|
||||
/// Spawn a random disease at regular intervals when artifact activated.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class DiseaseArtifactComponent : Component
|
||||
{
|
||||
public override string Name => "DiseaseArtifact";
|
||||
/// <summary>
|
||||
/// Disease the artifact will spawn
|
||||
/// If empty, picks a random one from its list
|
||||
/// </summary>
|
||||
[DataField("disease", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public string SpawnDisease = string.Empty;
|
||||
/// <summary>
|
||||
/// How far away it will check for people
|
||||
/// If empty, picks a random one from its list
|
||||
/// </summary>
|
||||
[DataField("range")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Range = 5f;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public DiseasePrototype ResolveDisease = default!;
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public readonly IReadOnlyList<string> ArtifactDiseases = new[]
|
||||
{
|
||||
"VanAusdallsRobovirus",
|
||||
"OwOnavirus",
|
||||
"BleedersBite",
|
||||
"Ultragigacancer",
|
||||
"AMIV"
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
|
||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||
using Content.Shared.Disease;
|
||||
using Content.Server.Disease;
|
||||
using Content.Server.Disease.Components;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Interaction;
|
||||
|
||||
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles disease-producing artifacts
|
||||
/// </summary>
|
||||
public sealed class DiseaseArtifactSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<DiseaseArtifactComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<DiseaseArtifactComponent, ArtifactActivatedEvent>(OnActivate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes sure this artifact is assigned a disease
|
||||
/// </summary>
|
||||
private void OnMapInit(EntityUid uid, DiseaseArtifactComponent component, MapInitEvent args)
|
||||
{
|
||||
if (component.SpawnDisease == string.Empty && component.ArtifactDiseases.Count != 0)
|
||||
{
|
||||
var diseaseName = _random.Pick(component.ArtifactDiseases);
|
||||
|
||||
component.SpawnDisease = diseaseName;
|
||||
}
|
||||
|
||||
if (_prototypeManager.TryIndex(component.SpawnDisease, out DiseasePrototype? disease) && disease != null)
|
||||
component.ResolveDisease = disease;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When activated, blasts everyone in LOS within 3 tiles
|
||||
/// with a high-probability disease infection attempt
|
||||
/// </summary>
|
||||
private void OnActivate(EntityUid uid, DiseaseArtifactComponent component, ArtifactActivatedEvent args)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
foreach (var entity in _lookup.GetEntitiesInRange(xform.MapID, xform.WorldPosition, 3f))
|
||||
{
|
||||
if (!_interactionSystem.InRangeUnobstructed(uid, entity, 3f))
|
||||
continue;
|
||||
|
||||
if (TryComp<DiseaseCarrierComponent>(entity, out var carrier))
|
||||
EntitySystem.Get<DiseaseSystem>().TryInfect(carrier, component.ResolveDisease);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Serialization.Manager.Attributes;
|
||||
|
||||
namespace Content.Shared.Chemistry.Reagent
|
||||
{
|
||||
|
|
|
|||
36
Content.Shared/Clothing/ClothingSpeedModifierComponent.cs
Normal file
36
Content.Shared/Clothing/ClothingSpeedModifierComponent.cs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Clothing;
|
||||
|
||||
[RegisterComponent, NetworkedComponent, Friend(typeof(ClothingSpeedModifierSystem))]
|
||||
public sealed class ClothingSpeedModifierComponent : Component
|
||||
{
|
||||
[DataField("walkModifier", required: true)] [ViewVariables(VVAccess.ReadWrite)]
|
||||
public float WalkModifier = 1.0f;
|
||||
|
||||
[DataField("sprintModifier", required: true)] [ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SprintModifier = 1.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Is this clothing item currently 'actively' slowing you down?
|
||||
/// e.g. magboots can be turned on and off.
|
||||
/// </summary>
|
||||
[DataField("enabled")] public bool Enabled = true;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ClothingSpeedModifierComponentState : ComponentState
|
||||
{
|
||||
public float WalkModifier;
|
||||
public float SprintModifier;
|
||||
|
||||
public bool Enabled;
|
||||
|
||||
public ClothingSpeedModifierComponentState(float walkModifier, float sprintModifier, bool enabled)
|
||||
{
|
||||
WalkModifier = walkModifier;
|
||||
SprintModifier = sprintModifier;
|
||||
Enabled = enabled;
|
||||
}
|
||||
}
|
||||
70
Content.Shared/Clothing/ClothingSpeedModifierSystem.cs
Normal file
70
Content.Shared/Clothing/ClothingSpeedModifierSystem.cs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
using Content.Shared.Movement.EntitySystems;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Clothing;
|
||||
|
||||
public sealed class ClothingSpeedModifierSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ClothingSpeedModifierComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<ClothingSpeedModifierComponent, ComponentHandleState>(OnHandleState);
|
||||
SubscribeLocalEvent<ClothingSpeedModifierComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMoveSpeed);
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
public void SetClothingSpeedModifierEnabled(EntityUid uid, bool enabled, ClothingSpeedModifierComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component, false))
|
||||
return;
|
||||
|
||||
if (component.Enabled != enabled)
|
||||
{
|
||||
component.Enabled = enabled;
|
||||
Dirty(component);
|
||||
|
||||
// inventory system will automatically hook into the event raised by this and update accordingly
|
||||
if (_container.TryGetContainingContainer(uid, out var container))
|
||||
{
|
||||
_movementSpeed.RefreshMovementSpeedModifiers(container.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
|
||||
private void OnGetState(EntityUid uid, ClothingSpeedModifierComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new ClothingSpeedModifierComponentState(component.WalkModifier, component.SprintModifier, component.Enabled);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, ClothingSpeedModifierComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is ClothingSpeedModifierComponentState state)
|
||||
{
|
||||
component.WalkModifier = state.WalkModifier;
|
||||
component.SprintModifier = state.SprintModifier;
|
||||
component.Enabled = state.Enabled;
|
||||
|
||||
if (_container.TryGetContainingContainer(uid, out var container))
|
||||
{
|
||||
_movementSpeed.RefreshMovementSpeedModifiers(container.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRefreshMoveSpeed(EntityUid uid, ClothingSpeedModifierComponent component, RefreshMovementSpeedModifiersEvent args)
|
||||
{
|
||||
if (!component.Enabled)
|
||||
return;
|
||||
|
||||
args.ModifySpeed(component.WalkModifier, component.SprintModifier);
|
||||
}
|
||||
}
|
||||
|
|
@ -18,22 +18,8 @@ namespace Content.Shared.Clothing
|
|||
protected void OnChanged()
|
||||
{
|
||||
EntitySystem.Get<SharedActionsSystem>().SetToggled(ToggleAction, On);
|
||||
|
||||
// inventory system will automatically hook into the event raised by this and update accordingly
|
||||
if (Owner.TryGetContainer(out var container))
|
||||
{
|
||||
EntitySystem.Get<MovementSpeedModifierSystem>().RefreshMovementSpeedModifiers(container.Owner);
|
||||
}
|
||||
EntitySystem.Get<ClothingSpeedModifierSystem>().SetClothingSpeedModifierEnabled(Owner, On);
|
||||
}
|
||||
[DataField("walkMoveCoeffecient", required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float WalkMoveCoeffecient = 0.85f;
|
||||
|
||||
[DataField("sprintMoveCoeffecient", required: true)]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float SprintMoveCoeffecient = 0.65f;
|
||||
public float WalkSpeedModifier => On ? WalkMoveCoeffecient : 1;
|
||||
public float SprintSpeedModifier => On ? SprintMoveCoeffecient : 1;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class MagbootsComponentState : ComponentState
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
</PropertyGroup>
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2020.3.0" PrivateAssets="All" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Content.Shared.Database\Content.Shared.Database.csproj" />
|
||||
|
|
|
|||
24
Content.Shared/Disease/DiseaseCure.cs
Normal file
24
Content.Shared/Disease/DiseaseCure.cs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Shared.Disease
|
||||
{
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
[MeansImplicitUse]
|
||||
public abstract class DiseaseCure
|
||||
{
|
||||
/// <summary>
|
||||
/// This returns true if the disease should be cured
|
||||
/// and false otherwise
|
||||
/// </summary>
|
||||
public abstract bool Cure(DiseaseEffectArgs args);
|
||||
|
||||
/// <summary>
|
||||
/// This is used by the disease diangoser machine
|
||||
/// to generate reports to tell people all of a disease's
|
||||
/// special cures using in-game methods.
|
||||
/// So it should return a localization string describing
|
||||
/// the cure
|
||||
/// </summary>
|
||||
public abstract string CureText();
|
||||
}
|
||||
}
|
||||
29
Content.Shared/Disease/DiseaseEffect.cs
Normal file
29
Content.Shared/Disease/DiseaseEffect.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Shared.Disease
|
||||
{
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
[MeansImplicitUse]
|
||||
public abstract class DiseaseEffect
|
||||
{
|
||||
/// <summary>
|
||||
/// What's the chance, from 0 to 1, that this effect will occur?
|
||||
/// </summary>
|
||||
[DataField("probability")]
|
||||
public float Probability = 1.0f;
|
||||
/// <summary>
|
||||
/// What effect the disease will have.
|
||||
/// </summary>
|
||||
public abstract void Effect(DiseaseEffectArgs args);
|
||||
}
|
||||
/// <summary>
|
||||
/// What you have to work with in any disease effect/cure.
|
||||
/// Includes an entity manager because it is out of scope
|
||||
/// otherwise.
|
||||
/// </summary>
|
||||
public readonly record struct DiseaseEffectArgs(
|
||||
EntityUid DiseasedEntity,
|
||||
DiseasePrototype Disease,
|
||||
IEntityManager EntityManager
|
||||
);
|
||||
}
|
||||
16
Content.Shared/Disease/DiseaseMachineVisuals.cs
Normal file
16
Content.Shared/Disease/DiseaseMachineVisuals.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Disease
|
||||
{
|
||||
[Serializable, NetSerializable]
|
||||
/// <summary>
|
||||
/// Stores bools for if the machine is on
|
||||
/// and if it's currently running.
|
||||
/// Used for the visualizer
|
||||
/// </summary>
|
||||
public enum DiseaseMachineVisuals : byte
|
||||
{
|
||||
IsOn,
|
||||
IsRunning
|
||||
}
|
||||
}
|
||||
69
Content.Shared/Disease/DiseasePrototype.cs
Normal file
69
Content.Shared/Disease/DiseasePrototype.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.Disease
|
||||
{
|
||||
/// <summary>
|
||||
/// Diseases encompass everything from viruses to cancers to heart disease.
|
||||
/// It's not just a virology thing.
|
||||
/// </summary>
|
||||
[Prototype("disease")]
|
||||
[DataDefinition]
|
||||
public sealed class DiseasePrototype : IPrototype, IInheritingPrototype
|
||||
{
|
||||
[ViewVariables]
|
||||
[DataField("id", required: true)]
|
||||
public string ID { get; } = default!;
|
||||
|
||||
[DataField("name")]
|
||||
public string Name { get; } = string.Empty;
|
||||
|
||||
[DataField("parent", customTypeSerializer: typeof(PrototypeIdSerializer<DiseasePrototype>))]
|
||||
public string? Parent { get; private set; }
|
||||
|
||||
[NeverPushInheritance]
|
||||
[DataField("abstract")]
|
||||
public bool Abstract { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Controls how often a disease ticks.
|
||||
/// </summary>
|
||||
public float TickTime = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Since disease isn't mapped to metabolism or anything,
|
||||
/// it needs something to control its tickrate
|
||||
/// </summary>
|
||||
public float Accumulator = 0f;
|
||||
/// <summary>
|
||||
/// List of effects the disease has that will
|
||||
/// run every second (by default anyway)
|
||||
/// </summary>
|
||||
[DataField("effects", serverOnly: true)]
|
||||
public readonly List<DiseaseEffect> Effects = new(0);
|
||||
/// <summary>
|
||||
/// List of SPECIFIC CURES the disease has that will
|
||||
/// be checked every second.
|
||||
/// Stuff like spaceacillin operates outside this.
|
||||
/// </summary>
|
||||
[DataField("cures", serverOnly: true)]
|
||||
public readonly List<DiseaseCure> Cures = new(0);
|
||||
/// <summary>
|
||||
/// This flatly reduces the probabilty disease medicine
|
||||
/// has to cure it every tick. Although, since spaceacillin is
|
||||
/// used as a reference and it has 0.15 chance, this is
|
||||
/// a base 33% reduction in cure chance
|
||||
/// </summary>
|
||||
[DataField("cureResist", serverOnly: true)]
|
||||
public float CureResist = 0.05f;
|
||||
/// <summary>
|
||||
/// Whether the disease can infect other people.
|
||||
/// Since this isn't just a virology thing, this
|
||||
/// primary determines what sort of disease it is.
|
||||
/// This also affects things like the vaccine machine.
|
||||
/// You can't print a cancer vaccine
|
||||
/// </summary>
|
||||
[DataField("infectious", serverOnly: true)]
|
||||
public bool Infectious = true;
|
||||
}
|
||||
}
|
||||
12
Content.Shared/Disease/DiseasedComponent.cs
Normal file
12
Content.Shared/Disease/DiseasedComponent.cs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Disease.Components
|
||||
{
|
||||
[NetworkedComponent]
|
||||
[RegisterComponent]
|
||||
/// This is added to anyone with at least 1 disease
|
||||
/// and helps cull event subscriptions and entity queries
|
||||
/// when they are not relevant.
|
||||
public sealed class DiseasedComponent : Component
|
||||
{}
|
||||
}
|
||||
|
|
@ -44,15 +44,17 @@ namespace Content.Shared.GameTicking
|
|||
{
|
||||
public bool IsRoundStarted { get; }
|
||||
public string? LobbySong { get; }
|
||||
public string? LobbyBackground { get; }
|
||||
public bool YouAreReady { get; }
|
||||
// UTC.
|
||||
public TimeSpan StartTime { get; }
|
||||
public bool Paused { get; }
|
||||
|
||||
public TickerLobbyStatusEvent(bool isRoundStarted, string? lobbySong, bool youAreReady, TimeSpan startTime, bool paused)
|
||||
public TickerLobbyStatusEvent(bool isRoundStarted, string? lobbySong, string? lobbyBackground, bool youAreReady, TimeSpan startTime, bool paused)
|
||||
{
|
||||
IsRoundStarted = isRoundStarted;
|
||||
LobbySong = lobbySong;
|
||||
LobbyBackground = lobbyBackground;
|
||||
YouAreReady = youAreReady;
|
||||
StartTime = startTime;
|
||||
Paused = paused;
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
</PropertyGroup>
|
||||
<Import Project="..\RobustToolbox\MSBuild\Robust.DefineConstants.targets" />
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NUnit" Version="3.13.1" />
|
||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.12.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit.ConsoleRunner" Version="3.15.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Content.Client\Content.Client.csproj" />
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Resources/Audio/Lobby/Flip_flap.ogg
Normal file
BIN
Resources/Audio/Lobby/Flip_flap.ogg
Normal file
Binary file not shown.
|
|
@ -8,7 +8,7 @@ absconditus.ogg (Absconditus) by ZhayTee is licensed under CC-BY-NC-SA-3.0. It w
|
|||
|
||||
atomicamnesiammx.ogg (Atomic Amnesia MMX) by Phillip Dyer is licensed under CC-BY-NC-SA-3.0. It was converted from MP3 to OGG. The source can be found here: https://soundcloud.com/3kliksphilip/atomic-amnesia-mmx-1
|
||||
|
||||
singuloose.ogg (Singuloose) by Janis Schiedková is licensed under a CC-BY-NC-SA 3.0. It was converted from MP3 to OGG. The source can be found here: https://soundcloud.com/js_m/singuloose
|
||||
singuloose.ogg (Singuloose) by Janis Schiedková is licensed under a CC-BY-NC-SA 3.0. It was converted from MP3 to OGG. The source can be found here: https://soundcloud.com/js_m/singuloose
|
||||
|
||||
vibe_ace.ogg (Vibe Ace) by Kevin MacLeod is licensed under CC-BY-3.0. It was converted from MP3 to OGG. The source can be found here: https://freemusicarchive.org/music/Kevin_MacLeod/Jazz_Sampler/Vibe_Ace
|
||||
|
||||
|
|
@ -17,3 +17,5 @@ Title3.ogg (Title3) By Cuboos is licensed under a BY-NC-SA 3.0 Creative Commons
|
|||
Marhaba.ogg (Marhaba) by Ian Alex Mac is licensed under a BY 4.0 Creative Commons License. It was converted from MP3 to OGG. The source can be found here: https://freemusicarchive.org/music/Ian_Alex_Mac/Cues/Marhaba
|
||||
|
||||
Comet-Haley.ogg (Comet Haley) by Stellardrone is licensed under a BY 3.0 Creative Commons License. It was converted from MP3 to OGG. The source can be found here: https://freemusicarchive.org/music/Stellardrone/Light_Years_1227/07_Comet_Halley
|
||||
|
||||
Flip Flap.ogg (Flip Flap) by X-ceed is licensed by soundimage.org and is free for non-commercial use. It was converted from MP3 to OGG. The source is here http://aminet.net/package/mods/xceed/Flipflap
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
BIN
Resources/Audio/Machines/diagnoser_printing.ogg
Normal file
BIN
Resources/Audio/Machines/diagnoser_printing.ogg
Normal file
Binary file not shown.
3
Resources/Audio/Machines/license.txt
Normal file
3
Resources/Audio/Machines/license.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
diagnoser_printing.ogg taken from https://freesound.org/people/RobSp1derp1g/sounds/615419/ and edited
|
||||
|
||||
vaccinator_running.ogg taken from https://freesound.org/people/RutgerMuller/sounds/365413/ and edited
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Resources/Audio/Machines/vaccinator_running.ogg
Normal file
BIN
Resources/Audio/Machines/vaccinator_running.ogg
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue