Merge branch 'space-wizards:master' into FoodSizeAdjustment
This commit is contained in:
@@ -334,7 +334,12 @@ namespace Content.Client.Actions
|
||||
|
||||
private void OnEntityTargetAttempt(Entity<EntityTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
|
||||
{
|
||||
if (args.Handled || args.Input.EntityUid is not { Valid: true } entity)
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (args.Input.EntityUid is not { Valid: true } entity)
|
||||
return;
|
||||
|
||||
// let world target component handle it
|
||||
@@ -345,8 +350,6 @@ namespace Content.Client.Actions
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
var action = args.Action;
|
||||
var user = args.User;
|
||||
|
||||
|
||||
@@ -62,11 +62,11 @@ public sealed class AnomalySystem : SharedAnomalySystem
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<AnomalySupercriticalComponent, SpriteComponent>();
|
||||
var query = EntityQueryEnumerator<AnomalyComponent, AnomalySupercriticalComponent, SpriteComponent>();
|
||||
|
||||
while (query.MoveNext(out var uid, out var super, out var sprite))
|
||||
while (query.MoveNext(out var uid, out var anomaly, out var super, out var sprite))
|
||||
{
|
||||
var completion = 1f - (float)((super.EndTime - _timing.CurTime) / super.SupercriticalDuration);
|
||||
var completion = 1f - (float) ((super.EndTime - _timing.CurTime) / anomaly.SupercriticalDuration);
|
||||
var scale = completion * (super.MaxScaleAmount - 1f) + 1f;
|
||||
_sprite.SetScale((uid, sprite), new Vector2(scale, scale));
|
||||
|
||||
|
||||
@@ -162,10 +162,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
|
||||
{
|
||||
var list = new List<AtmosMonitoringConsoleLine>();
|
||||
|
||||
foreach (var ((netId, layer, hexColor), atmosPipeData) in chunk.AtmosPipeData)
|
||||
foreach (var ((netId, layer, pipeColor), atmosPipeData) in chunk.AtmosPipeData)
|
||||
{
|
||||
// Determine the correct coloration for the pipe
|
||||
var color = Color.FromHex(hexColor) * _basePipeNetColor;
|
||||
var color = pipeColor * _basePipeNetColor;
|
||||
|
||||
if (FocusNetId != null && FocusNetId != netId)
|
||||
color *= _unfocusedPipeNetColor;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.SprayPainter.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Used to change the appearance of gas canisters.
|
||||
/// </summary>
|
||||
public sealed class GasCanisterAppearanceSystem : VisualizerSystem<GasCanisterComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, GasCanisterComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (!AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var protoName, args.Component) || args.Sprite is not { } old)
|
||||
return;
|
||||
|
||||
if (!_prototypeManager.HasIndex(protoName))
|
||||
return;
|
||||
|
||||
// Create the given prototype and get its first layer.
|
||||
var tempUid = Spawn(protoName);
|
||||
SpriteSystem.LayerSetRsiState(uid, 0, SpriteSystem.LayerGetRsiState(tempUid, 0));
|
||||
QueueDel(tempUid);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,46 @@
|
||||
using Content.Client.Atmos.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Content.Client.UserInterface.Systems.Storage.Controls;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Item;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly SharedItemSystem _itemSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PipeColorVisualsComponent, GetInhandVisualsEvent>(OnGetVisuals);
|
||||
SubscribeLocalEvent<PipeColorVisualsComponent, BeforeRenderInGridEvent>(OnDrawInGrid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is used to display the color changes of the pipe on the screen..
|
||||
/// </summary>
|
||||
private void OnGetVisuals(Entity<PipeColorVisualsComponent> item, ref GetInhandVisualsEvent args)
|
||||
{
|
||||
foreach (var (_, layerData) in args.Layers)
|
||||
{
|
||||
if (TryComp(item.Owner, out AtmosPipeColorComponent? pipeColor))
|
||||
layerData.Color = pipeColor.Color;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This method is used to change the pipe's color in a container grid.
|
||||
/// </summary>
|
||||
private void OnDrawInGrid(Entity<PipeColorVisualsComponent> item, ref BeforeRenderInGridEvent args)
|
||||
{
|
||||
if (TryComp(item.Owner, out AtmosPipeColorComponent? pipeColor))
|
||||
args.Color = pipeColor.Color;
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, PipeColorVisualsComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (TryComp<SpriteComponent>(uid, out var sprite)
|
||||
@@ -15,6 +50,8 @@ public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisual
|
||||
var layer = sprite[PipeVisualLayers.Pipe];
|
||||
layer.Color = color.WithAlpha(layer.Color.A);
|
||||
}
|
||||
|
||||
_itemSystem.VisualsChanged(uid);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2007/xaml"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
MinSize="500 500" Resizable="True" Title="Air Alarm">
|
||||
MinSize="500 500" Resizable="True" Title="{Loc air-alarm-ui-title}">
|
||||
<BoxContainer Orientation="Vertical" Margin="5 5 5 5">
|
||||
<!-- Status (pressure, temperature, alarm state, device total, address, etc) -->
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Client.Graphics;
|
||||
using System.Numerics;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -36,7 +37,7 @@ namespace Content.Client.Cooldown
|
||||
if (Progress >= 0f)
|
||||
{
|
||||
var hue = (5f / 18f) * lerp;
|
||||
color = Color.FromHsv((hue, 0.75f, 0.75f, 0.50f));
|
||||
color = Color.FromHsv(new Vector4(hue, 0.75f, 0.75f, 0.50f));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,7 +1,126 @@
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Client.Stunnable;
|
||||
using Content.Shared.Damage.Components;
|
||||
using Content.Shared.Damage.Systems;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Damage.Systems;
|
||||
|
||||
public sealed partial class StaminaSystem : SharedStaminaSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
[Dependency] private readonly StunSystem _stun = default!; // Clientside Stun System
|
||||
|
||||
private const string StaminaAnimationKey = "stamina";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StaminaComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
SubscribeLocalEvent<ActiveStaminaComponent, ComponentShutdown>(OnActiveStaminaShutdown);
|
||||
SubscribeLocalEvent<StaminaComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
}
|
||||
|
||||
protected override void OnStamHandleState(Entity<StaminaComponent> entity, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
base.OnStamHandleState(entity, ref args);
|
||||
|
||||
TryStartAnimation(entity);
|
||||
}
|
||||
|
||||
private void OnActiveStaminaShutdown(Entity<ActiveStaminaComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
// If we don't have active stamina, we shouldn't have stamina damage. If the update loop can trust it we can trust it.
|
||||
if (!TryComp<StaminaComponent>(entity, out var stamina))
|
||||
return;
|
||||
|
||||
StopAnimation((entity, stamina));
|
||||
}
|
||||
|
||||
protected override void OnShutdown(Entity<StaminaComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
base.OnShutdown(entity, ref args);
|
||||
|
||||
StopAnimation(entity);
|
||||
}
|
||||
|
||||
private void OnMobStateChanged(Entity<StaminaComponent> ent, ref MobStateChangedEvent args)
|
||||
{
|
||||
if (args.NewMobState == MobState.Dead)
|
||||
StopAnimation(ent);
|
||||
}
|
||||
|
||||
private void TryStartAnimation(Entity<StaminaComponent> entity)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(entity, out var sprite))
|
||||
return;
|
||||
|
||||
// If the animation is running, the system should update it accordingly
|
||||
// If we're below the threshold to animate, don't try to animate
|
||||
// If we're in stamcrit don't override it
|
||||
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage || _animation.HasRunningAnimation(entity, StaminaAnimationKey))
|
||||
return;
|
||||
|
||||
// Don't animate if we're dead
|
||||
if (_mobState.IsDead(entity))
|
||||
return;
|
||||
|
||||
entity.Comp.StartOffset = sprite.Offset;
|
||||
|
||||
PlayAnimation((entity, entity.Comp, sprite));
|
||||
}
|
||||
|
||||
private void StopAnimation(Entity<StaminaComponent, SpriteComponent?> entity)
|
||||
{
|
||||
if(!Resolve(entity, ref entity.Comp2))
|
||||
return;
|
||||
|
||||
_animation.Stop(entity.Owner, StaminaAnimationKey);
|
||||
entity.Comp1.StartOffset = entity.Comp2.Offset;
|
||||
}
|
||||
|
||||
private void OnAnimationCompleted(Entity<StaminaComponent> entity, ref AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != StaminaAnimationKey || !args.Finished || !TryComp<SpriteComponent>(entity, out var sprite))
|
||||
return;
|
||||
|
||||
// stop looping if we're below the threshold
|
||||
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage)
|
||||
{
|
||||
_animation.Stop(entity.Owner, StaminaAnimationKey);
|
||||
_sprite.SetOffset((entity, sprite), entity.Comp.StartOffset);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasComp<AnimationPlayerComponent>(entity))
|
||||
return;
|
||||
|
||||
PlayAnimation((entity, entity.Comp, sprite));
|
||||
}
|
||||
|
||||
private void PlayAnimation(Entity<StaminaComponent, SpriteComponent> entity)
|
||||
{
|
||||
var step = Math.Clamp((entity.Comp1.StaminaDamage - entity.Comp1.AnimationThreshold) /
|
||||
(entity.Comp1.CritThreshold - entity.Comp1.AnimationThreshold),
|
||||
0f,
|
||||
1f); // The things I do for project 0 warnings
|
||||
var frequency = entity.Comp1.FrequencyMin + step * entity.Comp1.FrequencyMod;
|
||||
var jitter = entity.Comp1.JitterAmplitudeMin + step * entity.Comp1.JitterAmplitudeMod;
|
||||
var breathing = entity.Comp1.BreathingAmplitudeMin + step * entity.Comp1.BreathingAmplitudeMod;
|
||||
|
||||
_animation.Play(entity.Owner,
|
||||
_stun.GetFatigueAnimation(entity.Comp2,
|
||||
frequency,
|
||||
entity.Comp1.Jitters,
|
||||
jitter * entity.Comp1.JitterMin,
|
||||
jitter * entity.Comp1.JitterMax,
|
||||
breathing,
|
||||
entity.Comp1.StartOffset,
|
||||
ref entity.Comp1.LastJitter),
|
||||
StaminaAnimationKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using Content.Shared.Devour;
|
||||
|
||||
namespace Content.Client.Devour;
|
||||
public sealed class DevourSystem : SharedDevourSystem
|
||||
{
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Disposal;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Disposal;
|
||||
using Content.Shared.Disposal.Unit;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.SprayPainter.Prototypes;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Doors;
|
||||
|
||||
public sealed class DoorSystem : SharedDoorSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -85,8 +86,8 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
if (!AppearanceSystem.TryGetData<DoorState>(entity, DoorVisuals.State, out var state, args.Component))
|
||||
state = DoorState.Closed;
|
||||
|
||||
if (AppearanceSystem.TryGetData<string>(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
|
||||
UpdateSpriteLayers((entity.Owner, args.Sprite), baseRsi);
|
||||
if (AppearanceSystem.TryGetData<string>(entity, PaintableVisuals.Prototype, out var prototype, args.Component))
|
||||
UpdateSpriteLayers((entity.Owner, args.Sprite), prototype);
|
||||
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
|
||||
_animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
|
||||
@@ -139,14 +140,14 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string baseRsi)
|
||||
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string targetProto)
|
||||
{
|
||||
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
|
||||
{
|
||||
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
|
||||
if (!_prototypeManager.TryIndex(targetProto, out var target))
|
||||
return;
|
||||
}
|
||||
|
||||
_sprite.SetBaseRsi(sprite.AsNullable(), res.RSI);
|
||||
if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory))
|
||||
return;
|
||||
|
||||
_sprite.SetBaseRsi(sprite.AsNullable(), targetSprite.BaseRSI);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class DrowsinessOverlay : Overlay
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
private readonly SharedStatusEffectsSystem _statusEffects = default!;
|
||||
private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
public override bool RequestScreenTexture => true;
|
||||
@@ -33,7 +33,7 @@ public sealed class DrowsinessOverlay : Overlay
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_statusEffects = _sysMan.GetEntitySystem<SharedStatusEffectsSystem>();
|
||||
_statusEffects = _sysMan.GetEntitySystem<StatusEffectsSystem>();
|
||||
|
||||
_drowsinessShader = _prototypeManager.Index(Shader).InstanceUnique();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem
|
||||
{
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
[Dependency] private readonly SharedStatusEffectsSystem _statusEffects = default!;
|
||||
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
|
||||
private DrowsinessOverlay _overlay = default!;
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class RainbowOverlay : Overlay
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
private readonly SharedStatusEffectsSystem _statusEffects = default!;
|
||||
private readonly StatusEffectsSystem _statusEffects = default!;
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpace;
|
||||
public override bool RequestScreenTexture => true;
|
||||
@@ -41,7 +41,7 @@ public sealed class RainbowOverlay : Overlay
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_statusEffects = _sysMan.GetEntitySystem<SharedStatusEffectsSystem>();
|
||||
_statusEffects = _sysMan.GetEntitySystem<StatusEffectsSystem>();
|
||||
|
||||
_rainbowShader = _prototypeManager.Index(Shader).InstanceUnique();
|
||||
_config.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);
|
||||
|
||||
@@ -5,6 +5,7 @@ using Robust.Client.Graphics;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||
|
||||
namespace Content.Client.Holopad;
|
||||
|
||||
@@ -55,6 +55,7 @@ namespace Content.Client.Input
|
||||
human.AddFunction(EngineKeyFunctions.MoveLeft);
|
||||
human.AddFunction(EngineKeyFunctions.MoveRight);
|
||||
human.AddFunction(EngineKeyFunctions.Walk);
|
||||
human.AddFunction(ContentKeyFunctions.ToggleKnockdown);
|
||||
human.AddFunction(ContentKeyFunctions.SwapHands);
|
||||
human.AddFunction(ContentKeyFunctions.SwapHandsReverse);
|
||||
human.AddFunction(ContentKeyFunctions.Drop);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Items.Systems;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.Hands;
|
||||
|
||||
@@ -57,7 +57,8 @@ public sealed class MouseRotatorSystem : SharedMouseRotatorSystem
|
||||
rotation += 2 * Math.PI;
|
||||
RaisePredictiveEvent(new RequestMouseRotatorRotationEvent
|
||||
{
|
||||
Rotation = rotation
|
||||
Rotation = rotation,
|
||||
User = GetNetEntity(player)
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -77,7 +78,8 @@ public sealed class MouseRotatorSystem : SharedMouseRotatorSystem
|
||||
|
||||
RaisePredictiveEvent(new RequestMouseRotatorRotationEvent
|
||||
{
|
||||
Rotation = angle
|
||||
Rotation = angle,
|
||||
User = GetNetEntity(player)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ namespace Content.Client.Options.UI.Tabs
|
||||
AddButton(EngineKeyFunctions.Walk);
|
||||
AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk);
|
||||
InitToggleWalk();
|
||||
AddButton(ContentKeyFunctions.ToggleKnockdown);
|
||||
|
||||
AddHeader("ui-options-header-camera");
|
||||
AddButton(EngineKeyFunctions.CameraRotateLeft);
|
||||
|
||||
@@ -195,7 +195,6 @@ public sealed class GeneratedParallaxCache : IPostInjectInit
|
||||
public required ResPath ConfigPath;
|
||||
public required Task<Texture> LoadTask;
|
||||
public required CancellationTokenSource CancellationSource;
|
||||
public ValueList<CancellationTokenRegistration> CancelRegistrations;
|
||||
|
||||
public int RefCount;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.APC;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
@@ -9,7 +9,7 @@ public static class StaticPowerSystem
|
||||
public static bool IsPowered(this EntitySystem system, EntityUid uid, IEntityManager entManager, ApcPowerReceiverComponent? receiver = null)
|
||||
{
|
||||
if (receiver == null && !entManager.TryGetComponent(uid, out receiver))
|
||||
return false;
|
||||
return true;
|
||||
|
||||
return receiver.Powered;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ using Robust.Shared.Utility;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Vector4 = Robust.Shared.Maths.Vector4;
|
||||
|
||||
namespace Content.Client.Power;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ using Robust.Shared.Physics;
|
||||
using Robust.Shared.Threading;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Vector2 = System.Numerics.Vector2;
|
||||
|
||||
namespace Content.Client.Shuttles.UI;
|
||||
|
||||
|
||||
@@ -220,9 +220,9 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
|
||||
|
||||
var gridCentre = Vector2.Transform(gridBody.LocalCenter, curGridToView);
|
||||
|
||||
var distance = gridCentre.Length();
|
||||
var gridDistance = (gridBody.LocalCenter - xform.LocalPosition).Length();
|
||||
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
|
||||
("distance", $"{distance:0.0}"));
|
||||
("distance", $"{gridDistance:0.0}"));
|
||||
|
||||
var mapCoords = _transform.GetWorldPosition(gUid);
|
||||
var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";
|
||||
|
||||
43
Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs
Normal file
43
Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.SmartFridge;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
public sealed class SmartFridgeBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private SmartFridgeMenu? _menu;
|
||||
|
||||
public SmartFridgeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = this.CreateWindow<SmartFridgeMenu>();
|
||||
_menu.OnItemSelected += OnItemSelected;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (_menu is not {} menu || !EntMan.TryGetComponent(Owner, out SmartFridgeComponent? fridge))
|
||||
return;
|
||||
|
||||
menu.SetFlavorText(Loc.GetString(fridge.FlavorText));
|
||||
menu.Populate((Owner, fridge));
|
||||
}
|
||||
|
||||
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
|
||||
{
|
||||
if (args.Function != EngineKeyFunctions.UIClick)
|
||||
return;
|
||||
|
||||
if (data is not SmartFridgeListData entry)
|
||||
return;
|
||||
SendPredictedMessage(new SmartFridgeDispenseItemMessage(entry.Entry));
|
||||
}
|
||||
}
|
||||
16
Content.Client/SmartFridge/SmartFridgeItem.xaml
Normal file
16
Content.Client/SmartFridge/SmartFridgeItem.xaml
Normal file
@@ -0,0 +1,16 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="4">
|
||||
<SpriteView
|
||||
Name="EntityView"
|
||||
Margin="4 0 0 0"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
MinSize="32 32"
|
||||
/>
|
||||
<Label Name="NameLabel"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
</BoxContainer>
|
||||
18
Content.Client/SmartFridge/SmartFridgeItem.xaml.cs
Normal file
18
Content.Client/SmartFridge/SmartFridgeItem.xaml.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SmartFridgeItem : BoxContainer
|
||||
{
|
||||
public SmartFridgeItem(EntityUid uid, string text)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
EntityView.SetEntity(uid);
|
||||
NameLabel.Text = text;
|
||||
}
|
||||
}
|
||||
24
Content.Client/SmartFridge/SmartFridgeMenu.xaml
Normal file
24
Content.Client/SmartFridge/SmartFridgeMenu.xaml
Normal file
@@ -0,0 +1,24 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinHeight="450"
|
||||
MinWidth="350"
|
||||
Title="{Loc 'smart-fridge-component-title'}">
|
||||
<BoxContainer Name="MainContainer" Orientation="Vertical">
|
||||
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'smart-fridge-component-search-filter'}" HorizontalExpand="True" Margin ="4 4"/>
|
||||
<co:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 4"/>
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
|
||||
<Label Name="LeftFlavorLabel" StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'vending-machine-flavor-right'}" StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
|
||||
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
81
Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs
Normal file
81
Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.SmartFridge;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
public record SmartFridgeListData(EntityUid Representative, SmartFridgeEntry Entry, int Amount) : ListData;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SmartFridgeMenu : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
|
||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
|
||||
|
||||
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
|
||||
|
||||
public SmartFridgeMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
VendingContents.SearchBar = SearchBar;
|
||||
VendingContents.DataFilterCondition += DataFilterCondition;
|
||||
VendingContents.GenerateItem += GenerateButton;
|
||||
VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data);
|
||||
}
|
||||
|
||||
private bool DataFilterCondition(string filter, ListData data)
|
||||
{
|
||||
if (data is not SmartFridgeListData entry)
|
||||
return false;
|
||||
|
||||
if (string.IsNullOrEmpty(filter))
|
||||
return true;
|
||||
|
||||
return entry.Entry.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
|
||||
}
|
||||
|
||||
private void GenerateButton(ListData data, ListContainerButton button)
|
||||
{
|
||||
if (data is not SmartFridgeListData entry)
|
||||
return;
|
||||
|
||||
var label = Loc.GetString("smart-fridge-list-item", ("item", entry.Entry.Name), ("amount", entry.Amount));
|
||||
button.AddChild(new SmartFridgeItem(entry.Representative, label));
|
||||
|
||||
button.ToolTip = label;
|
||||
button.StyleBoxOverride = _styleBox;
|
||||
}
|
||||
|
||||
public void Populate(Entity<SmartFridgeComponent> ent)
|
||||
{
|
||||
var listData = new List<ListData>();
|
||||
|
||||
foreach (var item in ent.Comp.Entries)
|
||||
{
|
||||
if (!ent.Comp.ContainedEntries.TryGetValue(item, out var items) || items.Count == 0)
|
||||
{
|
||||
listData.Add(new SmartFridgeListData(EntityUid.Invalid, item, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
var representative = _entityManager.GetEntity(items.First());
|
||||
listData.Add(new SmartFridgeListData(representative, item, items.Count));
|
||||
}
|
||||
}
|
||||
|
||||
VendingContents.PopulateList(listData);
|
||||
}
|
||||
|
||||
public void SetFlavorText(string flavor)
|
||||
{
|
||||
LeftFlavorLabel.Text = flavor;
|
||||
}
|
||||
}
|
||||
24
Content.Client/SmartFridge/SmartFridgeUISystem.cs
Normal file
24
Content.Client/SmartFridge/SmartFridgeUISystem.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using Content.Shared.SmartFridge;
|
||||
using Robust.Shared.Analyzers;
|
||||
|
||||
namespace Content.Client.SmartFridge;
|
||||
|
||||
public sealed class SmartFridgeUISystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SmartFridgeComponent, AfterAutoHandleStateEvent>(OnSmartFridgeAfterState);
|
||||
}
|
||||
|
||||
private void OnSmartFridgeAfterState(Entity<SmartFridgeComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (!_uiSystem.TryGetOpenUi<SmartFridgeBoundUserInterface>(ent.Owner, SmartFridgeUiKey.Key, out var bui))
|
||||
return;
|
||||
|
||||
bui.Refresh();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,129 @@
|
||||
using Content.Shared.SprayPainter;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Graphics;
|
||||
using Content.Client.Items;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.SprayPainter;
|
||||
using Content.Shared.SprayPainter.Components;
|
||||
using Content.Shared.SprayPainter.Prototypes;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.SprayPainter;
|
||||
|
||||
/// <summary>
|
||||
/// Client-side spray painter functions. Caches information for spray painter windows and updates the UI to reflect component state.
|
||||
/// </summary>
|
||||
public sealed class SprayPainterSystem : SharedSprayPainterSystem
|
||||
{
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
||||
|
||||
public List<SprayPainterEntry> Entries { get; private set; } = new();
|
||||
public List<SprayPainterDecalEntry> Decals = [];
|
||||
public Dictionary<string, List<string>> PaintableGroupsByCategory = new();
|
||||
public Dictionary<string, Dictionary<string, EntProtoId>> PaintableStylesByGroup = new();
|
||||
|
||||
protected override void CacheStyles()
|
||||
public override void Initialize()
|
||||
{
|
||||
base.CacheStyles();
|
||||
base.Initialize();
|
||||
|
||||
Entries.Clear();
|
||||
foreach (var style in Styles)
|
||||
Subs.ItemStatus<SprayPainterComponent>(ent => new StatusControl(ent));
|
||||
SubscribeLocalEvent<SprayPainterComponent, AfterAutoHandleStateEvent>(OnStateUpdate);
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
||||
|
||||
CachePrototypes();
|
||||
}
|
||||
|
||||
private void OnStateUpdate(Entity<SprayPainterComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
UpdateUi(ent);
|
||||
}
|
||||
|
||||
protected override void UpdateUi(Entity<SprayPainterComponent> ent)
|
||||
{
|
||||
if (_ui.TryGetOpenUi(ent.Owner, SprayPainterUiKey.Key, out var bui))
|
||||
bui.Update();
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
|
||||
{
|
||||
if (!args.WasModified<PaintableGroupCategoryPrototype>() || !args.WasModified<PaintableGroupPrototype>() || !args.WasModified<DecalPrototype>())
|
||||
return;
|
||||
|
||||
CachePrototypes();
|
||||
}
|
||||
|
||||
private void CachePrototypes()
|
||||
{
|
||||
PaintableGroupsByCategory.Clear();
|
||||
PaintableStylesByGroup.Clear();
|
||||
foreach (var category in Proto.EnumeratePrototypes<PaintableGroupCategoryPrototype>().OrderBy(x => x.ID))
|
||||
{
|
||||
var name = style.Name;
|
||||
string? iconPath = Groups
|
||||
.FindAll(x => x.StylePaths.ContainsKey(name))?
|
||||
.MaxBy(x => x.IconPriority)?.StylePaths[name];
|
||||
if (iconPath == null)
|
||||
var groupList = new List<string>();
|
||||
foreach (var groupId in category.Groups)
|
||||
{
|
||||
Entries.Add(new SprayPainterEntry(name, null));
|
||||
continue;
|
||||
if (!Proto.TryIndex(groupId, out var group))
|
||||
continue;
|
||||
|
||||
groupList.Add(groupId);
|
||||
PaintableStylesByGroup[groupId] = group.Styles;
|
||||
}
|
||||
|
||||
RSIResource doorRsi = _resourceCache.GetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / new ResPath(iconPath));
|
||||
if (!doorRsi.RSI.TryGetState("closed", out var icon))
|
||||
{
|
||||
Entries.Add(new SprayPainterEntry(name, null));
|
||||
continue;
|
||||
}
|
||||
if (groupList.Count > 0)
|
||||
PaintableGroupsByCategory[category.ID] = groupList;
|
||||
}
|
||||
|
||||
Entries.Add(new SprayPainterEntry(name, icon.Frame0));
|
||||
Decals.Clear();
|
||||
foreach (var decalPrototype in Proto.EnumeratePrototypes<DecalPrototype>().OrderBy(x => x.ID))
|
||||
{
|
||||
if (!decalPrototype.Tags.Contains("station")
|
||||
&& !decalPrototype.Tags.Contains("markings")
|
||||
|| decalPrototype.Tags.Contains("dirty"))
|
||||
continue;
|
||||
|
||||
Decals.Add(new SprayPainterDecalEntry(decalPrototype.ID, decalPrototype.Sprite));
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class StatusControl : Control
|
||||
{
|
||||
private readonly RichTextLabel _label;
|
||||
private readonly Entity<SprayPainterComponent> _entity;
|
||||
private DecalPaintMode? _lastPaintingDecals = null;
|
||||
|
||||
public StatusControl(Entity<SprayPainterComponent> ent)
|
||||
{
|
||||
_entity = ent;
|
||||
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
|
||||
AddChild(_label);
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (_entity.Comp.DecalMode == _lastPaintingDecals)
|
||||
return;
|
||||
|
||||
_lastPaintingDecals = _entity.Comp.DecalMode;
|
||||
|
||||
string modeLocString = _entity.Comp.DecalMode switch
|
||||
{
|
||||
DecalPaintMode.Add => "spray-painter-item-status-add",
|
||||
DecalPaintMode.Remove => "spray-painter-item-status-remove",
|
||||
_ => "spray-painter-item-status-off"
|
||||
};
|
||||
|
||||
_label.SetMarkupPermissive(Robust.Shared.Localization.Loc.GetString("spray-painter-item-status-label",
|
||||
("mode", Robust.Shared.Localization.Loc.GetString(modeLocString))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SprayPainterEntry
|
||||
{
|
||||
public string Name;
|
||||
public Texture? Icon;
|
||||
|
||||
public SprayPainterEntry(string name, Texture? icon)
|
||||
{
|
||||
Name = name;
|
||||
Icon = icon;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// A spray paintable decal, mapped by ID.
|
||||
/// </summary>
|
||||
public sealed record SprayPainterDecalEntry(string Name, SpriteSpecifier Sprite);
|
||||
|
||||
@@ -1,42 +1,96 @@
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.SprayPainter;
|
||||
using Content.Shared.SprayPainter.Components;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.SprayPainter.UI;
|
||||
|
||||
public sealed class SprayPainterBoundUserInterface : BoundUserInterface
|
||||
/// <summary>
|
||||
/// A BUI for a spray painter. Allows selecting pipe colours, decals, and paintable object types sorted by category.
|
||||
/// </summary>
|
||||
public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
[ViewVariables]
|
||||
private SprayPainterWindow? _window;
|
||||
|
||||
public SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindow<SprayPainterWindow>();
|
||||
|
||||
_window.OnSpritePicked = OnSpritePicked;
|
||||
_window.OnColorPicked = OnColorPicked;
|
||||
|
||||
if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? comp))
|
||||
if (_window == null)
|
||||
{
|
||||
_window.Populate(EntMan.System<SprayPainterSystem>().Entries, comp.Index, comp.PickedColor, comp.ColorPalette);
|
||||
_window = this.CreateWindow<SprayPainterWindow>();
|
||||
|
||||
_window.OnSpritePicked += OnSpritePicked;
|
||||
_window.OnSetPipeColor += OnSetPipeColor;
|
||||
_window.OnTabChanged += OnTabChanged;
|
||||
_window.OnDecalChanged += OnDecalChanged;
|
||||
_window.OnDecalColorChanged += OnDecalColorChanged;
|
||||
_window.OnDecalAngleChanged += OnDecalAngleChanged;
|
||||
_window.OnDecalSnapChanged += OnDecalSnapChanged;
|
||||
}
|
||||
|
||||
var sprayPainter = EntMan.System<SprayPainterSystem>();
|
||||
_window.PopulateCategories(sprayPainter.PaintableStylesByGroup, sprayPainter.PaintableGroupsByCategory, sprayPainter.Decals);
|
||||
Update();
|
||||
|
||||
if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainterComp))
|
||||
_window.SetSelectedTab(sprayPainterComp.SelectedTab);
|
||||
}
|
||||
|
||||
private void OnSpritePicked(ItemList.ItemListSelectedEventArgs args)
|
||||
public override void Update()
|
||||
{
|
||||
SendMessage(new SprayPainterSpritePickedMessage(args.ItemIndex));
|
||||
if (_window == null)
|
||||
return;
|
||||
|
||||
if (!EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainter))
|
||||
return;
|
||||
|
||||
_window.PopulateColors(sprayPainter.ColorPalette);
|
||||
if (sprayPainter.PickedColor != null)
|
||||
_window.SelectColor(sprayPainter.PickedColor);
|
||||
_window.SetSelectedStyles(sprayPainter.StylesByGroup);
|
||||
_window.SetSelectedDecal(sprayPainter.SelectedDecal);
|
||||
_window.SetDecalAngle(sprayPainter.SelectedDecalAngle);
|
||||
_window.SetDecalColor(sprayPainter.SelectedDecalColor);
|
||||
_window.SetDecalSnap(sprayPainter.SnapDecals);
|
||||
}
|
||||
|
||||
private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
|
||||
private void OnDecalSnapChanged(bool snap)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetDecalSnapMessage(snap));
|
||||
}
|
||||
|
||||
private void OnDecalAngleChanged(int angle)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetDecalAngleMessage(angle));
|
||||
}
|
||||
|
||||
private void OnDecalColorChanged(Color? color)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetDecalColorMessage(color));
|
||||
}
|
||||
|
||||
private void OnDecalChanged(ProtoId<DecalPrototype> protoId)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetDecalMessage(protoId));
|
||||
}
|
||||
|
||||
private void OnTabChanged(int index, bool isSelectedTabWithDecals)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterTabChangedMessage(index, isSelectedTabWithDecals));
|
||||
}
|
||||
|
||||
private void OnSpritePicked(string group, string style)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetPaintableStyleMessage(group, style));
|
||||
}
|
||||
|
||||
private void OnSetPipeColor(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
var key = _window?.IndexToColorKey(args.ItemIndex);
|
||||
SendMessage(new SprayPainterColorPickedMessage(key));
|
||||
SendPredictedMessage(new SprayPainterSetPipeColorMessage(key));
|
||||
}
|
||||
}
|
||||
|
||||
26
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
Normal file
26
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
Normal file
@@ -0,0 +1,26 @@
|
||||
<controls:SprayPainterDecals
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.SprayPainter.UI">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'spray-painter-selected-decals'}" />
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<GridContainer Columns="7" Name="DecalsGrid">
|
||||
<!-- populated by code -->
|
||||
</GridContainer>
|
||||
</ScrollContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ColorSelectorSliders Name="ColorSelector" IsAlphaVisible="True" />
|
||||
<CheckBox Name="UseCustomColorCheckBox" Text="{Loc 'spray-painter-use-custom-color'}" />
|
||||
<CheckBox Name="SnapToTileCheckBox" Text="{Loc 'spray-painter-use-snap-to-tile'}" />
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'spray-painter-angle-rotation'}" />
|
||||
<SpinBox Name="AngleSpinBox" HorizontalExpand="True" />
|
||||
<Button Text="{Loc 'spray-painter-angle-rotation-90-sub'}" Name="SubAngleButton" />
|
||||
<Button Text="{Loc 'spray-painter-angle-rotation-reset'}" Name="SetZeroAngleButton" />
|
||||
<Button Text="{Loc 'spray-painter-angle-rotation-90-add'}" Name="AddAngleButton" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:SprayPainterDecals>
|
||||
174
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs
Normal file
174
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Decals;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.SprayPainter.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Used to control decal painting parameters for the spray painter.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SprayPainterDecals : Control
|
||||
{
|
||||
public Action<ProtoId<DecalPrototype>>? OnDecalSelected;
|
||||
public Action<Color?>? OnColorChanged;
|
||||
public Action<int>? OnAngleChanged;
|
||||
public Action<bool>? OnSnapChanged;
|
||||
|
||||
private SpriteSystem? _sprite;
|
||||
private string _selectedDecal = string.Empty;
|
||||
private List<SprayPainterDecalEntry> _decals = [];
|
||||
|
||||
public SprayPainterDecals()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90;
|
||||
SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90;
|
||||
SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0;
|
||||
AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value);
|
||||
|
||||
UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed;
|
||||
SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed;
|
||||
ColorSelector.OnColorChanged += OnColorSelected;
|
||||
}
|
||||
|
||||
private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
|
||||
{
|
||||
OnColorChanged?.Invoke(UseCustomColorCheckBox.Pressed ? ColorSelector.Color : null);
|
||||
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
|
||||
}
|
||||
|
||||
private void SnapToTileCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
|
||||
{
|
||||
OnSnapChanged?.Invoke(SnapToTileCheckBox.Pressed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the decal list.
|
||||
/// </summary>
|
||||
public void PopulateDecals(List<SprayPainterDecalEntry> decals, SpriteSystem sprite)
|
||||
{
|
||||
_sprite ??= sprite;
|
||||
|
||||
_decals = decals;
|
||||
DecalsGrid.Children.Clear();
|
||||
|
||||
foreach (var decal in decals)
|
||||
{
|
||||
var button = new TextureButton()
|
||||
{
|
||||
TextureNormal = sprite.Frame0(decal.Sprite),
|
||||
Name = decal.Name,
|
||||
ToolTip = decal.Name,
|
||||
Scale = new Vector2(2, 2),
|
||||
};
|
||||
button.OnPressed += DecalButtonOnPressed;
|
||||
|
||||
if (UseCustomColorCheckBox.Pressed)
|
||||
{
|
||||
button.Modulate = ColorSelector.Color;
|
||||
}
|
||||
|
||||
if (_selectedDecal == decal.Name)
|
||||
{
|
||||
var panelContainer = new PanelContainer()
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat()
|
||||
{
|
||||
BackgroundColor = StyleNano.ButtonColorDefault,
|
||||
},
|
||||
Children =
|
||||
{
|
||||
button,
|
||||
},
|
||||
};
|
||||
DecalsGrid.AddChild(panelContainer);
|
||||
}
|
||||
else
|
||||
{
|
||||
DecalsGrid.AddChild(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnColorSelected(Color color)
|
||||
{
|
||||
if (!UseCustomColorCheckBox.Pressed)
|
||||
return;
|
||||
|
||||
OnColorChanged?.Invoke(color);
|
||||
|
||||
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
|
||||
}
|
||||
|
||||
private void UpdateColorButtons(bool apply)
|
||||
{
|
||||
Color modulateColor = apply ? ColorSelector.Color : Color.White;
|
||||
foreach (var button in DecalsGrid.Children)
|
||||
{
|
||||
switch (button)
|
||||
{
|
||||
case TextureButton:
|
||||
button.Modulate = modulateColor;
|
||||
break;
|
||||
case PanelContainer panelContainer:
|
||||
{
|
||||
foreach (TextureButton textureButton in panelContainer.Children)
|
||||
textureButton.Modulate = modulateColor;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DecalButtonOnPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
if (obj.Button.Name is not { } name)
|
||||
return;
|
||||
|
||||
_selectedDecal = name;
|
||||
OnDecalSelected?.Invoke(_selectedDecal);
|
||||
|
||||
if (_sprite is null)
|
||||
return;
|
||||
|
||||
PopulateDecals(_decals, _sprite);
|
||||
}
|
||||
|
||||
public void SetSelectedDecal(string name)
|
||||
{
|
||||
_selectedDecal = name;
|
||||
|
||||
if (_sprite is null)
|
||||
return;
|
||||
|
||||
PopulateDecals(_decals, _sprite);
|
||||
}
|
||||
|
||||
public void SetAngle(int degrees)
|
||||
{
|
||||
AngleSpinBox.OverrideValue(degrees);
|
||||
}
|
||||
|
||||
public void SetColor(Color? color)
|
||||
{
|
||||
UseCustomColorCheckBox.Pressed = color != null;
|
||||
if (color != null)
|
||||
ColorSelector.Color = color.Value;
|
||||
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
|
||||
}
|
||||
|
||||
public void SetSnap(bool snap)
|
||||
{
|
||||
SnapToTileCheckBox.Pressed = snap;
|
||||
}
|
||||
}
|
||||
12
Content.Client/SprayPainter/UI/SprayPainterGroup.xaml
Normal file
12
Content.Client/SprayPainter/UI/SprayPainterGroup.xaml
Normal file
@@ -0,0 +1,12 @@
|
||||
<BoxContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Orientation="Vertical">
|
||||
<Label Text="{Loc 'spray-painter-selected-style'}" />
|
||||
<controls:ListContainer
|
||||
Name="StyleList"
|
||||
Toggle="True"
|
||||
Group="True">
|
||||
<!-- populated by code -->
|
||||
</controls:ListContainer>
|
||||
</BoxContainer>
|
||||
66
Content.Client/SprayPainter/UI/SprayPainterGroup.xaml.cs
Normal file
66
Content.Client/SprayPainter/UI/SprayPainterGroup.xaml.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.SprayPainter.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Used to display a group of paintable styles in the spray painter menu.
|
||||
/// (e.g. each type of paintable locker or plastic crate)
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SprayPainterGroup : BoxContainer
|
||||
{
|
||||
public event Action<SpriteListData>? OnButtonPressed;
|
||||
|
||||
public SprayPainterGroup()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
StyleList.GenerateItem = GenerateItems;
|
||||
}
|
||||
|
||||
public void PopulateList(List<SpriteListData> spriteList)
|
||||
{
|
||||
StyleList.PopulateList(spriteList);
|
||||
}
|
||||
|
||||
public void SelectItemByStyle(string key)
|
||||
{
|
||||
foreach (var elem in StyleList.Data)
|
||||
{
|
||||
if (elem is not SpriteListData spriteElem)
|
||||
continue;
|
||||
|
||||
if (spriteElem.Style == key)
|
||||
{
|
||||
StyleList.Select(spriteElem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void GenerateItems(ListData data, ListContainerButton button)
|
||||
{
|
||||
if (data is not SpriteListData spriteListData)
|
||||
return;
|
||||
|
||||
var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
|
||||
var protoView = new EntityPrototypeView();
|
||||
protoView.SetPrototype(spriteListData.Prototype);
|
||||
var label = new Label()
|
||||
{
|
||||
Text = Loc.GetString($"spray-painter-style-{spriteListData.Group.ToLower()}-{spriteListData.Style.ToLower()}")
|
||||
};
|
||||
|
||||
box.AddChild(protoView);
|
||||
box.AddChild(label);
|
||||
button.AddChild(box);
|
||||
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
|
||||
button.OnPressed += _ => OnButtonPressed?.Invoke(spriteListData);
|
||||
|
||||
if (spriteListData.SelectedIndex == button.Index)
|
||||
button.Pressed = true;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,6 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
MinSize="500 300"
|
||||
SetSize="500 500"
|
||||
Title="{Loc 'spray-painter-window-title'}">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="4"
|
||||
MinWidth="450">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="4"
|
||||
MinWidth="200">
|
||||
<Label Name="SelectedSpriteLabel"
|
||||
Text="{Loc 'spray-painter-selected-style'}">
|
||||
</Label>
|
||||
<ItemList Name="SpriteList"
|
||||
SizeFlagsStretchRatio="8"
|
||||
VerticalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="4"
|
||||
MinWidth="200">
|
||||
<Label Name="SelectedColorLabel"
|
||||
Text="{Loc 'spray-painter-selected-color'}"/>
|
||||
<ItemList Name="ColorList"
|
||||
SizeFlagsStretchRatio="8"
|
||||
VerticalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
MinSize="520 300"
|
||||
SetSize="520 700"
|
||||
Title="{Loc 'spray-painter-window-title'}">
|
||||
<TabContainer Name="Tabs"/>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Decals;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.SprayPainter.UI;
|
||||
|
||||
/// <summary>
|
||||
/// A window to select spray painter settings by object type, as well as pipe colours and decals.
|
||||
/// </summary>
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
{
|
||||
@@ -15,13 +22,33 @@ public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
public Action<ItemList.ItemListSelectedEventArgs>? OnSpritePicked;
|
||||
public Action<ItemList.ItemListSelectedEventArgs>? OnColorPicked;
|
||||
// Events
|
||||
public event Action<string, string>? OnSpritePicked;
|
||||
public event Action<int, bool>? OnTabChanged;
|
||||
public event Action<ProtoId<DecalPrototype>>? OnDecalChanged;
|
||||
public event Action<ItemList.ItemListSelectedEventArgs>? OnSetPipeColor;
|
||||
public event Action<Color?>? OnDecalColorChanged;
|
||||
public event Action<int>? OnDecalAngleChanged;
|
||||
public event Action<bool>? OnDecalSnapChanged;
|
||||
|
||||
// Pipe color data
|
||||
private ItemList _colorList = default!;
|
||||
public Dictionary<string, int> ItemColorIndex = new();
|
||||
|
||||
private Dictionary<string, Color> currentPalette = new();
|
||||
private const string colorLocKeyPrefix = "pipe-painter-color-";
|
||||
private List<SprayPainterEntry> CurrentEntries = new List<SprayPainterEntry>();
|
||||
private Dictionary<string, Color> _currentPalette = new();
|
||||
private const string ColorLocKeyPrefix = "pipe-painter-color-";
|
||||
|
||||
// Paintable objects
|
||||
private Dictionary<string, Dictionary<string, EntProtoId>> _currentStylesByGroup = new();
|
||||
private Dictionary<string, List<string>> _currentGroupsByCategory = new();
|
||||
|
||||
// Tab controls
|
||||
private Dictionary<string, SprayPainterGroup> _paintableControls = new();
|
||||
private BoxContainer? _pipeControl;
|
||||
|
||||
// Decals
|
||||
private List<SprayPainterDecalEntry> _currentDecals = [];
|
||||
private SprayPainterDecals? _sprayPainterDecals;
|
||||
|
||||
private readonly SpriteSpecifier _colorEntryIconTexture = new SpriteSpecifier.Rsi(
|
||||
new ResPath("Structures/Piping/Atmospherics/pipe.rsi"),
|
||||
@@ -32,13 +59,14 @@ public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_spriteSystem = _sysMan.GetEntitySystem<SpriteSystem>();
|
||||
Tabs.OnTabChanged += (index) => OnTabChanged?.Invoke(index, _sprayPainterDecals?.GetPositionInParent() == index);
|
||||
}
|
||||
|
||||
private string GetColorLocString(string? colorKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(colorKey))
|
||||
return Loc.GetString("pipe-painter-no-color-selected");
|
||||
var locKey = colorLocKeyPrefix + colorKey;
|
||||
var locKey = ColorLocKeyPrefix + colorKey;
|
||||
|
||||
if (!_loc.TryGetString(locKey, out var locString))
|
||||
locString = colorKey;
|
||||
@@ -48,51 +76,229 @@ public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
|
||||
public string? IndexToColorKey(int index)
|
||||
{
|
||||
return (string?) ColorList[index].Metadata;
|
||||
return _colorList[index].Text;
|
||||
}
|
||||
|
||||
public void Populate(List<SprayPainterEntry> entries, int selectedStyle, string? selectedColorKey, Dictionary<string, Color> palette)
|
||||
private void OnStyleSelected(ListData data)
|
||||
{
|
||||
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
|
||||
if (!CurrentEntries.Equals(entries))
|
||||
if (data is SpriteListData listData)
|
||||
OnSpritePicked?.Invoke(listData.Group, listData.Style);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper to allow for selecting/deselecting the event to avoid loops
|
||||
/// </summary>
|
||||
private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
|
||||
{
|
||||
OnSetPipeColor?.Invoke(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Setup function for the window.
|
||||
/// </summary>
|
||||
/// <param name="stylesByGroup">Each group, mapped by name to the set of named styles by their associated entity prototype.</param>
|
||||
/// <param name="groupsByCategory">The set of categories and the groups associated with them.</param>
|
||||
/// <param name="decals">A list of each decal.</param>
|
||||
public void PopulateCategories(Dictionary<string, Dictionary<string, EntProtoId>> stylesByGroup, Dictionary<string, List<string>> groupsByCategory, List<SprayPainterDecalEntry> decals)
|
||||
{
|
||||
bool tabsCleared = false;
|
||||
var lastTab = Tabs.CurrentTab;
|
||||
|
||||
if (!_currentGroupsByCategory.Equals(groupsByCategory))
|
||||
{
|
||||
CurrentEntries = entries;
|
||||
SpriteList.Clear();
|
||||
foreach (var entry in entries)
|
||||
// Destroy all existing tabs
|
||||
tabsCleared = true;
|
||||
_paintableControls.Clear();
|
||||
_pipeControl = null;
|
||||
_sprayPainterDecals = null;
|
||||
Tabs.RemoveAllChildren();
|
||||
}
|
||||
|
||||
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
|
||||
if (tabsCleared || !_currentStylesByGroup.Equals(stylesByGroup))
|
||||
{
|
||||
_currentStylesByGroup = stylesByGroup;
|
||||
|
||||
var tabIndex = 0;
|
||||
foreach (var (categoryName, categoryGroups) in groupsByCategory.OrderBy(c => c.Key))
|
||||
{
|
||||
SpriteList.AddItem(entry.Name, entry.Icon);
|
||||
if (categoryGroups.Count <= 0)
|
||||
continue;
|
||||
|
||||
// Repopulating controls:
|
||||
// ensure that categories with multiple groups have separate subtabs
|
||||
// but single-group categories do not.
|
||||
if (tabsCleared)
|
||||
{
|
||||
TabContainer? subTabs = null;
|
||||
if (categoryGroups.Count > 1)
|
||||
subTabs = new();
|
||||
|
||||
foreach (var group in categoryGroups)
|
||||
{
|
||||
if (!stylesByGroup.TryGetValue(group, out var styles))
|
||||
continue;
|
||||
|
||||
var groupControl = new SprayPainterGroup();
|
||||
groupControl.OnButtonPressed += OnStyleSelected;
|
||||
_paintableControls[group] = groupControl;
|
||||
if (categoryGroups.Count > 1)
|
||||
{
|
||||
if (subTabs != null)
|
||||
{
|
||||
subTabs?.AddChild(groupControl);
|
||||
var subTabLocalization = Loc.GetString("spray-painter-tab-group-" + group.ToLower());
|
||||
TabContainer.SetTabTitle(groupControl, subTabLocalization);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Tabs.AddChild(groupControl);
|
||||
}
|
||||
}
|
||||
|
||||
if (subTabs != null)
|
||||
Tabs.AddChild(subTabs);
|
||||
|
||||
var tabLocalization = Loc.GetString("spray-painter-tab-category-" + categoryName.ToLower());
|
||||
Tabs.SetTabTitle(tabIndex, tabLocalization);
|
||||
tabIndex++;
|
||||
}
|
||||
|
||||
// Finally, populate all groups with new data.
|
||||
foreach (var group in categoryGroups)
|
||||
{
|
||||
if (!stylesByGroup.TryGetValue(group, out var styles) ||
|
||||
!_paintableControls.TryGetValue(group, out var control))
|
||||
continue;
|
||||
|
||||
var dataList = styles
|
||||
.Select(e => new SpriteListData(group, e.Key, e.Value, 0))
|
||||
.OrderBy(d => Loc.GetString($"spray-painter-style-{group.ToLower()}-{d.Style.ToLower()}"))
|
||||
.ToList();
|
||||
control.PopulateList(dataList);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentPalette.Equals(palette))
|
||||
{
|
||||
currentPalette = palette;
|
||||
ItemColorIndex.Clear();
|
||||
ColorList.Clear();
|
||||
PopulateColors(_currentPalette);
|
||||
|
||||
if (!_currentDecals.Equals(decals))
|
||||
{
|
||||
_currentDecals = decals;
|
||||
|
||||
if (_sprayPainterDecals is null)
|
||||
{
|
||||
_sprayPainterDecals = new SprayPainterDecals();
|
||||
|
||||
_sprayPainterDecals.OnDecalSelected += id => OnDecalChanged?.Invoke(id);
|
||||
_sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color);
|
||||
_sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle);
|
||||
_sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap);
|
||||
|
||||
Tabs.AddChild(_sprayPainterDecals);
|
||||
TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals"));
|
||||
}
|
||||
|
||||
_sprayPainterDecals.PopulateDecals(decals, _spriteSystem);
|
||||
}
|
||||
|
||||
if (tabsCleared)
|
||||
SetSelectedTab(lastTab);
|
||||
}
|
||||
|
||||
public void PopulateColors(Dictionary<string, Color> palette)
|
||||
{
|
||||
// Create pipe tab controls if they don't exist
|
||||
bool tabCreated = false;
|
||||
if (_pipeControl == null)
|
||||
{
|
||||
_pipeControl = new BoxContainer() { Orientation = BoxContainer.LayoutOrientation.Vertical };
|
||||
|
||||
var label = new Label() { Text = Loc.GetString("spray-painter-selected-color") };
|
||||
|
||||
_colorList = new ItemList() { VerticalExpand = true };
|
||||
_colorList.OnItemSelected += OnColorPicked;
|
||||
|
||||
_pipeControl.AddChild(label);
|
||||
_pipeControl.AddChild(_colorList);
|
||||
|
||||
Tabs.AddChild(_pipeControl);
|
||||
TabContainer.SetTabTitle(_pipeControl, Loc.GetString("spray-painter-tab-category-pipes"));
|
||||
tabCreated = true;
|
||||
}
|
||||
|
||||
// Populate the tab if needed (new tab/new data)
|
||||
if (tabCreated || !_currentPalette.Equals(palette))
|
||||
{
|
||||
_currentPalette = palette;
|
||||
ItemColorIndex.Clear();
|
||||
_colorList.Clear();
|
||||
|
||||
int index = 0;
|
||||
foreach (var color in palette)
|
||||
{
|
||||
var locString = GetColorLocString(color.Key);
|
||||
var item = ColorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture));
|
||||
var item = _colorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture), metadata: color.Key);
|
||||
item.IconModulate = color.Value;
|
||||
item.Metadata = color.Key;
|
||||
|
||||
ItemColorIndex.Add(color.Key, ColorList.IndexOf(item));
|
||||
ItemColorIndex.Add(color.Key, index);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
// Disable event so we don't send a new event for pre-selectedStyle entry and end up in a loop
|
||||
|
||||
if (selectedColorKey != null)
|
||||
{
|
||||
var index = ItemColorIndex[selectedColorKey];
|
||||
ColorList.OnItemSelected -= OnColorPicked;
|
||||
ColorList[index].Selected = true;
|
||||
ColorList.OnItemSelected += OnColorPicked;
|
||||
}
|
||||
|
||||
SpriteList.OnItemSelected -= OnSpritePicked;
|
||||
SpriteList[selectedStyle].Selected = true;
|
||||
SpriteList.OnItemSelected += OnSpritePicked;
|
||||
}
|
||||
|
||||
# region Setters
|
||||
public void SetSelectedStyles(Dictionary<string, string> selectedStyles)
|
||||
{
|
||||
foreach (var (group, style) in selectedStyles)
|
||||
{
|
||||
if (!_paintableControls.TryGetValue(group, out var control))
|
||||
continue;
|
||||
|
||||
control.SelectItemByStyle(style);
|
||||
}
|
||||
}
|
||||
|
||||
public void SelectColor(string color)
|
||||
{
|
||||
if (_colorList != null && ItemColorIndex.TryGetValue(color, out var colorIdx))
|
||||
{
|
||||
_colorList.OnItemSelected -= OnColorPicked;
|
||||
_colorList[colorIdx].Selected = true;
|
||||
_colorList.OnItemSelected += OnColorPicked;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSelectedTab(int tab)
|
||||
{
|
||||
Tabs.CurrentTab = int.Min(tab, Tabs.ChildCount - 1);
|
||||
}
|
||||
|
||||
public void SetSelectedDecal(string decal)
|
||||
{
|
||||
if (_sprayPainterDecals != null)
|
||||
_sprayPainterDecals.SetSelectedDecal(decal);
|
||||
}
|
||||
|
||||
public void SetDecalAngle(int angle)
|
||||
{
|
||||
if (_sprayPainterDecals != null)
|
||||
_sprayPainterDecals.SetAngle(angle);
|
||||
}
|
||||
|
||||
public void SetDecalColor(Color? color)
|
||||
{
|
||||
if (_sprayPainterDecals != null)
|
||||
_sprayPainterDecals.SetColor(color);
|
||||
}
|
||||
|
||||
public void SetDecalSnap(bool snap)
|
||||
{
|
||||
if (_sprayPainterDecals != null)
|
||||
_sprayPainterDecals.SetSnap(snap);
|
||||
}
|
||||
# endregion
|
||||
}
|
||||
|
||||
public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData;
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Content.Shared.StatusEffectNew.Components;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Client.StatusEffectNew;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed partial class ClientStatusEffectsSystem : SharedStatusEffectsSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StatusEffectContainerComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<StatusEffectContainerComponent> ent, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not StatusEffectContainerComponentState state)
|
||||
return;
|
||||
|
||||
var toRemove = new ValueList<EntityUid>();
|
||||
foreach (var effect in ent.Comp.ActiveStatusEffects)
|
||||
{
|
||||
if (state.ActiveStatusEffects.Contains(GetNetEntity(effect)))
|
||||
continue;
|
||||
|
||||
toRemove.Add(effect);
|
||||
}
|
||||
|
||||
foreach (var effect in toRemove)
|
||||
{
|
||||
ent.Comp.ActiveStatusEffects.Remove(effect);
|
||||
var ev = new StatusEffectRemovedEvent(ent);
|
||||
RaiseLocalEvent(effect, ref ev);
|
||||
}
|
||||
|
||||
foreach (var effect in state.ActiveStatusEffects)
|
||||
{
|
||||
var effectUid = GetEntity(effect);
|
||||
if (ent.Comp.ActiveStatusEffects.Contains(effectUid))
|
||||
continue;
|
||||
|
||||
ent.Comp.ActiveStatusEffects.Add(effectUid);
|
||||
var ev = new StatusEffectAppliedEvent(ent);
|
||||
RaiseLocalEvent(effectUid, ref ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
using Content.Shared.SprayPainter.Prototypes;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Storage.Visualizers;
|
||||
|
||||
public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStorageVisualsComponent>
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IComponentFactory _componentFactory = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -26,12 +31,34 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
|
||||
SpriteSystem.LayerSetRsiState((uid, sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, EntityStorageVisualsComponent comp, ref AppearanceChangeEvent args)
|
||||
protected override void OnAppearanceChange(EntityUid uid,
|
||||
EntityStorageVisualsComponent comp,
|
||||
ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null
|
||||
|| !AppearanceSystem.TryGetData<bool>(uid, StorageVisuals.Open, out var open, args.Component))
|
||||
|| !AppearanceSystem.TryGetData<bool>(uid, StorageVisuals.Open, out var open, args.Component))
|
||||
return;
|
||||
|
||||
var forceRedrawBase = false;
|
||||
if (AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
|
||||
{
|
||||
if (_prototypeManager.TryIndex(prototype, out var proto))
|
||||
{
|
||||
if (proto.TryGetComponent(out SpriteComponent? sprite, _componentFactory))
|
||||
{
|
||||
SpriteSystem.SetBaseRsi((uid, args.Sprite), sprite.BaseRSI);
|
||||
}
|
||||
if (proto.TryGetComponent(out EntityStorageVisualsComponent? visuals, _componentFactory))
|
||||
{
|
||||
comp.StateBaseOpen = visuals.StateBaseOpen;
|
||||
comp.StateBaseClosed = visuals.StateBaseClosed;
|
||||
comp.StateDoorOpen = visuals.StateDoorOpen;
|
||||
comp.StateDoorClosed = visuals.StateDoorClosed;
|
||||
forceRedrawBase = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open/Closed state for the storage entity.
|
||||
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), StorageVisualLayers.Door, out _, false))
|
||||
{
|
||||
@@ -52,6 +79,8 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
|
||||
|
||||
if (comp.StateBaseOpen != null)
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
|
||||
else if (forceRedrawBase && comp.StateBaseClosed != null)
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -68,6 +97,8 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
|
||||
|
||||
if (comp.StateBaseClosed != null)
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
|
||||
else if (forceRedrawBase && comp.StateBaseOpen != null)
|
||||
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,182 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Stunnable;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Client.Stunnable
|
||||
namespace Content.Client.Stunnable;
|
||||
|
||||
public sealed class StunSystem : SharedStunSystem
|
||||
{
|
||||
public sealed class StunSystem : SharedStunSystem
|
||||
{
|
||||
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
|
||||
|
||||
private readonly int[] _sign = [-1, 1];
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged);
|
||||
|
||||
CommandBinds.Builder
|
||||
.BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true), typeof(SharedInteractionSystem))
|
||||
.Register<StunSystem>();
|
||||
}
|
||||
|
||||
private bool OnUseSecondary(in PointerInputCmdHandler.PointerInputCmdArgs args)
|
||||
{
|
||||
if (args.Session?.AttachedEntity is not {Valid: true} uid)
|
||||
return false;
|
||||
|
||||
if (args.EntityUid != uid || !HasComp<KnockedDownComponent>(uid) || !_combat.IsInCombatMode(uid))
|
||||
return false;
|
||||
|
||||
RaisePredictiveEvent(new ForceStandUpEvent());
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add stun visual layers
|
||||
/// </summary>
|
||||
private void OnComponentInit(Entity<StunVisualsComponent> entity, ref ComponentInit args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(entity, out var sprite))
|
||||
return;
|
||||
|
||||
var spriteEntity = (entity.Owner, sprite);
|
||||
|
||||
_spriteSystem.LayerMapReserve(spriteEntity, StunVisualLayers.StamCrit);
|
||||
_spriteSystem.LayerSetVisible(spriteEntity, StunVisualLayers.StamCrit, false);
|
||||
_spriteSystem.LayerSetOffset(spriteEntity, StunVisualLayers.StamCrit, new Vector2(0, 0.3125f));
|
||||
|
||||
_spriteSystem.LayerSetRsi(spriteEntity, StunVisualLayers.StamCrit, entity.Comp.StarsPath);
|
||||
|
||||
UpdateAppearance((entity, sprite), entity.Comp.State);
|
||||
}
|
||||
|
||||
private void OnAppearanceChanged(Entity<StunVisualsComponent> entity, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite != null)
|
||||
UpdateAppearance((entity, args.Sprite), entity.Comp.State);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(Entity<SpriteComponent?> entity, string state)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp))
|
||||
return;
|
||||
|
||||
if (!_spriteSystem.LayerMapTryGet((entity, entity.Comp), StunVisualLayers.StamCrit, out var index, false))
|
||||
return;
|
||||
|
||||
var visible = Appearance.TryGetData<bool>(entity, StunVisuals.SeeingStars, out var stars) && stars;
|
||||
|
||||
_spriteSystem.LayerSetVisible((entity, entity.Comp), index, visible);
|
||||
_spriteSystem.LayerSetRsiState((entity, entity.Comp), index, state);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple fatigue animation, a mild modification of the jittering animation. The animation constructor is
|
||||
/// quite complex, but that's because the AnimationSystem doesn't have proper adjustment layers. In a potential
|
||||
/// future where proper adjustment layers are added feel free to clean this up to be an animation with two adjustment
|
||||
/// layers rather than one mega layer.
|
||||
/// </summary>
|
||||
/// <param name="sprite">The spriteComponent we're adjusting the offset of</param>
|
||||
/// <param name="frequency">How many times per second does the animation run?</param>
|
||||
/// <param name="jitters">How many times should we jitter during the animation? Also determines breathing frequency</param>
|
||||
/// <param name="minJitter">Mininum jitter offset multiplier for X and Y directions</param>
|
||||
/// <param name="maxJitter">Maximum jitter offset multiplier for X and Y directions</param>
|
||||
/// <param name="breathing">Maximum breathing offset, this is in the Y direction</param>
|
||||
/// <param name="startOffset">Starting offset because we don't have adjustment layers</param>
|
||||
/// <param name="lastJitter">Last jitter so we don't jitter to the same quadrant</param>
|
||||
/// <returns></returns>
|
||||
public Animation GetFatigueAnimation(SpriteComponent sprite,
|
||||
float frequency,
|
||||
int jitters,
|
||||
Vector2 minJitter,
|
||||
Vector2 maxJitter,
|
||||
float breathing,
|
||||
Vector2 startOffset,
|
||||
ref Vector2 lastJitter)
|
||||
{
|
||||
// avoid animations with negative length or infinite length
|
||||
if (frequency <= 0)
|
||||
return new Animation();
|
||||
|
||||
var breaths = new Vector2(0, breathing * 2) / jitters;
|
||||
|
||||
var length = 1 / frequency;
|
||||
var frames = length / jitters;
|
||||
|
||||
var keyFrames = new List<AnimationTrackProperty.KeyFrame> { new(sprite.Offset, 0f) };
|
||||
|
||||
// Spits out a list of keyframes to feed to the AnimationPlayer based on the variables we've inputted
|
||||
for (var i = 1; i <= jitters; i++)
|
||||
{
|
||||
var offset = new Vector2(_random.NextFloat(minJitter.X, maxJitter.X),
|
||||
_random.NextFloat(minJitter.Y, maxJitter.Y));
|
||||
offset.X *= _random.Pick(_sign);
|
||||
offset.Y *= _random.Pick(_sign);
|
||||
|
||||
if (i == 1 && Math.Sign(offset.X) == Math.Sign(lastJitter.X)
|
||||
&& Math.Sign(offset.Y) == Math.Sign(lastJitter.Y))
|
||||
{
|
||||
// If the sign is the same as last time on both axis we flip one randomly
|
||||
// to avoid jitter staying in one quadrant too much.
|
||||
if (_random.Prob(0.5f))
|
||||
offset.X *= -1;
|
||||
else
|
||||
offset.Y *= -1;
|
||||
}
|
||||
|
||||
lastJitter = offset;
|
||||
|
||||
// For the first half of the jitter, we vertically displace the sprite upwards to simulate breathing in
|
||||
if (i <= jitters / 2)
|
||||
{
|
||||
keyFrames.Add(new AnimationTrackProperty.KeyFrame(startOffset + breaths * i + offset, frames));
|
||||
}
|
||||
// For the next quarter we displace the sprite down, to about 12.5% breathing offset below our starting position
|
||||
// Simulates breathing out
|
||||
else if (i < jitters * 3 / 4)
|
||||
{
|
||||
keyFrames.Add(
|
||||
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( jitters - i * 1.5f ) + offset, frames));
|
||||
}
|
||||
// Return to our starting position for breathing, jitter reaches its final position
|
||||
else
|
||||
{
|
||||
keyFrames.Add(
|
||||
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( i - jitters ) + offset, frames));
|
||||
}
|
||||
}
|
||||
|
||||
return new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(length),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty
|
||||
{
|
||||
// Heavy Breathing
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Offset),
|
||||
InterpolationMode = AnimationInterpolationMode.Cubic,
|
||||
KeyFrames = keyFrames,
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum StunVisualLayers : byte
|
||||
{
|
||||
StamCrit,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Mobs;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
@@ -43,7 +44,7 @@ public sealed class ProgressColorSystem : EntitySystem
|
||||
|
||||
// lerp
|
||||
var hue = 5f / 18f * progress;
|
||||
return Color.FromHsv((hue, 1f, 0.75f, 1f));
|
||||
return Color.FromHsv(new Vector4(hue, 1f, 0.75f, 1f));
|
||||
}
|
||||
|
||||
return InterpolateColorGaussian(Plasma, progress);
|
||||
|
||||
@@ -185,7 +185,12 @@ public sealed class ItemGridPiece : Control, IEntityControl
|
||||
|
||||
handle.SetTransform(pos, iconRotation);
|
||||
var box = new UIBox2(root, root + sprite.Size * scale);
|
||||
handle.DrawTextureRect(sprite, box);
|
||||
|
||||
var ev = new BeforeRenderInGridEvent(new Color(255, 255, 255));
|
||||
_entityManager.EventBus.RaiseLocalEvent(Entity, ev);
|
||||
|
||||
handle.DrawTextureRect(sprite, box, ev.Color);
|
||||
|
||||
handle.SetTransform(GlobalPixelPosition, Angle.Zero);
|
||||
}
|
||||
else
|
||||
@@ -298,6 +303,19 @@ public sealed class ItemGridPiece : Control, IEntityControl
|
||||
public EntityUid? UiEntity => Entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This event gets raised before a sprite gets drawn in a grid and lets to change the sprite color for several gameobjects that have special sprites to render in containers.
|
||||
/// </summary>
|
||||
public sealed class BeforeRenderInGridEvent : EntityEventArgs
|
||||
{
|
||||
public Color Color { get; set; }
|
||||
|
||||
public BeforeRenderInGridEvent(Color color)
|
||||
{
|
||||
Color = color;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ItemGridPieceMarks
|
||||
{
|
||||
First,
|
||||
|
||||
@@ -621,7 +621,7 @@ public sealed class StorageWindow : BaseWindow
|
||||
{
|
||||
marked.Add(cell);
|
||||
cell.ModulateSelfOverride = spotFree
|
||||
? Color.FromHsv((0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
|
||||
? Color.FromHsv(new Vector4(0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
|
||||
: Color.FromHex("#2222CC");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace Content.IntegrationTests.Tests
|
||||
}
|
||||
});
|
||||
|
||||
await server.WaitRunTicks(15);
|
||||
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
|
||||
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
@@ -112,7 +112,7 @@ namespace Content.IntegrationTests.Tests
|
||||
entityMan.SpawnEntity(protoId, map.GridCoords);
|
||||
}
|
||||
});
|
||||
await server.WaitRunTicks(15);
|
||||
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
static IEnumerable<(EntityUid, TComp)> Query<TComp>(IEntityManager entityMan)
|
||||
@@ -270,7 +270,7 @@ namespace Content.IntegrationTests.Tests
|
||||
await pair.RunTicksSync(3);
|
||||
|
||||
// We consider only non-audio entities, as some entities will just play sounds when they spawn.
|
||||
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
|
||||
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
|
||||
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(entMan.HasComponent<AudioComponent>);
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -17,9 +17,7 @@ namespace Content.IntegrationTests.Tests
|
||||
components:
|
||||
- type: Inventory
|
||||
- type: ContainerContainer
|
||||
- type: StatusEffects
|
||||
allowed:
|
||||
- Stun
|
||||
- type: MobState
|
||||
|
||||
- type: entity
|
||||
name: InventoryJumpsuitJanitorDummy
|
||||
@@ -70,7 +68,7 @@ namespace Content.IntegrationTests.Tests
|
||||
});
|
||||
#pragma warning restore NUnit2045
|
||||
|
||||
systemMan.GetEntitySystem<StunSystem>().TryStun(human, TimeSpan.FromSeconds(1f), true);
|
||||
systemMan.GetEntitySystem<StunSystem>().TryUpdateStunDuration(human, TimeSpan.FromSeconds(1f));
|
||||
|
||||
#pragma warning disable NUnit2045
|
||||
// Since the mob is stunned, they can't equip this.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
@@ -339,7 +339,7 @@ public sealed partial class MindTests
|
||||
var entMan = server.ResolveDependency<IServerEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<SharedMindSystem>();
|
||||
var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
|
||||
|
||||
EntityUid entity = default!;
|
||||
EntityUid mindId = default!;
|
||||
@@ -379,7 +379,7 @@ public sealed partial class MindTests
|
||||
|
||||
mob = entMan.SpawnEntity(null, new MapCoordinates());
|
||||
|
||||
MakeSentientCommand.MakeSentient(mob, entMan);
|
||||
mindSystem.MakeSentient(mob);
|
||||
mobMindId = mindSystem.CreateMind(player.UserId, "Mindy McThinker the Second");
|
||||
mobMind = entMan.GetComponent<MindComponent>(mobMindId);
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Threading;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Shared.CCVar;
|
||||
@@ -22,7 +21,7 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
private void OnRoundEnd(RoundEndSystemChangedEvent ev)
|
||||
{
|
||||
Interlocked.Increment(ref RoundCount);
|
||||
RoundCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,13 +126,17 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
async Task WaitForEvent()
|
||||
{
|
||||
var timeout = Task.Delay(TimeSpan.FromSeconds(10));
|
||||
var currentCount = Thread.VolatileRead(ref sys.RoundCount);
|
||||
while (currentCount == Thread.VolatileRead(ref sys.RoundCount) && !timeout.IsCompleted)
|
||||
const int maxTicks = 60;
|
||||
var currentCount = sys.RoundCount;
|
||||
for (var i = 0; i < maxTicks; i++)
|
||||
{
|
||||
await pair.RunTicksSync(5);
|
||||
if (currentCount != sys.RoundCount)
|
||||
return;
|
||||
|
||||
await pair.RunTicksSync(1);
|
||||
}
|
||||
if (timeout.IsCompleted) throw new TimeoutException("Event took too long to trigger");
|
||||
|
||||
throw new TimeoutException("Event took too long to trigger");
|
||||
}
|
||||
|
||||
// Need to clean self up
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.SmartFridge;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.SmartFridge;
|
||||
|
||||
public sealed class SmartFridgeInteractionTest : InteractionTest
|
||||
{
|
||||
private const string SmartFridgeProtoId = "SmartFridge";
|
||||
private const string SampleItemProtoId = "FoodAmbrosiaVulgaris";
|
||||
private const string SampleDumpableAndInsertableId = "PillCanisterSomething";
|
||||
private const int SampleDumpableCount = 5;
|
||||
private const string SampleDumpableId = "ChemBagSomething";
|
||||
|
||||
[TestPrototypes]
|
||||
private const string TestPrototypes = $@"
|
||||
- type: entity
|
||||
parent: PillCanister
|
||||
id: {SampleDumpableAndInsertableId}
|
||||
components:
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: PillCopper
|
||||
amount: 5
|
||||
|
||||
- type: entity
|
||||
parent: ChemBag
|
||||
id: {SampleDumpableId}
|
||||
components:
|
||||
- type: StorageFill
|
||||
contents:
|
||||
- id: PillCopper
|
||||
amount: 5
|
||||
";
|
||||
|
||||
[Test]
|
||||
public async Task InsertAndDispenseItemTest()
|
||||
{
|
||||
await PlaceInHands(SampleItemProtoId);
|
||||
|
||||
await SpawnTarget(SmartFridgeProtoId);
|
||||
var fridge = SEntMan.GetEntity(Target.Value);
|
||||
var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
|
||||
|
||||
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
|
||||
// smartfridge spawns with nothing
|
||||
Assert.That(component.Entries, Is.Empty);
|
||||
await InteractUsing(SampleItemProtoId);
|
||||
|
||||
// smartfridge now has items
|
||||
Assert.That(component.Entries, Is.Not.Empty);
|
||||
Assert.That(component.ContainedEntries[component.Entries[0]], Is.Not.Empty);
|
||||
|
||||
// open the UI
|
||||
await Activate();
|
||||
Assert.That(IsUiOpen(SmartFridgeUiKey.Key));
|
||||
|
||||
// dispense an item
|
||||
await SendBui(SmartFridgeUiKey.Key, new SmartFridgeDispenseItemMessage(component.Entries[0]));
|
||||
|
||||
// assert that the listing is still there
|
||||
Assert.That(component.Entries, Is.Not.Empty);
|
||||
// but empty
|
||||
Assert.That(component.ContainedEntries[component.Entries[0]], Is.Empty);
|
||||
|
||||
// and that the thing we dispensed is actually around
|
||||
await AssertEntityLookup(
|
||||
("APCBasic", 1),
|
||||
(SampleItemProtoId, 1)
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InsertDumpableInsertableItemTest()
|
||||
{
|
||||
await PlaceInHands(SampleItemProtoId);
|
||||
|
||||
await SpawnTarget(SmartFridgeProtoId);
|
||||
var fridge = SEntMan.GetEntity(Target.Value);
|
||||
var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
|
||||
|
||||
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
|
||||
await InteractUsing(SampleDumpableAndInsertableId);
|
||||
|
||||
// smartfridge now has one item only
|
||||
Assert.That(component.Entries, Is.Not.Empty);
|
||||
Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task InsertDumpableItemTest()
|
||||
{
|
||||
await PlaceInHands(SampleItemProtoId);
|
||||
|
||||
await SpawnTarget(SmartFridgeProtoId);
|
||||
var fridge = SEntMan.GetEntity(Target.Value);
|
||||
var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
|
||||
|
||||
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
|
||||
await RunTicks(1);
|
||||
|
||||
await InteractUsing(SampleDumpableId);
|
||||
|
||||
// smartfridge now has N items
|
||||
Assert.That(component.Entries, Is.Not.Empty);
|
||||
Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(SampleDumpableCount));
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Slippery;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Tabletop.Components;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -48,6 +49,7 @@ using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
|
||||
@@ -877,7 +879,7 @@ public sealed partial class AdminVerbSystem
|
||||
if (!hadSlipComponent)
|
||||
{
|
||||
slipComponent.SlipData.SuperSlippery = true;
|
||||
slipComponent.SlipData.ParalyzeTime = TimeSpan.FromSeconds(5);
|
||||
slipComponent.SlipData.StunTime = TimeSpan.FromSeconds(5);
|
||||
slipComponent.SlipData.LaunchForwardsMultiplier = 20;
|
||||
}
|
||||
|
||||
@@ -922,5 +924,20 @@ public sealed partial class AdminVerbSystem
|
||||
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
|
||||
};
|
||||
args.Verbs.Add(omniaccent);
|
||||
|
||||
var crawlerName = Loc.GetString("admin-smite-crawler-name").ToLowerInvariant();
|
||||
Verb crawler = new()
|
||||
{
|
||||
Text = crawlerName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Mobs/Animals/snake.rsi"), "icon"),
|
||||
Act = () =>
|
||||
{
|
||||
EnsureComp<WormComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", crawlerName, Loc.GetString("admin-smite-crawler-description"))
|
||||
};
|
||||
args.Verbs.Add(crawler);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using Content.Server.Administration.UI;
|
||||
using Content.Server.Disposal.Tube;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Prayer;
|
||||
using Content.Server.Silicons.Laws;
|
||||
@@ -16,7 +15,6 @@ using Content.Shared.Configurable;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Movement.Components;
|
||||
@@ -458,7 +456,7 @@ namespace Content.Server.Administration.Systems
|
||||
Text = Loc.GetString("make-sentient-verb-get-data-text"),
|
||||
Category = VerbCategory.Debug,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/sentient.svg.192dpi.png")),
|
||||
Act = () => MakeSentientCommand.MakeSentient(args.Target, EntityManager),
|
||||
Act = () => _mindSystem.MakeSentient(args.Target),
|
||||
Impact = LogImpact.Medium
|
||||
};
|
||||
args.Verbs.Add(verb);
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed partial class ParrotMemoryComponent : Component
|
||||
/// The % chance an entity with this component learns a phrase when learning is off cooldown
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float LearnChance = 0.4f;
|
||||
public float LearnChance = 0.6f;
|
||||
|
||||
/// <summary>
|
||||
/// Time after which another attempt can be made at learning a phrase
|
||||
|
||||
@@ -96,7 +96,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
|
||||
EntityManager.AddComponents(ent, injectedAnom.Components);
|
||||
|
||||
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
|
||||
_stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration));
|
||||
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
|
||||
|
||||
if (ent.Comp.StartSound is not null)
|
||||
@@ -125,7 +125,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
|
||||
private void OnAnomalyPulse(Entity<InnerBodyAnomalyComponent> ent, ref AnomalyPulseEvent args)
|
||||
{
|
||||
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
|
||||
_stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity));
|
||||
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
|
||||
if (_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom))
|
||||
EntityManager.RemoveComponents(ent, injectedAnom.Components);
|
||||
|
||||
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
|
||||
_stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration));
|
||||
|
||||
if (ent.Comp.EndMessage is not null &&
|
||||
_mind.TryGetMind(ent, out _, out var mindComponent) &&
|
||||
|
||||
@@ -17,6 +17,7 @@ using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.NodeContainer;
|
||||
|
||||
@@ -53,6 +54,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
|
||||
|
||||
// Grid events
|
||||
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
|
||||
SubscribeLocalEvent<PipeNodeGroupRemovedEvent>(OnPipeNodeGroupRemoved);
|
||||
}
|
||||
|
||||
#region Event handling
|
||||
@@ -295,6 +297,25 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
|
||||
|
||||
#region Pipe net functions
|
||||
|
||||
private void OnPipeNodeGroupRemoved(ref PipeNodeGroupRemovedEvent args)
|
||||
{
|
||||
// When a pipe node group is removed, we need to iterate over all of
|
||||
// our pipe chunks and remove any entries with a matching net id.
|
||||
// (We only need to check the chunks for the affected grid, though.)
|
||||
|
||||
if (!_gridAtmosPipeChunks.TryGetValue(args.Grid, out var chunkData))
|
||||
return;
|
||||
|
||||
foreach (var chunk in chunkData.Values)
|
||||
{
|
||||
foreach (var key in chunk.AtmosPipeData.Keys)
|
||||
{
|
||||
if (key.NetId == args.NetId)
|
||||
chunk.AtmosPipeData.Remove(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid)
|
||||
{
|
||||
var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
|
||||
@@ -411,7 +432,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
|
||||
continue;
|
||||
|
||||
var netId = GetPipeNodeNetId(pipeNode);
|
||||
var subnet = new AtmosMonitoringConsoleSubnet(netId, pipeNode.CurrentPipeLayer, pipeColor.Color.ToHex());
|
||||
var subnet = new AtmosMonitoringConsoleSubnet(netId, pipeNode.CurrentPipeLayer, pipeColor.Color);
|
||||
var pipeDirection = pipeNode.CurrentPipeDirection;
|
||||
|
||||
chunk.AtmosPipeData.TryGetValue(subnet, out var atmosPipeData);
|
||||
|
||||
@@ -279,6 +279,14 @@ public partial class AtmosphereSystem
|
||||
|
||||
public bool RemovePipeNet(Entity<GridAtmosphereComponent?> grid, PipeNet pipeNet)
|
||||
{
|
||||
// Technically this event can be fired even on grids that don't
|
||||
// actually have grid atmospheres.
|
||||
if (pipeNet.Grid is not null)
|
||||
{
|
||||
var ev = new PipeNodeGroupRemovedEvent(grid, pipeNet.NetId);
|
||||
RaiseLocalEvent(ref ev);
|
||||
}
|
||||
|
||||
return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Remove(pipeNet);
|
||||
}
|
||||
|
||||
@@ -329,3 +337,12 @@ public partial class AtmosphereSystem
|
||||
[ByRefEvent] private record struct IsHotspotActiveMethodEvent
|
||||
(EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Raised broadcasted when a pipe node group within a grid has been removed.
|
||||
/// </summary>
|
||||
/// <param name="Grid">The grid with the removed node group.</param>
|
||||
/// <param name="NetId">The net id of the removed node group.</param>
|
||||
[ByRefEvent]
|
||||
public record struct PipeNodeGroupRemovedEvent(EntityUid Grid, int NetId);
|
||||
|
||||
@@ -596,8 +596,17 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
if (!reconsiderAdjacent)
|
||||
return;
|
||||
|
||||
// Before updating the adjacent tile flags that determine whether air is allowed to flow
|
||||
// or not, we explicitly update airtight data on these tiles right now.
|
||||
// This ensures that UpdateAdjacentTiles has updated data before updating flags.
|
||||
// This allows monstermos' floodfill check that determines if firelocks have dropped
|
||||
// to work correctly.
|
||||
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, tile);
|
||||
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, other);
|
||||
|
||||
UpdateAdjacentTiles(ent, tile);
|
||||
UpdateAdjacentTiles(ent, other);
|
||||
|
||||
InvalidateVisuals(ent, tile);
|
||||
InvalidateVisuals(ent, other);
|
||||
}
|
||||
|
||||
@@ -394,7 +394,7 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
flammable.Resisting = true;
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("flammable-component-resist-message"), uid, uid);
|
||||
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(2f), true);
|
||||
_stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(2f));
|
||||
|
||||
// TODO FLAMMABLE: Make this not use TimerComponent...
|
||||
uid.SpawnTimer(2000, () =>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Shared.Atmos.Piping;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.EntitySystems
|
||||
{
|
||||
public sealed class AtmosPipeColorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<AtmosPipeColorComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<AtmosPipeColorComponent, ComponentShutdown>(OnShutdown);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, AtmosPipeColorComponent component, ComponentStartup args)
|
||||
{
|
||||
if (!TryComp(uid, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
_appearance.SetData(uid, PipeColorVisuals.Color, component.Color, appearance);
|
||||
}
|
||||
|
||||
private void OnShutdown(EntityUid uid, AtmosPipeColorComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (!TryComp(uid, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
_appearance.SetData(uid, PipeColorVisuals.Color, Color.White, appearance);
|
||||
}
|
||||
|
||||
public void SetColor(EntityUid uid, AtmosPipeColorComponent component, Color color)
|
||||
{
|
||||
component.Color = color;
|
||||
|
||||
if (!TryComp(uid, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
_appearance.SetData(uid, PipeColorVisuals.Color, color, appearance);
|
||||
|
||||
var ev = new AtmosPipeColorChangedEvent(color);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
namespace Content.Server.Atmos
|
||||
{
|
||||
public abstract class PressureEvent : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// The environment pressure.
|
||||
/// </summary>
|
||||
public float Pressure { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The modifier for the apparent pressure.
|
||||
/// This number will be added to the environment pressure for calculation purposes.
|
||||
/// It can be negative to reduce the felt pressure, or positive to increase it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not set this directly. Add to it, or subtract from it to modify it.
|
||||
/// </remarks>
|
||||
public float Modifier { get; set; } = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// The multiplier for the apparent pressure.
|
||||
/// The environment pressure will be multiplied by this for calculation purposes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Do not set, add to or subtract from this directly. Multiply this by your multiplier only.
|
||||
/// </remarks>
|
||||
public float Multiplier { get; set; } = 1f;
|
||||
|
||||
protected PressureEvent(float pressure)
|
||||
{
|
||||
Pressure = pressure;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LowPressureEvent : PressureEvent
|
||||
{
|
||||
public LowPressureEvent(float pressure) : base(pressure) { }
|
||||
}
|
||||
|
||||
public sealed class HighPressureEvent : PressureEvent
|
||||
{
|
||||
public HighPressureEvent(float pressure) : base(pressure) { }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Hands.Systems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Part;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Body.Commands
|
||||
{
|
||||
@@ -15,9 +16,9 @@ namespace Content.Server.Body.Commands
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
private static readonly EntProtoId DefaultHandPrototype = "LeftHandHuman";
|
||||
private static int _handIdAccumulator;
|
||||
|
||||
public string Command => "addhand";
|
||||
public string Description => "Adds a hand to your entity.";
|
||||
@@ -114,9 +115,17 @@ namespace Content.Server.Body.Commands
|
||||
|
||||
if (!_entManager.TryGetComponent(entity, out BodyComponent? body) || body.RootContainer.ContainedEntity == null)
|
||||
{
|
||||
var text = $"You have no body{(_random.Prob(0.2f) ? " and you must scream." : ".")}";
|
||||
var location = _entManager.GetComponentOrNull<BodyPartComponent>(hand)?.Symmetry switch
|
||||
{
|
||||
BodyPartSymmetry.None => HandLocation.Middle,
|
||||
BodyPartSymmetry.Left => HandLocation.Left,
|
||||
BodyPartSymmetry.Right => HandLocation.Right,
|
||||
_ => HandLocation.Right
|
||||
};
|
||||
_entManager.DeleteEntity(hand);
|
||||
|
||||
shell.WriteLine(text);
|
||||
// You have no body and you must scream.
|
||||
_entManager.System<HandsSystem>().AddHand(entity, $"{hand}-cmd-{_handIdAccumulator++}", location);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,16 +10,6 @@ namespace Content.Server.Body.Components
|
||||
[RegisterComponent, Access(typeof(RespiratorSystem)), AutoGenerateComponentPause]
|
||||
public sealed partial class RespiratorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Gas container for this entity
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public GasMixture Air = new()
|
||||
{
|
||||
Volume = 6, // 6 liters, the average lung capacity for a human according to Google
|
||||
Temperature = Atmospherics.NormalBodyTemperature
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Volume of our breath in liters
|
||||
/// </summary>
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class InternalsSystem : SharedInternalsSystem
|
||||
return;
|
||||
|
||||
// Could the entity metabolise the air in the linked gas tank?
|
||||
if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air))
|
||||
if (!_respirator.CanMetabolizeInhaledAir(uid, tank.Value.Comp.Air))
|
||||
return;
|
||||
|
||||
ToggleInternals(uid, uid, force: false, component, ToggleMode.On);
|
||||
|
||||
@@ -49,8 +49,13 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
UpdatesAfter.Add(typeof(MetabolizerSystem));
|
||||
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
|
||||
|
||||
// BodyComp stuff
|
||||
SubscribeLocalEvent<BodyComponent, InhaledGasEvent>(OnGasInhaled);
|
||||
SubscribeLocalEvent<BodyComponent, ExhaledGasEvent>(OnGasExhaled);
|
||||
SubscribeLocalEvent<BodyComponent, CanMetabolizeGasEvent>(CanBodyMetabolizeGas);
|
||||
SubscribeLocalEvent<BodyComponent, SuffocationEvent>(OnSuffocation);
|
||||
SubscribeLocalEvent<BodyComponent, StopSuffocatingEvent>(OnStopSuffocating);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
|
||||
@@ -80,11 +85,11 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
switch (respirator.Status)
|
||||
{
|
||||
case RespiratorStatus.Inhaling:
|
||||
Inhale(uid);
|
||||
Inhale((uid, respirator));
|
||||
respirator.Status = RespiratorStatus.Exhaling;
|
||||
break;
|
||||
case RespiratorStatus.Exhaling:
|
||||
Exhale(uid);
|
||||
Exhale((uid, respirator));
|
||||
respirator.Status = RespiratorStatus.Inhaling;
|
||||
break;
|
||||
}
|
||||
@@ -111,10 +116,10 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public bool Inhale(Entity<RespiratorComponent?> entity)
|
||||
public void Inhale(Entity<RespiratorComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp, logMissing: false))
|
||||
return false;
|
||||
return;
|
||||
|
||||
// Inhale gas
|
||||
var ev = new InhaleLocationEvent
|
||||
@@ -126,23 +131,22 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
ev.Gas ??= _atmosSys.GetContainingMixture(entity.Owner, excite: true);
|
||||
|
||||
if (ev.Gas is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return;
|
||||
|
||||
var gas = ev.Gas.RemoveVolume(entity.Comp.BreathVolume);
|
||||
|
||||
var inhaleEv = new InhaledGasEvent(gas);
|
||||
RaiseLocalEvent(entity, ref inhaleEv);
|
||||
|
||||
return inhaleEv.Handled && inhaleEv.Succeeded;
|
||||
}
|
||||
|
||||
public void Exhale(Entity<RespiratorComponent?> entity)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp, logMissing: false))
|
||||
if (inhaleEv.Handled && inhaleEv.Succeeded)
|
||||
return;
|
||||
|
||||
// If nothing could inhale the gas give it back.
|
||||
_atmosSys.Merge(ev.Gas, gas);
|
||||
}
|
||||
|
||||
public void Exhale(Entity<RespiratorComponent> entity)
|
||||
{
|
||||
// exhale gas
|
||||
|
||||
var ev = new ExhaleLocationEvent();
|
||||
@@ -157,8 +161,16 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
ev.Gas ??= GasMixture.SpaceGas;
|
||||
}
|
||||
|
||||
var exhaleEv = new ExhaledGasEvent(ev.Gas);
|
||||
RaiseLocalEvent(entity, ref exhaleEv);
|
||||
Exhale(entity!, ev.Gas);
|
||||
}
|
||||
|
||||
public void Exhale(Entity<RespiratorComponent?> entity, GasMixture gas)
|
||||
{
|
||||
if (!Resolve(entity, ref entity.Comp, logMissing: false))
|
||||
return;
|
||||
|
||||
var ev = new ExhaledGasEvent(gas);
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -176,52 +188,76 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic
|
||||
/// gasses).
|
||||
/// Checks if it's safe for a given entity to breathe the air from the environment it is currently situated in.
|
||||
/// </summary>
|
||||
/// <param name="ent">The entity attempting to metabolize the gas.</param>
|
||||
/// <returns>Returns true only if the air is not toxic, and it wouldn't suffocate.</returns>
|
||||
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
if (!Inhale(ent))
|
||||
return false;
|
||||
// Get the gas at our location but don't actually remove it from the gas mixture.
|
||||
var ev = new InhaleLocationEvent
|
||||
{
|
||||
Respirator = ent.Comp,
|
||||
};
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
|
||||
// If we don't have a body we can't be poisoned by gas, yet...
|
||||
var success = TryMetabolizeGas((ent, ent.Comp));
|
||||
ev.Gas ??= _atmosSys.GetContainingMixture(ent.Owner, excite: true);
|
||||
|
||||
// Don't keep that gas in our lungs lest it poisons a poor nuclear operative.
|
||||
Exhale(ent);
|
||||
return success;
|
||||
// If there's no air to breathe or we can't metabolize it then internals should be on.
|
||||
return ev.Gas is not null && CanMetabolizeInhaledAir(ent, ev.Gas);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage
|
||||
/// (i.e., no toxic gasses).
|
||||
/// Checks if a given entity can safely metabolize a given gas mixture.
|
||||
/// </summary>
|
||||
public bool CanMetabolizeGas(Entity<RespiratorComponent?> ent, GasMixture gas)
|
||||
/// <param name="ent">The entity attempting to metabolize the gas.</param>
|
||||
/// <param name="gas">The gas mixture we are trying to metabolize.</param>
|
||||
/// <returns>Returns true only if the gas mixture is not toxic, and it wouldn't suffocate.</returns>
|
||||
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent, GasMixture gas)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
if (organs.Count == 0)
|
||||
var ev = new CanMetabolizeGasEvent(gas);
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
|
||||
if (!ev.Handled || ev.Toxic)
|
||||
return false;
|
||||
|
||||
gas = new GasMixture(gas);
|
||||
var lungRatio = 1.0f / organs.Count;
|
||||
gas.Multiply(MathF.Min(lungRatio * gas.Volume / ent.Comp.BreathVolume, lungRatio));
|
||||
var solution = _lungSystem.GasToReagent(gas);
|
||||
return ev.Saturation > ent.Comp.UpdateInterval.TotalSeconds;
|
||||
}
|
||||
|
||||
float saturation = 0;
|
||||
/// <summary>
|
||||
/// Tries to safely metabolize the current solutions in a body's lungs.
|
||||
/// </summary>
|
||||
private void CanBodyMetabolizeGas(Entity<BodyComponent> ent, ref CanMetabolizeGasEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
if (organs.Count == 0)
|
||||
return;
|
||||
|
||||
var solution = _lungSystem.GasToReagent(args.Gas);
|
||||
|
||||
var saturation = 0f;
|
||||
foreach (var organ in organs)
|
||||
{
|
||||
saturation += GetSaturation(solution, organ.Owner, out var toxic);
|
||||
if (toxic)
|
||||
return false;
|
||||
if (!toxic)
|
||||
continue;
|
||||
|
||||
args.Handled = true;
|
||||
args.Toxic = true;
|
||||
return;
|
||||
}
|
||||
|
||||
return saturation > ent.Comp.UpdateInterval.TotalSeconds;
|
||||
args.Handled = true;
|
||||
args.Saturation = saturation;
|
||||
}
|
||||
|
||||
public bool TryInhaleGasToBody(Entity<BodyComponent?> entity, GasMixture gas)
|
||||
@@ -265,30 +301,6 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
_atmosSys.Merge(gas, outGas);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to safely metabolize the current solutions in a body's lungs.
|
||||
/// </summary>
|
||||
private bool TryMetabolizeGas(Entity<RespiratorComponent, BodyComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp2))
|
||||
return false;
|
||||
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
if (organs.Count == 0)
|
||||
return false;
|
||||
|
||||
float saturation = 0;
|
||||
foreach (var organ in organs)
|
||||
{
|
||||
var solution = _lungSystem.GasToReagent(organ.Comp1.Air);
|
||||
saturation += GetSaturation(solution, organ.Owner, out var toxic);
|
||||
if (toxic)
|
||||
return false;
|
||||
}
|
||||
|
||||
return saturation > ent.Comp1.UpdateInterval.TotalSeconds;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the amount of saturation that would be generated if the lung were to metabolize the given solution.
|
||||
/// </summary>
|
||||
@@ -354,15 +366,11 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
|
||||
|
||||
if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold)
|
||||
{
|
||||
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
foreach (var entity in organs)
|
||||
{
|
||||
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
|
||||
}
|
||||
}
|
||||
if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold)
|
||||
return;
|
||||
|
||||
var ev = new SuffocationEvent();
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
}
|
||||
|
||||
private void StopSuffocation(Entity<RespiratorComponent> ent)
|
||||
@@ -372,6 +380,22 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
|
||||
|
||||
var ev = new StopSuffocatingEvent();
|
||||
RaiseLocalEvent(ent, ref ev);
|
||||
}
|
||||
|
||||
private void OnSuffocation(Entity<BodyComponent> ent, ref SuffocationEvent args)
|
||||
{
|
||||
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
foreach (var entity in organs)
|
||||
{
|
||||
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStopSuffocating(Entity<BodyComponent> ent, ref StopSuffocatingEvent args)
|
||||
{
|
||||
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
|
||||
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
|
||||
foreach (var entity in organs)
|
||||
@@ -397,6 +421,9 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
private void OnGasInhaled(Entity<BodyComponent> entity, ref InhaledGasEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
args.Succeeded = TryInhaleGasToBody((entity, entity.Comp), args.Gas);
|
||||
@@ -404,20 +431,65 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
private void OnGasExhaled(Entity<BodyComponent> entity, ref ExhaledGasEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
RemoveGasFromBody(entity, args.Gas);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an entity first tries to inhale that returns a GasMixture from a given location.
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas that gets returned, null if there is none.</param>
|
||||
/// <param name="Respirator">The Respirator component of the entity attempting to inhale</param>
|
||||
[ByRefEvent]
|
||||
public record struct InhaleLocationEvent(GasMixture? Gas, RespiratorComponent Respirator);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an entity first tries to exhale a gas, determines where the gas they're exhaling will be sent.
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas mixture that the exhaled gas will be merged into.</param>
|
||||
[ByRefEvent]
|
||||
public record struct ExhaleLocationEvent(GasMixture? Gas);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an entity successfully inhales a gas, attempts to find a place to put the gas.
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas we're inhaling.</param>
|
||||
/// <param name="Handled">Whether a system has responded appropriately.</param>
|
||||
/// <param name="Succeeded">Whether we successfully managed to inhale the gas</param>
|
||||
[ByRefEvent]
|
||||
public record struct InhaledGasEvent(GasMixture Gas, bool Handled = false, bool Succeeded = false);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised when an entity is exhaling
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas mixture we're exhaling into.</param>
|
||||
/// <param name="Handled">Whether we have successfully exhaled or not.</param>
|
||||
[ByRefEvent]
|
||||
public record struct ExhaledGasEvent(GasMixture Gas, bool Handled = false);
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an entity starts suffocating and when suffocation progresses.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct SuffocationEvent;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when an entity that was suffocating stops suffocating.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public record struct StopSuffocatingEvent;
|
||||
|
||||
/// <summary>
|
||||
/// An event raised to inhalation handlers that asks them nicely to simulate what it would be like to metabolize
|
||||
/// a given volume of gas, without actually metabolizing it.
|
||||
/// </summary>
|
||||
/// <param name="Gas">The gas mixture we are testing.</param>
|
||||
/// <param name="Toxic">Whether the gas returns as toxic to any respirator.</param>
|
||||
/// <param name="Saturation">The amount of saturation we got from the gas.</param>
|
||||
[ByRefEvent]
|
||||
public record struct CanMetabolizeGasEvent(GasMixture Gas, bool Toxic = false, float Saturation = 0f, bool Handled = false);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Frozen;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Emoting;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -10,6 +12,8 @@ namespace Content.Server.Chat.Systems;
|
||||
// emotes using emote prototype
|
||||
public partial class ChatSystem
|
||||
{
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
|
||||
private FrozenDictionary<string, EmotePrototype> _wordEmoteDict = FrozenDictionary<string, EmotePrototype>.Empty;
|
||||
|
||||
protected override void OnPrototypeReload(PrototypesReloadedEventArgs obj)
|
||||
@@ -51,7 +55,8 @@ public partial class ChatSystem
|
||||
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
|
||||
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
|
||||
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
|
||||
public void TryEmoteWithChat(
|
||||
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithChat(
|
||||
EntityUid source,
|
||||
string emoteId,
|
||||
ChatTransmitRange range = ChatTransmitRange.Normal,
|
||||
@@ -62,8 +67,8 @@ public partial class ChatSystem
|
||||
)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
|
||||
return;
|
||||
TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
|
||||
return false;
|
||||
return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -76,7 +81,8 @@ public partial class ChatSystem
|
||||
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
|
||||
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
|
||||
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
|
||||
public void TryEmoteWithChat(
|
||||
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithChat(
|
||||
EntityUid source,
|
||||
EmotePrototype emote,
|
||||
ChatTransmitRange range = ChatTransmitRange.Normal,
|
||||
@@ -84,43 +90,46 @@ public partial class ChatSystem
|
||||
string? nameOverride = null,
|
||||
bool ignoreActionBlocker = false,
|
||||
bool forceEmote = false
|
||||
)
|
||||
)
|
||||
{
|
||||
if (!forceEmote && !AllowedToUseEmote(source, emote))
|
||||
return;
|
||||
return false;
|
||||
|
||||
var didEmote = TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
|
||||
|
||||
// check if proto has valid message for chat
|
||||
if (emote.ChatMessages.Count != 0)
|
||||
if (didEmote && emote.ChatMessages.Count != 0)
|
||||
{
|
||||
// not all emotes are loc'd, but for the ones that are we pass in entity
|
||||
var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
|
||||
SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker);
|
||||
}
|
||||
|
||||
// do the rest of emote event logic here
|
||||
TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
|
||||
return didEmote;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
|
||||
/// </summary>
|
||||
public void TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
|
||||
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
|
||||
return;
|
||||
return false;
|
||||
|
||||
TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
|
||||
return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
|
||||
/// </summary>
|
||||
public void TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
|
||||
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
|
||||
public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
|
||||
{
|
||||
if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker)
|
||||
return;
|
||||
return false;
|
||||
|
||||
InvokeEmoteEvent(uid, proto);
|
||||
return TryInvokeEmoteEvent(uid, proto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -160,17 +169,17 @@ public partial class ChatSystem
|
||||
/// </summary>
|
||||
/// <param name="uid"></param>
|
||||
/// <param name="textInput"></param>
|
||||
private void TryEmoteChatInput(EntityUid uid, string textInput)
|
||||
/// <returns>True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be.</returns>
|
||||
private bool TryEmoteChatInput(EntityUid uid, string textInput)
|
||||
{
|
||||
var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
|
||||
if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
|
||||
return;
|
||||
return true;
|
||||
|
||||
if (!AllowedToUseEmote(uid, emote))
|
||||
return;
|
||||
return true;
|
||||
|
||||
InvokeEmoteEvent(uid, emote);
|
||||
return;
|
||||
return TryInvokeEmoteEvent(uid, emote);
|
||||
|
||||
static string TrimPunctuation(string textInput)
|
||||
{
|
||||
@@ -220,11 +229,50 @@ public partial class ChatSystem
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
|
||||
/// <summary>
|
||||
/// Creates and raises <see cref="BeforeEmoteEvent"/> and then <see cref="EmoteEvent"/> to let other systems do things like play audio.
|
||||
/// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player
|
||||
/// explaining why the emote didn't happen.
|
||||
/// </summary>
|
||||
/// <param name="uid">The entity which is emoting</param>
|
||||
/// <param name="proto">The emote which is being performed</param>
|
||||
/// <returns>True if the emote was performed, false otherwise.</returns>
|
||||
private bool TryInvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
|
||||
{
|
||||
var beforeEv = new BeforeEmoteEvent(uid, proto);
|
||||
RaiseLocalEvent(uid, ref beforeEv);
|
||||
|
||||
if (beforeEv.Cancelled)
|
||||
{
|
||||
if (beforeEv.Blocker != null)
|
||||
{
|
||||
_popupSystem.PopupEntity(
|
||||
Loc.GetString(
|
||||
"chat-system-emote-cancelled-blocked",
|
||||
("emote", Loc.GetString(proto.Name).ToLower()),
|
||||
("blocker", beforeEv.Blocker.Value)
|
||||
),
|
||||
uid,
|
||||
uid
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
_popupSystem.PopupEntity(
|
||||
Loc.GetString("chat-system-emote-cancelled-generic",
|
||||
("emote", Loc.GetString(proto.Name).ToLower())),
|
||||
uid,
|
||||
uid
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
var ev = new EmoteEvent(proto);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,9 +281,8 @@ public partial class ChatSystem
|
||||
/// Use it to play sound, change sprite or something else.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public struct EmoteEvent
|
||||
public sealed class EmoteEvent : HandledEntityEventArgs
|
||||
{
|
||||
public bool Handled;
|
||||
public readonly EmotePrototype Emote;
|
||||
|
||||
public EmoteEvent(EmotePrototype emote)
|
||||
|
||||
@@ -5,9 +5,8 @@ using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Players.RateLimiting;
|
||||
using Content.Server.Speech.Prototypes;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Speech.Prototypes;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.ActionBlocker;
|
||||
@@ -536,7 +535,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
|
||||
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.
|
||||
|
||||
if (data.Range <= WhisperClearRange)
|
||||
if (data.Range <= WhisperClearRange || data.Observer)
|
||||
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel);
|
||||
//If listener is too far, they only hear fragments of the message
|
||||
else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange))
|
||||
@@ -593,8 +592,10 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
("entity", ent),
|
||||
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
|
||||
|
||||
if (checkEmote)
|
||||
TryEmoteChatInput(source, action);
|
||||
if (checkEmote &&
|
||||
!TryEmoteChatInput(source, action))
|
||||
return;
|
||||
|
||||
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
|
||||
if (!hideLog)
|
||||
if (name != Name(source))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Server.Animals.Systems;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
@@ -19,19 +19,19 @@ public sealed partial class TransformableContainerComponent : Component
|
||||
/// It will revert to this when emptied.
|
||||
/// /// It defaults to the description of the parent entity unless overwritten.
|
||||
/// </summary>
|
||||
[DataField("initialDescription")]
|
||||
[DataField]
|
||||
public string? InitialDescription;
|
||||
|
||||
/// <summary>
|
||||
/// This stores whatever primary reagent is currently in the container.
|
||||
/// It is used to help determine if a transformation is needed on solution update.
|
||||
/// </summary>
|
||||
[DataField("currentReagent")]
|
||||
public ReagentPrototype? CurrentReagent;
|
||||
[DataField]
|
||||
public ProtoId<ReagentPrototype>? CurrentReagent;
|
||||
|
||||
/// <summary>
|
||||
/// This returns whether this container in a transformed or initial state.
|
||||
/// </summary>
|
||||
///
|
||||
[DataField("transformed")]
|
||||
[DataField]
|
||||
public bool Transformed;
|
||||
}
|
||||
|
||||
@@ -45,8 +45,8 @@ public sealed class TransformableContainerSystem : EntitySystem
|
||||
//the biggest reagent in the solution decides the appearance
|
||||
var reagentId = solution.GetPrimaryReagentId();
|
||||
|
||||
//If biggest reagent didn't changed - don't change anything at all
|
||||
if (entity.Comp.CurrentReagent != null && entity.Comp.CurrentReagent.ID == reagentId?.Prototype)
|
||||
//If biggest reagent didn't change - don't change anything at all
|
||||
if (entity.Comp.CurrentReagent != null && entity.Comp.CurrentReagent == reagentId?.Prototype)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public sealed class TransformableContainerSystem : EntitySystem
|
||||
|
||||
private void OnRefreshNameModifiers(Entity<TransformableContainerComponent> entity, ref RefreshNameModifiersEvent args)
|
||||
{
|
||||
if (entity.Comp.CurrentReagent is { } currentReagent)
|
||||
if (_prototypeManager.TryIndex(entity.Comp.CurrentReagent, out var currentReagent))
|
||||
{
|
||||
args.AddModifier("transformable-container-component-glass", priority: -1, ("reagent", currentReagent.LocalizedName));
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public sealed class CluwneSystem : EntitySystem
|
||||
else if (_robustRandom.Prob(component.KnockChance))
|
||||
{
|
||||
_audio.PlayPvs(component.KnockSound, uid);
|
||||
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(component.ParalyzeTime), true);
|
||||
_stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(component.ParalyzeTime));
|
||||
_chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, ChatTransmitRange.Normal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ namespace Content.Server.Communications
|
||||
{
|
||||
// All events that refresh the BUI
|
||||
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
|
||||
SubscribeLocalEvent<CommunicationsConsoleComponent, ComponentInit>((uid, comp, _) => UpdateCommsConsoleInterface(uid, comp));
|
||||
SubscribeLocalEvent<RoundEndSystemChangedEvent>(_ => OnGenericBroadcastEvent());
|
||||
SubscribeLocalEvent<AlertLevelDelayFinishedEvent>(_ => OnGenericBroadcastEvent());
|
||||
|
||||
@@ -85,6 +84,7 @@ namespace Content.Server.Communications
|
||||
public void OnCommunicationsConsoleMapInit(EntityUid uid, CommunicationsConsoleComponent comp, MapInitEvent args)
|
||||
{
|
||||
comp.AnnouncementCooldownRemaining = comp.InitialDelay;
|
||||
UpdateCommsConsoleInterface(uid, comp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Damage.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This component shows entity damage severity when it is examined by player.
|
||||
/// This component shows entity damage severity when it is examined by player.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ExaminableDamageComponent : Component
|
||||
{
|
||||
[DataField("messages", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<ExaminableDamagePrototype>))]
|
||||
public string? MessagesProtoId;
|
||||
|
||||
public ExaminableDamagePrototype? MessagesProto;
|
||||
/// <summary>
|
||||
/// ID of the <see cref="LocalizedDatasetPrototype"/> containing messages to display a different damage levels.
|
||||
/// The first message will be used at 0 damage with the others equally distributed across the range from undamaged to fully damaged.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<LocalizedDatasetPrototype>? Messages;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Damage.Components;
|
||||
using Content.Server.Damage.Components;
|
||||
using Content.Server.Destructible;
|
||||
using Content.Server.Destructible.Thresholds.Triggers;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Rounding;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -12,59 +9,42 @@ namespace Content.Server.Damage.Systems;
|
||||
|
||||
public sealed class ExaminableDamageSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DestructibleSystem _destructible = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<ExaminableDamageComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<ExaminableDamageComponent, ExaminedEvent>(OnExamine);
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, ExaminableDamageComponent component, ComponentInit args)
|
||||
private void OnExamine(Entity<ExaminableDamageComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
if (component.MessagesProtoId == null)
|
||||
if (!_prototype.TryIndex(ent.Comp.Messages, out var proto) || proto.Values.Count == 0)
|
||||
return;
|
||||
component.MessagesProto = _prototype.Index<ExaminableDamagePrototype>(component.MessagesProtoId);
|
||||
|
||||
var percent = GetDamagePercent(ent);
|
||||
var level = ContentHelpers.RoundToNearestLevels(percent, 1, proto.Values.Count - 1);
|
||||
var msg = Loc.GetString(proto.Values[level]);
|
||||
args.PushMarkup(msg, -99);
|
||||
}
|
||||
|
||||
private void OnExamine(EntityUid uid, ExaminableDamageComponent component, ExaminedEvent args)
|
||||
/// <summary>
|
||||
/// Returns a value between 0 and 1 representing how damaged the entity is,
|
||||
/// where 0 is undamaged and 1 is fully damaged.
|
||||
/// </summary>
|
||||
/// <returns>How damaged the entity is from 0 to 1</returns>
|
||||
private float GetDamagePercent(Entity<ExaminableDamageComponent> ent)
|
||||
{
|
||||
if (component.MessagesProto == null)
|
||||
return;
|
||||
|
||||
var messages = component.MessagesProto.Messages;
|
||||
if (messages.Length == 0)
|
||||
return;
|
||||
|
||||
var level = GetDamageLevel(uid, component);
|
||||
var msg = Loc.GetString(messages[level]);
|
||||
args.PushMarkup(msg,-99);
|
||||
}
|
||||
|
||||
private int GetDamageLevel(EntityUid uid, ExaminableDamageComponent? component = null,
|
||||
DamageableComponent? damageable = null, DestructibleComponent? destructible = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component, ref damageable, ref destructible))
|
||||
return 0;
|
||||
|
||||
if (component.MessagesProto == null)
|
||||
return 0;
|
||||
|
||||
var maxLevels = component.MessagesProto.Messages.Length - 1;
|
||||
if (maxLevels <= 0)
|
||||
return 0;
|
||||
|
||||
var trigger = (DamageTrigger?) destructible.Thresholds
|
||||
.LastOrDefault(threshold => threshold.Trigger is DamageTrigger)?.Trigger;
|
||||
if (trigger == null)
|
||||
if (!TryComp<DamageableComponent>(ent, out var damageable))
|
||||
return 0;
|
||||
|
||||
var damage = damageable.TotalDamage;
|
||||
var damageThreshold = trigger.Damage;
|
||||
var fraction = damageThreshold == 0 ? 0f : (float) damage / damageThreshold;
|
||||
var damageThreshold = _destructible.DestroyedAt(ent);
|
||||
|
||||
var level = ContentHelpers.RoundToNearestLevels(fraction, 1, maxLevels);
|
||||
return level;
|
||||
if (damageThreshold == 0)
|
||||
return 0;
|
||||
|
||||
return (damage / damageThreshold).Float();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Devour;
|
||||
using Content.Shared.Devour.Components;
|
||||
using Content.Shared.Whitelist;
|
||||
|
||||
namespace Content.Server.Devour;
|
||||
|
||||
public sealed class DevourSystem : SharedDevourSystem
|
||||
{
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _entityWhitelistSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DevourerComponent, DevourDoAfterEvent>(OnDoAfter);
|
||||
SubscribeLocalEvent<DevourerComponent, BeingGibbedEvent>(OnGibContents);
|
||||
}
|
||||
|
||||
private void OnDoAfter(EntityUid uid, DevourerComponent component, DevourDoAfterEvent args)
|
||||
{
|
||||
if (args.Handled || args.Cancelled)
|
||||
return;
|
||||
|
||||
var ichorInjection = new Solution(component.Chemical, component.HealRate);
|
||||
|
||||
// Grant ichor if the devoured thing meets the dragon's food preference
|
||||
if (args.Args.Target != null && _entityWhitelistSystem.IsWhitelistPassOrNull(component.FoodPreferenceWhitelist, (EntityUid)args.Args.Target))
|
||||
{
|
||||
_bloodstreamSystem.TryAddToChemicals(uid, ichorInjection);
|
||||
}
|
||||
|
||||
// If the devoured thing meets the stomach whitelist criteria, add it to the stomach
|
||||
if (args.Args.Target != null && _entityWhitelistSystem.IsWhitelistPass(component.StomachStorageWhitelist, (EntityUid)args.Args.Target))
|
||||
{
|
||||
ContainerSystem.Insert(args.Args.Target.Value, component.Stomach);
|
||||
}
|
||||
//TODO: Figure out a better way of removing structures via devour that still entails standing still and waiting for a DoAfter. Somehow.
|
||||
//If it's not alive, it must be a structure.
|
||||
// Delete if the thing isn't in the stomach storage whitelist (or the stomach whitelist is null/empty)
|
||||
else if (args.Args.Target != null)
|
||||
{
|
||||
QueueDel(args.Args.Target.Value);
|
||||
}
|
||||
|
||||
_audioSystem.PlayPvs(component.SoundDevour, uid);
|
||||
}
|
||||
|
||||
private void OnGibContents(EntityUid uid, DevourerComponent component, ref BeingGibbedEvent args)
|
||||
{
|
||||
if (component.StomachStorageWhitelist == null)
|
||||
return;
|
||||
|
||||
// For some reason we have two different systems that should handle gibbing,
|
||||
// and for some another reason GibbingSystem, which should empty all containers, doesn't get involved in this process
|
||||
ContainerSystem.EmptyContainer(component.Stomach);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Server.StatusEffectNew;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Drowsiness;
|
||||
using Content.Shared.StatusEffectNew;
|
||||
using Content.Shared.StatusEffectNew.Components;
|
||||
|
||||
@@ -397,7 +397,12 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
|
||||
var shouldStun = siemensCoefficient > 0.5f;
|
||||
|
||||
if (shouldStun)
|
||||
_stun.TryParalyze(uid, time * ParalyzeTimeMultiplier, refresh, statusEffects);
|
||||
{
|
||||
_ = refresh
|
||||
? _stun.TryUpdateParalyzeDuration(uid, time * ParalyzeTimeMultiplier)
|
||||
: _stun.TryAddParalyzeDuration(uid, time * ParalyzeTimeMultiplier);
|
||||
}
|
||||
|
||||
|
||||
// TODO: Sparks here.
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
using System.Threading;
|
||||
using Content.Shared.Ensnaring.Components;
|
||||
|
||||
|
||||
|
||||
@@ -58,6 +58,11 @@ public sealed class ProjectileGrenadeSystem : EntitySystem
|
||||
var grenadeCoord = _transformSystem.GetMapCoordinates(uid);
|
||||
var shootCount = 0;
|
||||
var totalCount = component.Container.ContainedEntities.Count + component.UnspawnedCount;
|
||||
|
||||
// Just in case
|
||||
if (totalCount == 0)
|
||||
return;
|
||||
|
||||
var segmentAngle = 360 / totalCount;
|
||||
|
||||
while (TrySpawnContents(grenadeCoord, component, out var contentUid))
|
||||
|
||||
@@ -338,6 +338,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
// Ensure we actually have the component
|
||||
EnsureComp<TileFrictionModifierComponent>(entity);
|
||||
|
||||
EnsureComp<SlipperyComponent>(entity, out var slipComp);
|
||||
|
||||
// This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
|
||||
// the sprite threshold for a puddle larger than 5 pixels.
|
||||
var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
|
||||
@@ -356,17 +358,21 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
var launchMult = FixedPoint2.Zero;
|
||||
// A cumulative weighted amount of stun times from slippery reagents
|
||||
var stunTimer = TimeSpan.Zero;
|
||||
// A cumulative weighted amount of knockdown times from slippery reagents
|
||||
var knockdownTimer = TimeSpan.Zero;
|
||||
|
||||
// Check if the puddle is big enough to slip in to avoid doing unnecessary logic
|
||||
if (solution.Volume <= smallPuddleThreshold)
|
||||
{
|
||||
_stepTrigger.SetActive(entity, false, comp);
|
||||
_tile.SetModifier(entity, 1f);
|
||||
slipComp.SlipData.SlipFriction = 1f;
|
||||
slipComp.AffectsSliding = false;
|
||||
Dirty(entity, slipComp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<SlipperyComponent>(entity, out var slipComp))
|
||||
return;
|
||||
slipComp.AffectsSliding = true;
|
||||
|
||||
foreach (var (reagent, quantity) in solution.Contents)
|
||||
{
|
||||
@@ -386,7 +392,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
// Aggregate launch speed based on quantity
|
||||
launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
|
||||
// Aggregate stun times based on quantity
|
||||
stunTimer += reagentProto.SlipData.ParalyzeTime * (float)quantity;
|
||||
stunTimer += reagentProto.SlipData.StunTime * (float)quantity;
|
||||
knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity;
|
||||
|
||||
if (reagentProto.SlipData.SuperSlippery)
|
||||
superSlipperyUnits += quantity;
|
||||
@@ -404,8 +411,9 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
|
||||
// A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount.
|
||||
if (slipperyUnits > 0)
|
||||
{
|
||||
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits);
|
||||
slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits;
|
||||
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
|
||||
slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits);
|
||||
slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits);
|
||||
}
|
||||
|
||||
// Only make it super slippery if there is enough super slippery units for its own puddle
|
||||
|
||||
@@ -228,7 +228,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
continue;
|
||||
|
||||
_npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction);
|
||||
_stun.TryParalyze(uid, stunTime, true);
|
||||
_stun.TryUpdateParalyzeDuration(uid, stunTime);
|
||||
RemCompDeferred<RevolutionaryComponent>(uid);
|
||||
_popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
|
||||
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");
|
||||
|
||||
@@ -5,7 +5,6 @@ using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Ghost.Roles.Events;
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using Content.Server.Ghost.Roles.UI;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
@@ -698,7 +697,7 @@ public sealed class GhostRoleSystem : EntitySystem
|
||||
RaiseLocalEvent(mob, spawnedEvent);
|
||||
|
||||
if (ghostRole.MakeSentient)
|
||||
MakeSentientCommand.MakeSentient(mob, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
|
||||
_mindSystem.MakeSentient(mob, ghostRole.AllowMovement, ghostRole.AllowSpeech);
|
||||
|
||||
EnsureComp<MindContainerComponent>(mob);
|
||||
|
||||
@@ -745,7 +744,7 @@ public sealed class GhostRoleSystem : EntitySystem
|
||||
}
|
||||
|
||||
if (ghostRole.MakeSentient)
|
||||
MakeSentientCommand.MakeSentient(uid, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
|
||||
_mindSystem.MakeSentient(uid, ghostRole.AllowMovement, ghostRole.AllowSpeech);
|
||||
|
||||
GhostRoleInternalCreateMindAndTransfer(args.Player, uid, uid, ghostRole);
|
||||
UnregisterGhostRole((uid, ghostRole));
|
||||
|
||||
@@ -10,55 +10,46 @@ using Robust.Shared.Network;
|
||||
namespace Content.Server.Info;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class ShowRulesCommand : IConsoleCommand
|
||||
public sealed class ShowRulesCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configuration = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
|
||||
public string Command => "showrules";
|
||||
public string Description => "Opens the rules popup for the specified player.";
|
||||
public string Help => "showrules <username> [seconds]";
|
||||
public async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
public override string Command => "showrules";
|
||||
|
||||
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
string target;
|
||||
float seconds;
|
||||
|
||||
switch (args.Length)
|
||||
if (args.Length is < 1 or > 2)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
target = args[0];
|
||||
seconds = _configuration.GetCVar(CCVars.RulesWaitTime);
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
if (!float.TryParse(args[1], out seconds))
|
||||
{
|
||||
shell.WriteError($"{args[1]} is not a valid amount of seconds.\n{Help}");
|
||||
return;
|
||||
}
|
||||
|
||||
target = args[0];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
shell.WriteLine(Help);
|
||||
return;
|
||||
}
|
||||
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
var seconds = _configuration.GetCVar(CCVars.RulesWaitTime);
|
||||
|
||||
if (!_player.TryGetSessionByUsername(target, out var player))
|
||||
if (args.Length == 2 && !float.TryParse(args[1], out seconds))
|
||||
{
|
||||
shell.WriteError("Unable to find a player with that name.");
|
||||
return;
|
||||
shell.WriteError(Loc.GetString("cmd-showrules-invalid-seconds", ("seconds", args[1])));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_player.TryGetSessionByUsername(args[0], out var player))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-target-player-does-not-exist"));
|
||||
return;
|
||||
}
|
||||
|
||||
var coreRules = _configuration.GetCVar(CCVars.RulesFile);
|
||||
var message = new SendRulesInformationMessage { PopupTime = seconds, CoreRules = coreRules, ShouldShowRules = true};
|
||||
var message = new SendRulesInformationMessage
|
||||
{ PopupTime = seconds, CoreRules = coreRules, ShouldShowRules = true };
|
||||
_net.ServerSendMessage(message, player.Channel);
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
return args.Length == 1
|
||||
? CompletionResult.FromOptions(CompletionHelper.SessionNames(players: _player))
|
||||
: CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,7 +461,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
|
||||
{
|
||||
if (instrument.InstrumentPlayer is {Valid: true} mob)
|
||||
{
|
||||
_stuns.TryParalyze(mob, TimeSpan.FromSeconds(1), true);
|
||||
_stuns.TryUpdateParalyzeDuration(mob, TimeSpan.FromSeconds(1));
|
||||
|
||||
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-max-message"),
|
||||
uid, mob, PopupType.LargeCaution);
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Materials.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.Audio;
|
||||
|
||||
namespace Content.Server.Materials;
|
||||
@@ -13,6 +14,7 @@ public sealed class ProduceMaterialExtractorSystem : EntitySystem
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
@@ -39,7 +41,16 @@ public sealed class ProduceMaterialExtractorSystem : EntitySystem
|
||||
var matAmount = solution.Value.Comp.Solution.Contents
|
||||
.Where(r => ent.Comp.ExtractionReagents.Contains(r.Reagent.Prototype))
|
||||
.Sum(r => r.Quantity.Float());
|
||||
_materialStorage.TryChangeMaterialAmount(ent, ent.Comp.ExtractedMaterial, (int) matAmount);
|
||||
|
||||
var changed = (int)matAmount;
|
||||
|
||||
if (changed == 0)
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("material-extractor-comp-wrongreagent", ("used", args.Used)), args.User, args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
_materialStorage.TryChangeMaterialAmount(ent, ent.Comp.ExtractedMaterial, changed);
|
||||
|
||||
_audio.PlayPvs(ent.Comp.ExtractSound, ent);
|
||||
QueueDel(args.Used);
|
||||
|
||||
@@ -2,16 +2,15 @@ using Content.Server.Body.Systems;
|
||||
using Content.Server.Fluids.EntitySystems;
|
||||
using Content.Server.Forensics;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Stunnable;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -27,7 +26,7 @@ namespace Content.Server.Medical
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly PuddleSystem _puddle = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly StunSystem _stun = default!;
|
||||
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
|
||||
[Dependency] private readonly ThirstSystem _thirst = default!;
|
||||
[Dependency] private readonly ForensicsSystem _forensics = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
||||
@@ -57,8 +56,7 @@ namespace Content.Server.Medical
|
||||
// It fully empties the stomach, this amount from the chem stream is relatively small
|
||||
var solutionSize = (MathF.Abs(thirstAdded) + MathF.Abs(hungerAdded)) / 6;
|
||||
// Apply a bit of slowdown
|
||||
if (TryComp<StatusEffectsComponent>(uid, out var status))
|
||||
_stun.TrySlowdown(uid, TimeSpan.FromSeconds(solutionSize), true, 0.5f, 0.5f, status);
|
||||
_movementMod.TryUpdateMovementSpeedModDuration(uid, MovementModStatusSystem.VomitingSlowdown, TimeSpan.FromSeconds(solutionSize), 0.5f);
|
||||
|
||||
// TODO: Need decals
|
||||
var solution = new Solution();
|
||||
|
||||
@@ -1,63 +1,30 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Emoting;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Speech;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Mind.Commands
|
||||
namespace Content.Server.Mind.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class MakeSentientCommand : LocalizedEntityCommands
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class MakeSentientCommand : IConsoleCommand
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
|
||||
public override string Command => "makesentient";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public string Command => "makesentient";
|
||||
public string Description => "Makes an entity sentient (able to be controlled by a player)";
|
||||
public string Help => "makesentient <entity id>";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
if (args.Length != 1)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteLine("Wrong number of arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetEntity.TryParse(args[0], out var entNet) || !_entManager.TryGetEntity(entNet, out var entId))
|
||||
{
|
||||
shell.WriteLine("Invalid argument.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entManager.EntityExists(entId))
|
||||
{
|
||||
shell.WriteLine("Invalid entity specified!");
|
||||
return;
|
||||
}
|
||||
|
||||
MakeSentient(entId.Value, _entManager, true, true);
|
||||
shell.WriteLine(Loc.GetString("shell-need-exactly-one-argument"));
|
||||
return;
|
||||
}
|
||||
|
||||
public static void MakeSentient(EntityUid uid, IEntityManager entityManager, bool allowMovement = true, bool allowSpeech = true)
|
||||
if (!NetEntity.TryParse(args[0], out var entNet) || !EntityManager.TryGetEntity(entNet, out var entId) || !EntityManager.EntityExists(entId))
|
||||
{
|
||||
entityManager.EnsureComponent<MindContainerComponent>(uid);
|
||||
if (allowMovement)
|
||||
{
|
||||
entityManager.EnsureComponent<InputMoverComponent>(uid);
|
||||
entityManager.EnsureComponent<MobMoverComponent>(uid);
|
||||
entityManager.EnsureComponent<MovementSpeedModifierComponent>(uid);
|
||||
}
|
||||
|
||||
if (allowSpeech)
|
||||
{
|
||||
entityManager.EnsureComponent<SpeechComponent>(uid);
|
||||
entityManager.EnsureComponent<EmotingComponent>(uid);
|
||||
}
|
||||
|
||||
entityManager.EnsureComponent<ExaminerComponent>(uid);
|
||||
shell.WriteLine(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
_mindSystem.MakeSentient(entId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Mind;
|
||||
@@ -349,7 +348,7 @@ public sealed class MindSystem : SharedMindSystem
|
||||
return;
|
||||
}
|
||||
|
||||
MakeSentientCommand.MakeSentient(target, EntityManager);
|
||||
MakeSentient(target);
|
||||
TransferTo(mindId, target, ghostCheckOverride: true, mind: mind);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ public sealed partial class NPCCombatSystem
|
||||
return;
|
||||
}
|
||||
|
||||
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates);
|
||||
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates, comp.Target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,8 +82,8 @@ public sealed class NameIdentifierSystem : EntitySystem
|
||||
randomVal = set[^1];
|
||||
set.RemoveAt(set.Count - 1);
|
||||
|
||||
return proto.Prefix is not null
|
||||
? $"{proto.Prefix}-{randomVal}"
|
||||
return proto.Format is not null
|
||||
? Loc.GetString(proto.Format, ("number", randomVal))
|
||||
: $"{randomVal}";
|
||||
}
|
||||
|
||||
@@ -104,8 +104,8 @@ public sealed class NameIdentifierSystem : EntitySystem
|
||||
ids.Remove(ent.Comp.Identifier))
|
||||
{
|
||||
id = ent.Comp.Identifier;
|
||||
uniqueName = group.Prefix is not null
|
||||
? $"{group.Prefix}-{id}"
|
||||
uniqueName = group.Format is not null
|
||||
? Loc.GetString(group.Format, ("number", id))
|
||||
: $"{id}";
|
||||
}
|
||||
else
|
||||
|
||||
@@ -63,7 +63,7 @@ public sealed class StunProviderSystem : SharedStunProviderSystem
|
||||
_audio.PlayPvs(comp.Sound, target);
|
||||
|
||||
_damageable.TryChangeDamage(target, comp.StunDamage, false, true, null, origin: uid);
|
||||
_stun.TryParalyze(target, comp.StunTime, refresh: false);
|
||||
_stun.TryAddParalyzeDuration(target, comp.StunTime);
|
||||
|
||||
// short cooldown to prevent instant stunlocking
|
||||
_useDelay.SetLength((uid, useDelay), comp.Cooldown, id: comp.DelayId);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Storage.EntitySystems;
|
||||
using Content.Server.Stunnable;
|
||||
@@ -7,7 +6,6 @@ using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.PneumaticCannon;
|
||||
using Content.Shared.StatusEffect;
|
||||
using Content.Shared.Tools.Systems;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
@@ -80,10 +78,9 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
|
||||
if (gas == null && component.GasUsage > 0f)
|
||||
return;
|
||||
|
||||
if (TryComp<StatusEffectsComponent>(args.User, out var status)
|
||||
&& component.Power == PneumaticCannonPower.High)
|
||||
if (component.Power == PneumaticCannonPower.High
|
||||
&& _stun.TryUpdateParalyzeDuration(args.User, TimeSpan.FromSeconds(component.HighPowerStunTime)))
|
||||
{
|
||||
_stun.TryParalyze(args.User, TimeSpan.FromSeconds(component.HighPowerStunTime), true, status);
|
||||
Popup.PopupEntity(Loc.GetString("pneumatic-cannon-component-power-stun",
|
||||
("cannon", uid)), cannon, args.User);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
using Content.Server.Actions;
|
||||
using Content.Server.Humanoid;
|
||||
using Content.Server.Inventory;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Server.Polymorph.Components;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.Components;
|
||||
using Content.Shared.Buckle;
|
||||
using Content.Shared.Coordinates;
|
||||
using Content.Shared.Damage;
|
||||
@@ -210,7 +207,7 @@ public sealed partial class PolymorphSystem : EntitySystem
|
||||
("child", Identity.Entity(child, EntityManager))),
|
||||
child);
|
||||
|
||||
MakeSentientCommand.MakeSentient(child, EntityManager);
|
||||
_mindSystem.MakeSentient(child);
|
||||
|
||||
var polymorphedComp = Factory.GetComponent<PolymorphedEntityComponent>();
|
||||
polymorphedComp.Parent = uid;
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.NodeContainer;
|
||||
using Content.Server.NodeContainer.NodeGroups;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Content.Shared.NodeContainer.NodeGroups;
|
||||
using Content.Shared.Power;
|
||||
|
||||
namespace Content.Server.Power.Components
|
||||
{
|
||||
@@ -94,11 +95,4 @@ namespace Content.Server.Power.Components
|
||||
TryFindAndSetNet();
|
||||
}
|
||||
}
|
||||
|
||||
public enum Voltage
|
||||
{
|
||||
High = NodeGroupID.HVPower,
|
||||
Medium = NodeGroupID.MVPower,
|
||||
Apc = NodeGroupID.Apc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +85,8 @@ public sealed class PowerChargeSystem : EntitySystem
|
||||
if (!Resolve(uid, ref powerReceiver))
|
||||
return;
|
||||
|
||||
if (user is { } )
|
||||
_adminLogger.Add(LogType.Action, on ? LogImpact.Medium : LogImpact.High, $"{ToPrettyString(user):player} set ${ToPrettyString(uid):target} to {(on ? "on" : "off")}");
|
||||
if (user is { })
|
||||
_adminLogger.Add(LogType.Action, on ? LogImpact.Medium : LogImpact.High, $"{ToPrettyString(user):player} set {ToPrettyString(uid):target} to {(on ? "on" : "off")}");
|
||||
|
||||
component.SwitchedOn = on;
|
||||
UpdatePowerState(component, powerReceiver);
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Popups;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.Nodes;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Power.Generator;
|
||||
using Content.Shared.Timing;
|
||||
using Content.Shared.Verbs;
|
||||
|
||||
@@ -177,7 +177,7 @@ public sealed partial class RevenantSystem : EntitySystem
|
||||
ChangeEssenceAmount(uid, -abilityCost, component, false);
|
||||
|
||||
_statusEffects.TryAddStatusEffect<CorporealComponent>(uid, "Corporeal", TimeSpan.FromSeconds(debuffs.Y), false);
|
||||
_stun.TryStun(uid, TimeSpan.FromSeconds(debuffs.X), false);
|
||||
_stun.TryAddStunDuration(uid, TimeSpan.FromSeconds(debuffs.X));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.Atmos.Piping.EntitySystems;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.NodeContainer;
|
||||
using Content.Shared.NodeContainer.NodeGroups;
|
||||
@@ -78,7 +78,7 @@ namespace Content.Server.Sandbox.Commands
|
||||
if (!EntityManager.TryGetComponent(x.Owner, out AtmosPipeColorComponent? atmosPipeColorComponent))
|
||||
continue;
|
||||
|
||||
_pipeColorSystem.SetColor(x.Owner, atmosPipeColorComponent, color);
|
||||
_pipeColorSystem.SetColor((x.Owner, atmosPipeColorComponent), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -616,10 +616,7 @@ public sealed partial class ShuttleSystem
|
||||
{
|
||||
foreach (var child in toKnock)
|
||||
{
|
||||
if (!_statusQuery.TryGetComponent(child, out var status))
|
||||
continue;
|
||||
|
||||
_stuns.TryParalyze(child, _hyperspaceKnockdownTime, true, status);
|
||||
_stuns.TryUpdateParalyzeDuration(child, _hyperspaceKnockdownTime);
|
||||
|
||||
// If the guy we knocked down is on a spaced tile, throw them too
|
||||
if (grid != null)
|
||||
|
||||
@@ -249,7 +249,7 @@ public sealed partial class ShuttleSystem
|
||||
|
||||
if (direction.LengthSquared() > minsq)
|
||||
{
|
||||
_stuns.TryKnockdown(uid, knockdownTime, true);
|
||||
_stuns.TryUpdateKnockdownDuration(uid, knockdownTime);
|
||||
_throwing.TryThrow(uid, direction, physics, Transform(uid), _projQuery, direction.Length(), playSound: false);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,44 +1,23 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Server.Chat;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Shared.Speech;
|
||||
|
||||
namespace Content.Server.Speech
|
||||
namespace Content.Server.Speech;
|
||||
|
||||
public sealed class AccentSystem : EntitySystem
|
||||
{
|
||||
public sealed class AccentSystem : EntitySystem
|
||||
public static readonly Regex SentenceRegex = new(@"(?<=[\.!\?‽])(?![\.!\?‽])", RegexOptions.Compiled);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
public static readonly Regex SentenceRegex = new(@"(?<=[\.!\?‽])(?![\.!\?‽])", RegexOptions.Compiled);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<TransformSpeechEvent>(AccentHandler);
|
||||
}
|
||||
|
||||
private void AccentHandler(TransformSpeechEvent args)
|
||||
{
|
||||
var accentEvent = new AccentGetEvent(args.Sender, args.Message);
|
||||
|
||||
RaiseLocalEvent(args.Sender, accentEvent, true);
|
||||
args.Message = accentEvent.Message;
|
||||
}
|
||||
SubscribeLocalEvent<TransformSpeechEvent>(AccentHandler);
|
||||
}
|
||||
|
||||
public sealed class AccentGetEvent : EntityEventArgs
|
||||
private void AccentHandler(TransformSpeechEvent args)
|
||||
{
|
||||
/// <summary>
|
||||
/// The entity to apply the accent to.
|
||||
/// </summary>
|
||||
public EntityUid Entity { get; }
|
||||
var accentEvent = new AccentGetEvent(args.Sender, args.Message);
|
||||
|
||||
/// <summary>
|
||||
/// The message to apply the accent transformation to.
|
||||
/// Modify this to apply the accent.
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
public AccentGetEvent(EntityUid entity, string message)
|
||||
{
|
||||
Entity = entity;
|
||||
Message = message;
|
||||
}
|
||||
RaiseLocalEvent(args.Sender, accentEvent, true);
|
||||
args.Message = accentEvent.Message;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user