diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 3bfacb5bc6..8efe0b2367 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -334,7 +334,12 @@ namespace Content.Client.Actions private void OnEntityTargetAttempt(Entity 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; diff --git a/Content.Client/Anomaly/AnomalySystem.cs b/Content.Client/Anomaly/AnomalySystem.cs index 83f15a9086..4eee43fac6 100644 --- a/Content.Client/Anomaly/AnomalySystem.cs +++ b/Content.Client/Anomaly/AnomalySystem.cs @@ -62,11 +62,11 @@ public sealed class AnomalySystem : SharedAnomalySystem { base.Update(frameTime); - var query = EntityQueryEnumerator(); + var query = EntityQueryEnumerator(); - 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)); diff --git a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs index 20902722ff..94bfb4b4f3 100644 --- a/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs +++ b/Content.Client/Atmos/Consoles/AtmosMonitoringConsoleNavMapControl.cs @@ -162,10 +162,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl { var list = new List(); - 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; diff --git a/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs new file mode 100644 index 0000000000..f16774ce24 --- /dev/null +++ b/Content.Client/Atmos/EntitySystems/GasCanisterAppearanceSystem.cs @@ -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; + +/// +/// Used to change the appearance of gas canisters. +/// +public sealed class GasCanisterAppearanceSystem : VisualizerSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + protected override void OnAppearanceChange(EntityUid uid, GasCanisterComponent component, ref AppearanceChangeEvent args) + { + if (!AppearanceSystem.TryGetData(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); + } +} diff --git a/Content.Client/Atmos/EntitySystems/PipeColorVisualizerSystem.cs b/Content.Client/Atmos/EntitySystems/PipeColorVisualizerSystem.cs index 5595f441f7..b23a44e403 100644 --- a/Content.Client/Atmos/EntitySystems/PipeColorVisualizerSystem.cs +++ b/Content.Client/Atmos/EntitySystems/PipeColorVisualizerSystem.cs @@ -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 { + [Dependency] private readonly SharedItemSystem _itemSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetVisuals); + SubscribeLocalEvent(OnDrawInGrid); + } + + /// + /// This method is used to display the color changes of the pipe on the screen.. + /// + private void OnGetVisuals(Entity item, ref GetInhandVisualsEvent args) + { + foreach (var (_, layerData) in args.Layers) + { + if (TryComp(item.Owner, out AtmosPipeColorComponent? pipeColor)) + layerData.Color = pipeColor.Color; + } + } + + /// + /// This method is used to change the pipe's color in a container grid. + /// + private void OnDrawInGrid(Entity 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(uid, out var sprite) @@ -15,6 +50,8 @@ public sealed class PipeColorVisualizerSystem : VisualizerSystem + MinSize="500 500" Resizable="True" Title="{Loc air-alarm-ui-title}"> diff --git a/Content.Client/Cooldown/CooldownGraphic.cs b/Content.Client/Cooldown/CooldownGraphic.cs index 6779e4027a..c973b9572b 100644 --- a/Content.Client/Cooldown/CooldownGraphic.cs +++ b/Content.Client/Cooldown/CooldownGraphic.cs @@ -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 { diff --git a/Content.Client/Damage/Systems/StaminaSystem.cs b/Content.Client/Damage/Systems/StaminaSystem.cs index 69eb461a7d..046c72a8fa 100644 --- a/Content.Client/Damage/Systems/StaminaSystem.cs +++ b/Content.Client/Damage/Systems/StaminaSystem.cs @@ -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(OnAnimationCompleted); + SubscribeLocalEvent(OnActiveStaminaShutdown); + SubscribeLocalEvent(OnMobStateChanged); + } + + protected override void OnStamHandleState(Entity entity, ref AfterAutoHandleStateEvent args) + { + base.OnStamHandleState(entity, ref args); + + TryStartAnimation(entity); + } + + private void OnActiveStaminaShutdown(Entity 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(entity, out var stamina)) + return; + + StopAnimation((entity, stamina)); + } + + protected override void OnShutdown(Entity entity, ref ComponentShutdown args) + { + base.OnShutdown(entity, ref args); + + StopAnimation(entity); + } + + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState == MobState.Dead) + StopAnimation(ent); + } + + private void TryStartAnimation(Entity entity) + { + if (!TryComp(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 entity) + { + if(!Resolve(entity, ref entity.Comp2)) + return; + + _animation.Stop(entity.Owner, StaminaAnimationKey); + entity.Comp1.StartOffset = entity.Comp2.Offset; + } + + private void OnAnimationCompleted(Entity entity, ref AnimationCompletedEvent args) + { + if (args.Key != StaminaAnimationKey || !args.Finished || !TryComp(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(entity)) + return; + + PlayAnimation((entity, entity.Comp, sprite)); + } + + private void PlayAnimation(Entity 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); + } } diff --git a/Content.Client/Devour/DevourSystem.cs b/Content.Client/Devour/DevourSystem.cs deleted file mode 100644 index ad905ffab3..0000000000 --- a/Content.Client/Devour/DevourSystem.cs +++ /dev/null @@ -1,6 +0,0 @@ -using Content.Shared.Devour; - -namespace Content.Client.Devour; -public sealed class DevourSystem : SharedDevourSystem -{ -} diff --git a/Content.Client/Disposal/PressureBar.cs b/Content.Client/Disposal/PressureBar.cs index 7b2ebacaf7..de785de8c6 100644 --- a/Content.Client/Disposal/PressureBar.cs +++ b/Content.Client/Disposal/PressureBar.cs @@ -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; diff --git a/Content.Client/Doors/DoorSystem.cs b/Content.Client/Doors/DoorSystem.cs index cb17cfaf21..3d9a3e2a9a 100644 --- a/Content.Client/Doors/DoorSystem.cs +++ b/Content.Client/Doors/DoorSystem.cs @@ -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(entity, DoorVisuals.State, out var state, args.Component)) state = DoorState.Closed; - if (AppearanceSystem.TryGetData(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component)) - UpdateSpriteLayers((entity.Owner, args.Sprite), baseRsi); + if (AppearanceSystem.TryGetData(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 sprite, string baseRsi) + private void UpdateSpriteLayers(Entity sprite, string targetProto) { - if (!_resourceCache.TryGetResource(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); } } diff --git a/Content.Client/Drowsiness/DrowsinessOverlay.cs b/Content.Client/Drowsiness/DrowsinessOverlay.cs index 9c47ec2fbe..3770802302 100644 --- a/Content.Client/Drowsiness/DrowsinessOverlay.cs +++ b/Content.Client/Drowsiness/DrowsinessOverlay.cs @@ -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(); + _statusEffects = _sysMan.GetEntitySystem(); _drowsinessShader = _prototypeManager.Index(Shader).InstanceUnique(); } diff --git a/Content.Client/Drowsiness/DrowsinessSystem.cs b/Content.Client/Drowsiness/DrowsinessSystem.cs index 3b35101489..632d2134ca 100644 --- a/Content.Client/Drowsiness/DrowsinessSystem.cs +++ b/Content.Client/Drowsiness/DrowsinessSystem.cs @@ -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!; diff --git a/Content.Client/Drugs/RainbowOverlay.cs b/Content.Client/Drugs/RainbowOverlay.cs index a2a431feb6..e40fcdfca2 100644 --- a/Content.Client/Drugs/RainbowOverlay.cs +++ b/Content.Client/Drugs/RainbowOverlay.cs @@ -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(); + _statusEffects = _sysMan.GetEntitySystem(); _rainbowShader = _prototypeManager.Index(Shader).InstanceUnique(); _config.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true); diff --git a/Content.Client/Holopad/HolopadSystem.cs b/Content.Client/Holopad/HolopadSystem.cs index 9fc0815516..51491de98f 100644 --- a/Content.Client/Holopad/HolopadSystem.cs +++ b/Content.Client/Holopad/HolopadSystem.cs @@ -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; diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 5b7614780b..6b279cb12c 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -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); diff --git a/Content.Client/Light/RgbLightControllerSystem.cs b/Content.Client/Light/RgbLightControllerSystem.cs index 9977a35118..292820ec27 100644 --- a/Content.Client/Light/RgbLightControllerSystem.cs +++ b/Content.Client/Light/RgbLightControllerSystem.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Numerics; using Content.Client.Items.Systems; using Content.Shared.Clothing; using Content.Shared.Hands; diff --git a/Content.Client/MouseRotator/MouseRotatorSystem.cs b/Content.Client/MouseRotator/MouseRotatorSystem.cs index 18d60d9a7b..2ee6e43368 100644 --- a/Content.Client/MouseRotator/MouseRotatorSystem.cs +++ b/Content.Client/MouseRotator/MouseRotatorSystem.cs @@ -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) }); } } diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 9ef3def541..ee72a91fb7 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -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); diff --git a/Content.Client/Parallax/Managers/GeneratedParallaxCache.cs b/Content.Client/Parallax/Managers/GeneratedParallaxCache.cs index 845f631de8..911f655c6d 100644 --- a/Content.Client/Parallax/Managers/GeneratedParallaxCache.cs +++ b/Content.Client/Parallax/Managers/GeneratedParallaxCache.cs @@ -195,7 +195,6 @@ public sealed class GeneratedParallaxCache : IPostInjectInit public required ResPath ConfigPath; public required Task LoadTask; public required CancellationTokenSource CancellationSource; - public ValueList CancelRegistrations; public int RefCount; } diff --git a/Content.Client/Power/APC/UI/ApcMenu.xaml.cs b/Content.Client/Power/APC/UI/ApcMenu.xaml.cs index 25e885b3c7..65464abd54 100644 --- a/Content.Client/Power/APC/UI/ApcMenu.xaml.cs +++ b/Content.Client/Power/APC/UI/ApcMenu.xaml.cs @@ -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; diff --git a/Content.Client/Power/EntitySystems/StaticPowerSystem.cs b/Content.Client/Power/EntitySystems/StaticPowerSystem.cs index 2ca945cbbd..f803b92067 100644 --- a/Content.Client/Power/EntitySystems/StaticPowerSystem.cs +++ b/Content.Client/Power/EntitySystems/StaticPowerSystem.cs @@ -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; } diff --git a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs index d3671e265a..e2b27c1b62 100644 --- a/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs +++ b/Content.Client/Power/PowerMonitoringWindow.xaml.Widgets.cs @@ -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; diff --git a/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs index 8bb49bdb2b..c2d978e609 100644 --- a/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs +++ b/Content.Client/Shuttles/UI/BaseShuttleControl.xaml.cs @@ -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; diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs index 1a0e3aae81..2dcec6b44a 100644 --- a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs +++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs @@ -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})"; diff --git a/Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs b/Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs new file mode 100644 index 0000000000..60d8ea413a --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeBoundUserInterface.cs @@ -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(); + _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)); + } +} diff --git a/Content.Client/SmartFridge/SmartFridgeItem.xaml b/Content.Client/SmartFridge/SmartFridgeItem.xaml new file mode 100644 index 0000000000..3960d7ce42 --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeItem.xaml @@ -0,0 +1,16 @@ + + + diff --git a/Content.Client/SmartFridge/SmartFridgeItem.xaml.cs b/Content.Client/SmartFridge/SmartFridgeItem.xaml.cs new file mode 100644 index 0000000000..c69d2e7de2 --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeItem.xaml.cs @@ -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; + } +} diff --git a/Content.Client/SmartFridge/SmartFridgeMenu.xaml b/Content.Client/SmartFridge/SmartFridgeMenu.xaml new file mode 100644 index 0000000000..73cb0f3925 --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeMenu.xaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs b/Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs new file mode 100644 index 0000000000..c896e7fada --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeMenu.xaml.cs @@ -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? 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 ent) + { + var listData = new List(); + + 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; + } +} diff --git a/Content.Client/SmartFridge/SmartFridgeUISystem.cs b/Content.Client/SmartFridge/SmartFridgeUISystem.cs new file mode 100644 index 0000000000..4068c27e05 --- /dev/null +++ b/Content.Client/SmartFridge/SmartFridgeUISystem.cs @@ -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(OnSmartFridgeAfterState); + } + + private void OnSmartFridgeAfterState(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (!_uiSystem.TryGetOpenUi(ent.Owner, SmartFridgeUiKey.Key, out var bui)) + return; + + bui.Refresh(); + } +} diff --git a/Content.Client/SprayPainter/SprayPainterSystem.cs b/Content.Client/SprayPainter/SprayPainterSystem.cs index 6a1d27e98b..8f7d7f0362 100644 --- a/Content.Client/SprayPainter/SprayPainterSystem.cs +++ b/Content.Client/SprayPainter/SprayPainterSystem.cs @@ -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; +/// +/// Client-side spray painter functions. Caches information for spray painter windows and updates the UI to reflect component state. +/// public sealed class SprayPainterSystem : SharedSprayPainterSystem { - [Dependency] private readonly IResourceCache _resourceCache = default!; + [Dependency] private readonly UserInterfaceSystem _ui = default!; - public List Entries { get; private set; } = new(); + public List Decals = []; + public Dictionary> PaintableGroupsByCategory = new(); + public Dictionary> PaintableStylesByGroup = new(); - protected override void CacheStyles() + public override void Initialize() { - base.CacheStyles(); + base.Initialize(); - Entries.Clear(); - foreach (var style in Styles) + Subs.ItemStatus(ent => new StatusControl(ent)); + SubscribeLocalEvent(OnStateUpdate); + SubscribeLocalEvent(OnPrototypesReloaded); + + CachePrototypes(); + } + + private void OnStateUpdate(Entity ent, ref AfterAutoHandleStateEvent args) + { + UpdateUi(ent); + } + + protected override void UpdateUi(Entity ent) + { + if (_ui.TryGetOpenUi(ent.Owner, SprayPainterUiKey.Key, out var bui)) + bui.Update(); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + if (!args.WasModified() || !args.WasModified() || !args.WasModified()) + return; + + CachePrototypes(); + } + + private void CachePrototypes() + { + PaintableGroupsByCategory.Clear(); + PaintableStylesByGroup.Clear(); + foreach (var category in Proto.EnumeratePrototypes().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(); + 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(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().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 _entity; + private DecalPaintMode? _lastPaintingDecals = null; + + public StatusControl(Entity 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; - } -} +/// +/// A spray paintable decal, mapped by ID. +/// +public sealed record SprayPainterDecalEntry(string Name, SpriteSpecifier Sprite); diff --git a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs index 7d6a6cf2a5..701ec80bac 100644 --- a/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs +++ b/Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs @@ -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 +/// +/// A BUI for a spray painter. Allows selecting pipe colours, decals, and paintable object types sorted by category. +/// +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(); - - _window.OnSpritePicked = OnSpritePicked; - _window.OnColorPicked = OnColorPicked; - - if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? comp)) + if (_window == null) { - _window.Populate(EntMan.System().Entries, comp.Index, comp.PickedColor, comp.ColorPalette); + _window = this.CreateWindow(); + + _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(); + _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 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)); } } diff --git a/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml new file mode 100644 index 0000000000..0d5c8e4f16 --- /dev/null +++ b/Content.Client/SprayPainter/UI/SprayPainterDecals.xaml @@ -0,0 +1,26 @@ + + +