From 37940bbcc50ed55492f0e7064050c59771d02bfe Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Sun, 20 Sep 2026 00:37:57 +0200 Subject: [PATCH 01/11] Port upstream fixes, CM14 speech bubbles, and visual effects package - Upstream fixes: predict sprite movement visuals, fix eye tracking in vehicles, allow eye rotation when immobilized, add photograph inhand sprites, unshaded MMI mind lights, fix food item dupe, and audio/light mapinit sync - Overhead speech bubbles: dynamic opacity scaling with stealth/invisibility, speech styles (commander/megaphone), language icons, and click pass-through - Visual systems: bullet tracers, procedural emote animations, explosion refractive shockwave rings, phosphor night vision enhancement, and Sandevistan chromatic vision with motion trails --- .../Animations/EmoteAnimationSystem.cs | 193 ++++++++++++++++++ .../Sandevistan/SandevistanVisionOverlay.cs | 98 +++++++++ .../Sandevistan/SandevistanVisionSystem.cs | 54 +++++ .../Overlays/ShockWave/ShockWaveOverlay.cs | 133 ++++++++++++ .../Overlays/ShockWave/ShockWaveSystem.cs | 65 ++++++ .../Weapons/Ranged/Tracer/TracerOverlay.cs | 22 ++ .../Weapons/Ranged/Tracer/TracerSystem.cs | 122 +++++++++++ Content.Client/Chat/UI/SpeechBubble.cs | 136 ++++++++++-- .../Disposal/Unit/DisposalUnitSystem.cs | 2 +- .../PoweredLightVisualizerSystem.cs | 2 +- .../Systems/ClientSpriteMovementSystem.cs | 20 +- Content.Client/Silicons/Borgs/BorgSystem.cs | 17 +- .../Sheetlets/Hud/TooltipSheetlet.cs | 16 +- Content.Client/Stylesheets/StyleNano.cs | 26 +++ .../Systems/TimerTriggerVisualizerSystem.cs | 2 +- .../Animations/EmoteAnimationSystem.cs | 44 ++++ Content.Server/Audio/AmbientSoundSystem.cs | 8 + .../Kitchen/EntitySystems/MicrowaveSystem.cs | 14 +- .../Light/EntitySystems/LitOnPoweredSystem.cs | 18 +- .../Animations/EmoteAnimationComponent.cs | 16 ++ .../Animations/SharedEmoteAnimationSystem.cs | 13 ++ .../Sandevistan/SandevistanVisionComponent.cs | 14 ++ .../Overlays/ShockWave/ShockWaveComponent.cs | 25 +++ .../Weapons/Ranged/Tracer/TracerComponent.cs | 40 ++++ Content.Shared/Chat/MsgChatMessage.cs | 8 +- .../EntitySystems/SharedPoweredLightSystem.cs | 2 +- .../Systems/SharedMoverController.Relay.cs | 7 - .../Systems/SharedSpriteMovementSystem.cs | 12 +- .../EntitySystems/IngestionSystem.API.cs | 29 +-- .../Silicons/Borgs/Components/MMIComponent.cs | 45 +++- .../Vehicle/Systems/VehicleSystem.cs | 11 +- .../Weapons/Misc/SharedGrapplingGunSystem.cs | 2 +- Resources/Audio/Machines/light_tube_on.ogg | Bin 10444 -> 11585 bytes .../en-US/Ashfall/animations/emotes.ftl | 14 ++ .../ru-RU/Ashfall/animations/emotes.ftl | 14 ++ .../Ashfall/Entities/Effects/shockwave.yml | 11 + .../Prototypes/Ashfall/Shaders/shaders.yml | 5 + .../Ashfall/Voice/speech_emotes.yml | 57 ++++++ Resources/Prototypes/Body/species_base.yml | 1 + .../Objects/Consumable/Food/produce.yml | 24 +-- .../Objects/Devices/travel_camera.yml | 10 +- .../Objects/Specific/Robotics/mmi.yml | 11 +- .../Weapons/Guns/Projectiles/Bullets/base.yml | 6 + .../Structures/Lighting/base_lighting.yml | 4 +- .../Ashfall/Shaders/sandevistan_vision.swsl | 14 ++ .../Objects/Misc/photograph.rsi/meta.json | 24 ++- .../Misc/photograph.rsi/paper-inhand-left.png | Bin 0 -> 256 bytes .../photograph.rsi/paper-inhand-right.png | Bin 0 -> 271 bytes .../Specific/Robotics/mmi.rsi/empty.png | Bin 0 -> 96 bytes .../Specific/Robotics/mmi.rsi/meta.json | 56 ++--- .../Specific/Robotics/mmi.rsi/mmi_light.png | Bin 0 -> 115 bytes .../Specific/Robotics/mmi.rsi/mmi_on.png | Bin 0 -> 659 bytes .../Robotics/mmi.rsi/posibrain-occupied.png | Bin 2294 -> 609 bytes .../Robotics/mmi.rsi/posibrain-searching.png | Bin 3396 -> 739 bytes Resources/Textures/Shaders/nightvision.swsl | 47 ++++- .../Machines/biofabricator.rsi/meta.json | 107 +++++----- .../Machines/biofabricator.rsi/unlit.png | Bin 405 -> 439 bytes 57 files changed, 1403 insertions(+), 218 deletions(-) create mode 100644 Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs create mode 100644 Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs create mode 100644 Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs create mode 100644 Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs create mode 100644 Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs create mode 100644 Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs create mode 100644 Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs create mode 100644 Content.Server/Ashfall/Animations/EmoteAnimationSystem.cs create mode 100644 Content.Shared/Ashfall/Animations/EmoteAnimationComponent.cs create mode 100644 Content.Shared/Ashfall/Animations/SharedEmoteAnimationSystem.cs create mode 100644 Content.Shared/Ashfall/Overlays/Sandevistan/SandevistanVisionComponent.cs create mode 100644 Content.Shared/Ashfall/Overlays/ShockWave/ShockWaveComponent.cs create mode 100644 Content.Shared/Ashfall/Weapons/Ranged/Tracer/TracerComponent.cs create mode 100644 Resources/Locale/en-US/Ashfall/animations/emotes.ftl create mode 100644 Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl create mode 100644 Resources/Prototypes/Ashfall/Entities/Effects/shockwave.yml create mode 100644 Resources/Textures/Ashfall/Shaders/sandevistan_vision.swsl create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-left.png create mode 100644 Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-right.png create mode 100644 Resources/Textures/Objects/Specific/Robotics/mmi.rsi/empty.png create mode 100644 Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_light.png create mode 100644 Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_on.png diff --git a/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs b/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs new file mode 100644 index 00000000000..73ceab8a227 --- /dev/null +++ b/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs @@ -0,0 +1,193 @@ +using System.Numerics; +using Content.Shared.Ashfall.Animations; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Shared.Animations; + +namespace Content.Client.Ashfall.Animations; + +public sealed partial class EmoteAnimationSystem : SharedEmoteAnimationSystem +{ + [Dependency] private AnimationPlayerSystem _animationPlayer = default!; + + private const string EmoteAnimKey = "AshfallEmoteAnimation"; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnHandleState); + } + + private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + if (ent.Comp.CurAnimationIndex == ent.Comp.LastClientAnimationIndex) + return; + + ent.Comp.LastClientAnimationIndex = ent.Comp.CurAnimationIndex; + + switch (ent.Comp.AnimationId) + { + case AnimationFlip: + PlayEmoteFlip(ent.Owner); + break; + case AnimationJump: + PlayEmoteJump(ent.Owner); + break; + case AnimationTurn: + case AnimationSpin: + PlayEmoteTurn(ent.Owner); + break; + case AnimationTremble: + case AnimationShiver: + PlayEmoteTremble(ent.Owner); + break; + case AnimationTailWag: + PlayEmoteTail(ent.Owner, true); + break; + case AnimationTailStop: + PlayEmoteTail(ent.Owner, false); + break; + } + } + + public void PlayEmoteFlip(EntityUid uid) + { + if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + return; + + var baseAngle = Angle.Zero; + if (TryComp(uid, out var sprite)) + baseAngle = sprite.Rotation; + + var anim = new Animation + { + Length = TimeSpan.FromMilliseconds(500), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Rotation), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees), 0f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees + 180), 0.25f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees + 360), 0.5f), + } + } + } + }; + + _animationPlayer.Play(uid, anim, EmoteAnimKey); + } + + public void PlayEmoteJump(EntityUid uid) + { + if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + return; + + var anim = new Animation + { + Length = TimeSpan.FromMilliseconds(250), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + InterpolationMode = AnimationInterpolationMode.Cubic, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0f), + new AnimationTrackProperty.KeyFrame(new Vector2(0, 0.65f), 0.125f), + new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0.25f), + } + } + } + }; + + _animationPlayer.Play(uid, anim, EmoteAnimKey); + } + + public void PlayEmoteTurn(EntityUid uid) + { + if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + return; + + var baseAngle = Angle.Zero; + if (TryComp(uid, out var sprite)) + baseAngle = sprite.Rotation; + + var anim = new Animation + { + Length = TimeSpan.FromMilliseconds(600), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Rotation), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees), 0f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees + 180), 0.3f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(baseAngle.Degrees + 360), 0.6f), + } + } + } + }; + + _animationPlayer.Play(uid, anim, EmoteAnimKey); + } + + public void PlayEmoteTremble(EntityUid uid) + { + if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + return; + + var anim = new Animation + { + Length = TimeSpan.FromMilliseconds(400), + AnimationTracks = + { + new AnimationTrackComponentProperty + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Offset), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0f), + new AnimationTrackProperty.KeyFrame(new Vector2(-0.06f, 0), 0.05f), + new AnimationTrackProperty.KeyFrame(new Vector2(0.06f, 0), 0.10f), + new AnimationTrackProperty.KeyFrame(new Vector2(-0.05f, 0), 0.15f), + new AnimationTrackProperty.KeyFrame(new Vector2(0.05f, 0), 0.20f), + new AnimationTrackProperty.KeyFrame(new Vector2(-0.03f, 0), 0.25f), + new AnimationTrackProperty.KeyFrame(new Vector2(0.03f, 0), 0.30f), + new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0.40f), + } + } + } + }; + + _animationPlayer.Play(uid, anim, EmoteAnimKey); + } + + public void PlayEmoteTail(EntityUid uid, bool start) + { + if (!TryComp(uid, out var sprite)) + return; + + foreach (var layer in sprite.AllLayers) + { + if (layer.RsiState.Name != null && layer.RsiState.Name.Contains("tail", StringComparison.OrdinalIgnoreCase)) + { + layer.AutoAnimated = start; + if (!start) + layer.AnimationTime = 0; + } + } + } +} diff --git a/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs new file mode 100644 index 00000000000..a72f83e539e --- /dev/null +++ b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs @@ -0,0 +1,98 @@ +using System.Numerics; +using Content.Shared.Ashfall.Overlays.Sandevistan; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Ashfall.Overlays.Sandevistan; + +public sealed partial class SandevistanVisionOverlay : Overlay +{ + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private IPlayerManager _playerManager = default!; + [Dependency] private IEntityManager _entityManager = default!; + [Dependency] private IGameTiming _timing = default!; + + public override bool RequestScreenTexture => true; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + private static readonly ProtoId ShaderProto = "AshfallSandevistanVision"; + private readonly ShaderInstance _shader; + private readonly TransformSystem _transformSystem; + + private readonly List<(Vector2 Position, Angle Rotation, TimeSpan Time)> _afterImages = new(); + private Vector2 _lastPosition = Vector2.Zero; + + public SandevistanVisionOverlay() + { + IoCManager.InjectDependencies(this); + ZIndex = 20; + _shader = _prototypeManager.Index(ShaderProto).InstanceUnique(); + _transformSystem = _entityManager.System(); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (ScreenTexture == null) + return false; + + if (_playerManager.LocalEntity is not { Valid: true } player + || !_entityManager.TryGetComponent(player, out var comp) + || !comp.Enabled) + { + _afterImages.Clear(); + return false; + } + + return base.BeforeDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null || args.Viewport.Eye == null) + return; + + var player = _playerManager.LocalEntity; + if (player == null || !_entityManager.TryGetComponent(player.Value, out var playerSprite)) + return; + + var worldHandle = args.WorldHandle; + var viewport = args.WorldBounds; + var eye = args.Viewport.Eye; + + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + worldHandle.SetTransform(Matrix3x2.Identity); + worldHandle.UseShader(_shader); + worldHandle.DrawRect(viewport, Color.White); + worldHandle.UseShader(null); + + var playerXform = _entityManager.GetComponent(player.Value); + var playerPos = _transformSystem.GetWorldPosition(playerXform); + var playerRot = _transformSystem.GetWorldRotation(playerXform); + var curTime = _timing.CurTime; + + if (Vector2.Distance(playerPos, _lastPosition) > 0.35f) + { + _lastPosition = playerPos; + _afterImages.Add((playerPos, playerRot, curTime)); + } + + _afterImages.RemoveAll(x => (curTime - x.Time).TotalSeconds > 0.30f); + + // Render after-image motion trail silhouettes + foreach (var ghost in _afterImages) + { + var age = (float)(curTime - ghost.Time).TotalSeconds; + var alpha = Math.Clamp(1f - age / 0.30f, 0f, 0.5f); + var originalColor = playerSprite.Color; + playerSprite.Color = new Color(0.2f, 0.9f, 0.85f, alpha * 0.40f); + playerSprite.Render(worldHandle, eye.Rotation, ghost.Rotation, null, ghost.Position); + playerSprite.Color = originalColor; + } + + // Render player cleanly on top + playerSprite.Render(worldHandle, eye.Rotation, playerRot, null, playerPos); + } +} diff --git a/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs new file mode 100644 index 00000000000..25199b78785 --- /dev/null +++ b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs @@ -0,0 +1,54 @@ +using Content.Shared.Ashfall.Overlays.Sandevistan; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Player; + +namespace Content.Client.Ashfall.Overlays.Sandevistan; + +public sealed partial class SandevistanVisionSystem : EntitySystem +{ + [Dependency] private IOverlayManager _overlayMan = default!; + [Dependency] private IPlayerManager _playerMan = default!; + + private SandevistanVisionOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + _overlay = new SandevistanVisionOverlay(); + + SubscribeLocalEvent(OnComponentStartup); + SubscribeLocalEvent(OnComponentShutdown); + SubscribeLocalEvent(OnPlayerAttached); + SubscribeLocalEvent(OnPlayerDetached); + } + + public override void Shutdown() + { + _overlayMan.RemoveOverlay(_overlay); + base.Shutdown(); + } + + private void OnComponentStartup(Entity ent, ref ComponentStartup args) + { + if (ent.Owner == _playerMan.LocalEntity) + _overlayMan.AddOverlay(_overlay); + } + + private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Owner == _playerMan.LocalEntity) + _overlayMan.RemoveOverlay(_overlay); + } + + private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) + { + _overlayMan.AddOverlay(_overlay); + } + + private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args) + { + _overlayMan.RemoveOverlay(_overlay); + } +} diff --git a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs new file mode 100644 index 00000000000..c3102a80b0b --- /dev/null +++ b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs @@ -0,0 +1,133 @@ +using System.Numerics; +using Content.Shared.Ashfall.Overlays.ShockWave; +using Robust.Client.Graphics; +using Robust.Shared.Enums; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Ashfall.Overlays.ShockWave; + +public sealed partial class ShockWaveOverlay : Overlay +{ + [Dependency] private IEntityManager _entMan = default!; + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private IGameTiming _timing = default!; + + private SharedTransformSystem? _xformSystem; + public override OverlaySpace Space => OverlaySpace.WorldSpace; + public override bool RequestScreenTexture => true; + private readonly ShaderInstance _shader; + private static readonly ProtoId ShaderProto = "ScreechShockWave"; + + private readonly List<(EntityUid Entity, InnerShaderInstance Instance)> _cached = new(); + private int _currentCount; + private const int MaximumInstances = 10; + + private readonly Vector2[] _positions; + private readonly float[] _waveStrengths; + private readonly float[] _waveSpeeds; + private readonly float[] _downScales; + private readonly float[] _fades; + private readonly float[] _times; + + public ShockWaveOverlay() + { + IoCManager.InjectDependencies(this); + ZIndex = 15; + _shader = _prototypeManager.Index(ShaderProto).InstanceUnique(); + _positions = new Vector2[MaximumInstances]; + _waveStrengths = new float[MaximumInstances]; + _waveSpeeds = new float[MaximumInstances]; + _downScales = new float[MaximumInstances]; + _fades = new float[MaximumInstances]; + _times = new float[MaximumInstances]; + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (args.Viewport.Eye == null || ScreenTexture == null) + return false; + + if (_xformSystem is null && !_entMan.TrySystem(out _xformSystem)) + return false; + + _currentCount = 0; + + _cached.RemoveAll(entry => (float)(_timing.CurTime - entry.Instance.InitTime).TotalSeconds > entry.Instance.FadeTime); + + foreach (var (entityUid, distortion) in _cached) + { + if (!_entMan.EntityExists(entityUid)) + continue; + + var xform = _entMan.GetComponent(entityUid); + if (xform.MapID != args.MapId) + continue; + + var mapPos = _xformSystem.GetWorldPosition(xform); + var tempCoords = args.Viewport.WorldToLocal(mapPos); + + tempCoords.Y = 1 - tempCoords.Y / args.Viewport.Size.Y; + tempCoords.X /= args.Viewport.Size.X; + + var time = (float)(_timing.CurTime - distortion.InitTime).TotalSeconds; + var fade = 1f - Math.Clamp(time / distortion.FadeTime, 0f, 1f); + + var i = _currentCount; + _positions[i] = tempCoords; + _waveStrengths[i] = distortion.WaveStrength; + _waveSpeeds[i] = distortion.WaveSpeed; + _downScales[i] = distortion.DownScale; + _fades[i] = fade; + _times[i] = time; + + _currentCount += 1; + if (_currentCount == MaximumInstances) + break; + } + + return _currentCount != 0; + } + + protected override void Draw(in OverlayDrawArgs args) + { + if (ScreenTexture == null || args.Viewport.Eye == null) + return; + + _shader.SetParameter("positions", _positions); + _shader.SetParameter("waveSpeeds", _waveSpeeds); + _shader.SetParameter("downScales", _downScales); + _shader.SetParameter("waveStrengths", _waveStrengths); + _shader.SetParameter("fades", _fades); + _shader.SetParameter("times", _times); + _shader.SetParameter("count", _currentCount); + _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture); + _shader.SetParameter("renderScale", args.Viewport.RenderScale * args.Viewport.Eye.Scale); + + var worldHandle = args.WorldHandle; + worldHandle.UseShader(_shader); + worldHandle.DrawRect(args.WorldBounds, Color.White); + worldHandle.UseShader(null); + } + + public void Register(Entity ent) + { + _cached.Add((ent.Owner, new InnerShaderInstance + { + WaveSpeed = ent.Comp.WaveSpeed, + WaveStrength = ent.Comp.WaveStrength, + DownScale = ent.Comp.DownScale, + FadeTime = ent.Comp.FadeTime, + InitTime = ent.Comp.InitTime == TimeSpan.Zero ? _timing.CurTime : ent.Comp.InitTime + })); + } + + private struct InnerShaderInstance + { + public float WaveSpeed; + public float WaveStrength; + public float DownScale; + public float FadeTime; + public TimeSpan InitTime; + } +} diff --git a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs new file mode 100644 index 00000000000..68f96b47a1b --- /dev/null +++ b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs @@ -0,0 +1,65 @@ +using Content.Shared.Ashfall.Overlays.ShockWave; +using Content.Shared.Explosion; +using Content.Shared.Explosion.Components; +using Robust.Client.Graphics; +using Robust.Shared.GameStates; +using Robust.Shared.Timing; + +namespace Content.Client.Ashfall.Overlays.ShockWave; + +public sealed partial class ShockWaveSystem : EntitySystem +{ + [Dependency] private IOverlayManager _overlayMan = default!; + [Dependency] private IGameTiming _timing = default!; + + private readonly HashSet _registered = new(); + private readonly HashSet _spawnedForExplosion = new(); + private ShockWaveOverlay _overlay = default!; + + public override void Initialize() + { + base.Initialize(); + + _overlay = new ShockWaveOverlay(); + _overlayMan.AddOverlay(_overlay); + + SubscribeLocalEvent(OnShockWaveStartup); + SubscribeLocalEvent(OnShockWaveRemoved); + SubscribeLocalEvent(OnExplosionState); + } + + public override void Shutdown() + { + _overlayMan.RemoveOverlay(_overlay); + _registered.Clear(); + _spawnedForExplosion.Clear(); + base.Shutdown(); + } + + private void OnExplosionState(EntityUid uid, ExplosionVisualsComponent comp, ref ComponentHandleState args) + { + if (args.Current is not ExplosionVisualsState state) + return; + + if (state.Epicenter != Robust.Shared.Map.MapCoordinates.Nullspace && _spawnedForExplosion.Add(uid)) + { + Spawn("AshfallEffectShockWave", state.Epicenter); + } + } + + private void OnShockWaveStartup(Entity ent, ref ComponentStartup args) + { + if (!_registered.Add(ent.Owner)) + return; + + if (ent.Comp.InitTime == TimeSpan.Zero) + ent.Comp.InitTime = _timing.CurTime; + + _overlay.Register(ent); + } + + private void OnShockWaveRemoved(Entity ent, ref ComponentRemove args) + { + _registered.Remove(ent.Owner); + } +} diff --git a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs new file mode 100644 index 00000000000..7fad5fe6672 --- /dev/null +++ b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs @@ -0,0 +1,22 @@ +using Robust.Client.Graphics; +using Robust.Shared.Enums; + +namespace Content.Client.Ashfall.Weapons.Ranged.Tracer; + +public sealed class TracerOverlay : Overlay +{ + private readonly TracerSystem _tracer; + + public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities; + + public TracerOverlay(TracerSystem tracer) + { + _tracer = tracer; + IoCManager.InjectDependencies(this); + } + + protected override void Draw(in OverlayDrawArgs args) + { + _tracer.Draw(args.WorldHandle, args.MapId); + } +} diff --git a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs new file mode 100644 index 00000000000..f80d773c3ca --- /dev/null +++ b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs @@ -0,0 +1,122 @@ +using System.Numerics; +using Content.Shared.Ashfall.Weapons.Ranged.Tracer; +using Robust.Client.Graphics; +using Robust.Shared.Map; +using Robust.Shared.Timing; + +namespace Content.Client.Ashfall.Weapons.Ranged.Tracer; + +public sealed partial class TracerSystem : EntitySystem +{ + [Dependency] private IGameTiming _timing = default!; + [Dependency] private IOverlayManager _overlay = default!; + [Dependency] private SharedTransformSystem _transform = default!; + + private TracerOverlay _tracerOverlay = default!; + + public override void Initialize() + { + base.Initialize(); + _tracerOverlay = new TracerOverlay(this); + _overlay.AddOverlay(_tracerOverlay); + + SubscribeLocalEvent(OnTracerStart); + } + + public override void Shutdown() + { + base.Shutdown(); + _overlay.RemoveOverlay(_tracerOverlay); + } + + private void OnTracerStart(Entity ent, ref ComponentStartup args) + { + var xform = Transform(ent); + var pos = xform.Coordinates.Position; + + ent.Comp.Data = new TracerData( + new List { pos }, + _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.Lifetime) + ); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var curTime = _timing.CurTime; + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out var uid, out var tracer, out var xform)) + { + if (tracer.Data == null) + { + tracer.Data = new TracerData( + new List { xform.Coordinates.Position }, + curTime + TimeSpan.FromSeconds(tracer.Lifetime) + ); + } + + var data = tracer.Data.Value; + if (curTime > data.EndTime) + { + RemCompDeferred(uid); + continue; + } + + var currentPos = xform.Coordinates.Position; + data.PositionHistory.Add(currentPos); + + while (data.PositionHistory.Count > 2 && + GetTrailLength(data.PositionHistory) > tracer.Length) + { + data.PositionHistory.RemoveAt(0); + } + } + } + + private static float GetTrailLength(List positions) + { + var length = 0f; + for (var i = 1; i < positions.Count; i++) + { + length += Vector2.Distance(positions[i - 1], positions[i]); + } + return length; + } + + public void Draw(DrawingHandleWorld handle, MapId currentMap) + { + var query = EntityQueryEnumerator(); + + while (query.MoveNext(out _, out var tracer, out var xform)) + { + if (xform.MapID != currentMap || tracer.Data == null) + continue; + + var positions = tracer.Data.Value.PositionHistory; + + if (positions.Count < 2) + continue; + + var parentPos = Vector2.Zero; + var parentRot = Angle.Zero; + + if (xform.ParentUid.IsValid()) + { + var parent = Transform(xform.ParentUid); + parentPos = _transform.GetWorldPosition(parent); + parentRot = _transform.GetWorldRotation(parent); + } + + handle.SetTransform(Matrix3x2.Identity); + + for (var i = 1; i < positions.Count; i++) + { + var start = parentPos + parentRot.RotateVec(positions[i - 1]); + var end = parentPos + parentRot.RotateVec(positions[i]); + handle.DrawLine(start, end, tracer.Color); + } + } + } +} diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index 87b47d0fd87..1396b128609 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -1,12 +1,18 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Content.Client.Chat.Managers; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Speech; +using Content.Shared.Stealth; +using Content.Shared.Stealth.Components; +using Robust.Client.GameObjects; using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Configuration; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -16,8 +22,10 @@ public abstract partial class SpeechBubble : Control { [Dependency] private IGameTiming _timing = default!; [Dependency] private IEyeManager _eyeManager = default!; - [Dependency] private IEntityManager _entityManager = default!; + [Dependency] protected IEntityManager _entityManager = default!; [Dependency] protected IConfigurationManager ConfigManager = default!; + [Dependency] protected IPrototypeManager _prototypeManager = default!; + [Dependency] protected IResourceCache _resourceCache = default!; private readonly SharedTransformSystem _transformSystem; public enum SpeechType : byte @@ -56,6 +64,9 @@ public enum SpeechType : byte /// private TimeSpan _deathTime; + private bool _dying; + private bool _dead; + public float VerticalOffset { get; set; } private float _verticalOffsetAchieved; @@ -90,6 +101,7 @@ public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechSt IoCManager.InjectDependencies(this); _senderEntity = senderEntity; _transformSystem = _entityManager.System(); + MouseFilter = MouseFilterMode.Ignore; // Use text clipping so new messages don't overlap old ones being pushed up. RectClipContent = true; @@ -108,6 +120,38 @@ public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechSt protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null); + protected virtual Vector2 GetWorldPositionOffset(EntityUid senderEntity, TransformComponent xform) + { + return Vector2.Zero; + } + + protected virtual Vector2 GetScreenPositionOffset(EntityUid senderEntity, TransformComponent xform) + { + return Vector2.Zero; + } + + protected virtual float GetSenderVisibilityAlpha() + { + var alpha = 1f; + + if (_entityManager.TryGetComponent(_senderEntity, out var sprite)) + { + if (!sprite.Visible && _entityManager.IsClientSide(_senderEntity)) + return 0f; + + alpha = sprite.Color.A; + } + + if (_entityManager.TryGetComponent(_senderEntity, out var stealth) && stealth.Enabled) + { + var stealthSys = _entityManager.System(); + var stealthAlpha = Math.Clamp(stealthSys.GetVisibility(_senderEntity, stealth), 0f, 1f); + alpha = Math.Min(alpha, stealthAlpha); + } + + return alpha; + } + protected override void FrameUpdate(FrameEventArgs args) { base.FrameUpdate(args); @@ -116,7 +160,11 @@ protected override void FrameUpdate(FrameEventArgs args) if (_entityManager.Deleted(_senderEntity) || timeLeft <= 0) { // Timer spawn to prevent concurrent modification exception. - Timer.Spawn(0, Die); + if (!_dying) + { + _dying = true; + Timer.Spawn(0, Die); + } return; } @@ -136,15 +184,17 @@ protected override void FrameUpdate(FrameEventArgs args) return; } + var alpha = GetSenderVisibilityAlpha(); + if (timeLeft <= FadeTime.TotalSeconds) { // Update alpha if we're fading. - Modulate = Color.White.WithAlpha(timeLeft / (float)FadeTime.TotalSeconds); + Modulate = Color.White.WithAlpha((timeLeft / (float)FadeTime.TotalSeconds) * alpha); } else { // Make opaque otherwise, because it might have been hidden before - Modulate = Color.White; + Modulate = Color.White.WithAlpha(alpha); } var baseOffset = 0f; @@ -153,9 +203,9 @@ protected override void FrameUpdate(FrameEventArgs args) baseOffset = speech.SpeechBubbleOffset; var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset); - var worldPos = _transformSystem.GetWorldPosition(xform) + offset; + var worldPos = _transformSystem.GetWorldPosition(xform) + offset + GetWorldPositionOffset(_senderEntity, xform); - var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale; + var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale + GetScreenPositionOffset(_senderEntity, xform); var screenPos = lowerCenter - new Vector2(ContentSize.X / 2, ContentSize.Y + _verticalOffsetAchieved); // Round to nearest 0.5 screenPos = (screenPos * 2).Rounded() / 2; @@ -167,12 +217,14 @@ protected override void FrameUpdate(FrameEventArgs args) private void Die() { - if (Disposed) + if (Disposed || _dead) { return; } + _dead = true; OnDied?.Invoke(_senderEntity, this); + OnDied = null; } /// @@ -200,6 +252,35 @@ protected FormattedMessage ExtractAndFormatSpeechSubstring(ChatMessage message, return FormatSpeech(SharedChatSystem.GetStringInsideTag(message, tag), fontColor); } + protected bool TryGetLanguageIcon(ChatMessage message, [NotNullWhen(true)] out Texture? texture) + { + texture = null; + + if (string.IsNullOrEmpty(message.LanguageIcon)) + return false; + + if (_resourceCache.TryGetResource(new ResPath(message.LanguageIcon), out var textureResource)) + { + texture = textureResource.Texture; + return true; + } + + if (_entityManager.EntitySysManager.TryGetEntitySystem(out var spriteSystem)) + { + try + { + var specifier = new SpriteSpecifier.Rsi(new ResPath(message.LanguageIcon), "icon"); + texture = spriteSystem.Frame0(specifier); + return true; + } + catch + { + // ignored + } + } + + return false; + } } public sealed class TextSpeechBubble : SpeechBubble @@ -232,7 +313,6 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl public sealed class FancyTextSpeechBubble : SpeechBubble { - public FancyTextSpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) : base(message, senderEntity, speechStyleClass, fontColor) { @@ -240,20 +320,40 @@ public FancyTextSpeechBubble(ChatMessage message, EntityUid senderEntity, string protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null) { + if (speechStyleClass == "sayBox" && message.SpeechStyleClass != null) + { + speechStyleClass = message.SpeechStyleClass; + } + if (!ConfigManager.GetCVar(CCVars.ChatEnableFancyBubbles)) { + var container = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal }; + if (TryGetLanguageIcon(message, out var iconTexture)) + { + var textureRect = new TextureRect + { + Texture = iconTexture, + TextureScale = Vector2.One * 0.5f, + VerticalAlignment = VAlignment.Center, + Margin = new Thickness(0, 0, 4, 0) + }; + container.AddChild(textureRect); + } + var label = new RichTextLabel { MaxWidth = SpeechMaxWidth, + StyleClasses = { "bubbleContent" }, OutlineColorOverride = TextOutline.Default.Color, }; label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); + container.AddChild(label); var unfanciedPanel = new PanelContainer { StyleClasses = { "speechBox", speechStyleClass }, - Children = { label }, + Children = { container }, ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity)), }; return unfanciedPanel; @@ -275,11 +375,23 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl OutlineColorOverride = TextOutline.Default.Color, }; - //We'll be honest. *Yes* this is hacky. Doing this in a cleaner way would require a bottom-up refactor of how saycode handles sending chat messages. -Myr + var headerContainer = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal }; + if (TryGetLanguageIcon(message, out var headerIcon)) + { + var iconTexture = new TextureRect + { + Texture = headerIcon, + TextureScale = Vector2.One * 0.5f, + VerticalAlignment = VAlignment.Center, + Margin = new Thickness(0, 0, 4, 0) + }; + headerContainer.AddChild(iconTexture); + } + bubbleHeader.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleHeader", fontColor)); + headerContainer.AddChild(bubbleHeader); bubbleContent.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); - //As for below: Some day this could probably be converted to xaml. But that is not today. -Myr var mainPanel = new PanelContainer { StyleClasses = { "speechBox", speechStyleClass }, @@ -293,7 +405,7 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl var headerPanel = new PanelContainer { StyleClasses = { "speechBox", speechStyleClass }, - Children = { bubbleHeader }, + Children = { headerContainer }, ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.ChatFancyNameBackground) ? ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity) : 0f), HorizontalAlignment = HAlignment.Center, VerticalAlignment = VAlignment.Top diff --git a/Content.Client/Disposal/Unit/DisposalUnitSystem.cs b/Content.Client/Disposal/Unit/DisposalUnitSystem.cs index 9bb0f88c462..2a23ad7bc06 100644 --- a/Content.Client/Disposal/Unit/DisposalUnitSystem.cs +++ b/Content.Client/Disposal/Unit/DisposalUnitSystem.cs @@ -47,7 +47,7 @@ protected override void OnComponentInit(Entity ent, ref C anim.AnimationTracks.Add( new AnimationTrackPlaySound { - KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.FlushSound), 0) } + KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.FlushSound), 0, () => ent.Comp.FlushSound.Params) } } ); } diff --git a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs index 561cda1aa37..3f90be8bd37 100644 --- a/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs +++ b/Content.Client/Light/Visualizers/PoweredLightVisualizerSystem.cs @@ -127,7 +127,7 @@ private Animation BlinkingAnimation(PoweredLightVisualsComponent comp) { KeyFrames = { - new AnimationTrackPlaySound.KeyFrame(sound, 0.5f) + new AnimationTrackPlaySound.KeyFrame(sound, 0.5f, () => comp.BlinkingSound.Params) } }); } diff --git a/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs b/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs index ff706ea35e3..9a8688a6291 100644 --- a/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs +++ b/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs @@ -1,30 +1,36 @@ using Content.Shared.Movement.Components; +using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Robust.Client.GameObjects; namespace Content.Client.Movement.Systems; /// -/// Controls the switching of motion and standing still animation +/// Controls the switching of motion and standing still animation. /// public sealed partial class ClientSpriteMovementSystem : SharedSpriteMovementSystem { [Dependency] private SpriteSystem _sprite = default!; - [Dependency] private EntityQuery _spriteQuery = default!; + [Dependency] private EntityQuery _spriteQuery; - public override void Initialize() + protected override void OnSpriteMoveInput(Entity ent, ref SpriteMoveEvent args) { - base.Initialize(); - - SubscribeLocalEvent(OnAfterAutoHandleState); + base.OnSpriteMoveInput(ent, ref args); + UpdateSprite(ent, args.IsMoving); } + [SubscribeLocalEvent] private void OnAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + UpdateSprite(ent, ent.Comp.IsMoving); + } + + private void UpdateSprite(Entity ent, bool isMoving) { if (!_spriteQuery.TryGetComponent(ent, out var sprite)) return; - if (ent.Comp.IsMoving) + if (isMoving) { foreach (var (layer, state) in ent.Comp.MovementLayers) { diff --git a/Content.Client/Silicons/Borgs/BorgSystem.cs b/Content.Client/Silicons/Borgs/BorgSystem.cs index 647e4d3ec73..c1dcb38b273 100644 --- a/Content.Client/Silicons/Borgs/BorgSystem.cs +++ b/Content.Client/Silicons/Borgs/BorgSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Alert; +using Content.Shared.Alert; using Content.Shared.Mobs; using Content.Shared.Power.EntitySystems; using Content.Shared.PowerCell; @@ -104,6 +104,9 @@ private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref A if (!_appearance.TryGetData(uid, MMIVisuals.HasMind, out bool hasMind)) hasMind = false; + var lightColor = Color.White; + var lightVisible = false; + _sprite.LayerSetVisible((uid, sprite), MMIVisualLayers.Brain, brain); if (!brain) { @@ -115,6 +118,18 @@ private void OnMMIAppearanceChanged(EntityUid uid, MMIComponent component, ref A ? component.HasMindState : component.NoMindState; _sprite.LayerSetRsiState((uid, sprite), MMIVisualLayers.Base, state); + + lightColor = hasMind + ? component.HasMindLightColor + : component.NoMindLightColor; + lightVisible = true; + } + + // Update color if it exists. + if (_sprite.LayerMapTryGet((uid, sprite), MMIVisualLayers.Unlit, out var layerIndex, logMissing: false)) + { + _sprite.LayerSetVisible((uid, sprite), layerIndex, lightVisible); + _sprite.LayerSetColor((uid, sprite), layerIndex, lightColor); } } diff --git a/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs b/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs index 16a5cfe62f7..0cfa1f3336b 100644 --- a/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs +++ b/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs @@ -1,4 +1,4 @@ -using Content.Client.Examine; +using Content.Client.Examine; using Content.Client.Stylesheets.Fonts; using Content.Client.Stylesheets.SheetletConfigs; using Content.Client.Stylesheets.Stylesheets; @@ -59,6 +59,20 @@ public override StyleRule[] GetRules(T sheet, object config) .Class("speechBox", "emoteBox") .ParentOf(E().Class("bubbleContent")) .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(12, FontKind.Italic)), + E() + .Class("speechBox", "commanderSpeech") + .Panel(tooltipBox), + E() + .Class("speechBox", "commanderSpeech") + .ParentOf(E().Class("bubbleContent")) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(16, FontKind.Bold)), + E() + .Class("speechBox", "megaphoneSpeech") + .Panel(tooltipBox), + E() + .Class("speechBox", "megaphoneSpeech") + .ParentOf(E().Class("bubbleContent")) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(20, FontKind.Bold)), ]; } } diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index 2d0229627f8..aef7606aff0 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -956,6 +956,32 @@ public StyleNano(IResourceCache resCache) : base(resCache) new StyleProperty("font", notoSansItalic12), }), + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] { "speechBox", "commanderSpeech" }, null, null), + new SelectorElement(typeof(RichTextLabel), new[] { "bubbleContent" }, null, null)), + new[] + { + new StyleProperty("font", notoSansBold16), + }), + + new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "commanderSpeech"}, null, null), new[] + { + new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox) + }), + + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] { "speechBox", "megaphoneSpeech" }, null, null), + new SelectorElement(typeof(RichTextLabel), new[] { "bubbleContent" }, null, null)), + new[] + { + new StyleProperty("font", notoSansBold20), + }), + + new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "megaphoneSpeech"}, null, null), new[] + { + new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox) + }), + new StyleRule(new SelectorElement(typeof(RichTextLabel), new[] {StyleClassLabelKeyText}, null, null), new[] { new StyleProperty(Label.StylePropertyFont, notoSansBold12), diff --git a/Content.Client/Trigger/Systems/TimerTriggerVisualizerSystem.cs b/Content.Client/Trigger/Systems/TimerTriggerVisualizerSystem.cs index 23ebf503331..626b7d6221b 100644 --- a/Content.Client/Trigger/Systems/TimerTriggerVisualizerSystem.cs +++ b/Content.Client/Trigger/Systems/TimerTriggerVisualizerSystem.cs @@ -35,7 +35,7 @@ private void OnComponentInit(Entity ent, ref Compo ent.Comp.PrimingAnimation.AnimationTracks.Add( new AnimationTrackPlaySound() { - KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.PrimingSound), 0) } + KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.PrimingSound), 0, () => ent.Comp.PrimingSound.Params) } } ); } diff --git a/Content.Server/Ashfall/Animations/EmoteAnimationSystem.cs b/Content.Server/Ashfall/Animations/EmoteAnimationSystem.cs new file mode 100644 index 00000000000..fb4ca0f6573 --- /dev/null +++ b/Content.Server/Ashfall/Animations/EmoteAnimationSystem.cs @@ -0,0 +1,44 @@ +using Content.Server.Chat.Systems; +using Content.Shared.Ashfall.Animations; +using Content.Shared.Chat; + +namespace Content.Server.Ashfall.Animations; + +public sealed partial class EmoteAnimationSystem : SharedEmoteAnimationSystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnEmote); + } + + private void OnEmote(EntityUid uid, EmoteAnimationComponent component, ref EmoteEvent args) + { + if (args.Handled) + return; + + var emoteId = args.Emote.ID; + if (emoteId.Equals("Flip", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationFlip, component); + else if (emoteId.Equals("Jump", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationJump, component); + else if (emoteId.Equals("Turn", StringComparison.OrdinalIgnoreCase) || emoteId.Equals("Spin", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationTurn, component); + else if (emoteId.Equals("Tremble", StringComparison.OrdinalIgnoreCase) || emoteId.Equals("Shiver", StringComparison.OrdinalIgnoreCase) || emoteId.Equals("Shudder", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationTremble, component); + else if (emoteId.Equals("TailWag", StringComparison.OrdinalIgnoreCase) || emoteId.Equals("Wag", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationTailWag, component); + else if (emoteId.Equals("TailStop", StringComparison.OrdinalIgnoreCase)) + PlayAnimation(uid, AnimationTailStop, component); + } + + public void PlayAnimation(EntityUid uid, string animationId, EmoteAnimationComponent? comp = null) + { + if (!Resolve(uid, ref comp, false)) + return; + + comp.AnimationId = animationId; + comp.CurAnimationIndex++; + Dirty(uid, comp); + } +} diff --git a/Content.Server/Audio/AmbientSoundSystem.cs b/Content.Server/Audio/AmbientSoundSystem.cs index ea0409c725d..9b2287f01f0 100644 --- a/Content.Server/Audio/AmbientSoundSystem.cs +++ b/Content.Server/Audio/AmbientSoundSystem.cs @@ -8,13 +8,21 @@ namespace Content.Server.Audio; public sealed partial class AmbientSoundSystem : SharedAmbientSoundSystem { + [Dependency] private PowerReceiverSystem _powerReceiver = default!; + public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(HandleMapInit); SubscribeLocalEvent(HandlePowerChange); SubscribeLocalEvent(HandlePowerSupply); } + private void HandleMapInit(EntityUid uid, AmbientOnPoweredComponent component, MapInitEvent args) + { + SetAmbience(uid, _powerReceiver.IsPowered(uid)); + } + private void HandlePowerSupply(EntityUid uid, AmbientOnPoweredComponent component, ref PowerNetBatterySupplyEvent args) { SetAmbience(uid, args.Supply); diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs index 204194d40f7..ab9cc981d99 100644 --- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs @@ -99,7 +99,8 @@ public override void Initialize() SubscribeLocalEvent(OnActiveMicrowaveRemove); SubscribeLocalEvent(OnConstructionTemp); - SubscribeLocalEvent>(OnReactionAttempt); + SubscribeLocalEvent>(OnReactionAttemptRelay); + SubscribeLocalEvent(OnReactionAttempt); SubscribeLocalEvent(OnGetSecretRecipes); } @@ -144,9 +145,14 @@ private void OnConstructionTemp(Entity ent, ref OnC args.Result = HandleResult.False; } + private void OnReactionAttemptRelay(Entity ent, ref SolutionRelayEvent args) + { + OnReactionAttempt(ent, ref args.Event); + } + // Stop reagents from reacting if they are currently reserved for a microwave recipe. // For example Egg would cook into EggCooked, causing it to not being removed once we are done microwaving. - private void OnReactionAttempt(Entity ent, ref SolutionRelayEvent args) + private void OnReactionAttempt(Entity ent, ref ReactionAttemptEvent args) { if (!TryComp(ent.Comp.Microwave, out var activeMicrowaveComp)) return; @@ -158,9 +164,9 @@ private void OnReactionAttempt(Entity ent, ref Solu foreach (var reagent in recipeReagents) { - if (args.Event.Reaction.Reactants.ContainsKey(reagent)) + if (args.Reaction.Reactants.ContainsKey(reagent)) { - args.Event.Cancelled = true; + args.Cancelled = true; return; } } diff --git a/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs b/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs index 8acfe0ac76b..eb6022006fb 100644 --- a/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs +++ b/Content.Server/Light/EntitySystems/LitOnPoweredSystem.cs @@ -1,5 +1,4 @@ using Content.Server.Light.Components; -using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Shared.Power; @@ -8,28 +7,29 @@ namespace Content.Server.Light.EntitySystems public sealed partial class LitOnPoweredSystem : EntitySystem { [Dependency] private SharedPointLightSystem _lights = default!; + [Dependency] private PowerReceiverSystem _powerReceiver = default!; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnPowerSupply); } + private void OnMapInit(EntityUid uid, LitOnPoweredComponent component, MapInitEvent args) + { + _lights.SetEnabled(uid, _powerReceiver.IsPowered(uid)); + } + private void OnPowerChanged(EntityUid uid, LitOnPoweredComponent component, ref PowerChangedEvent args) { - if (_lights.TryGetLight(uid, out var light)) - { - _lights.SetEnabled(uid, args.Powered, light); - } + _lights.SetEnabled(uid, args.Powered); } private void OnPowerSupply(EntityUid uid, LitOnPoweredComponent component, ref PowerNetBatterySupplyEvent args) { - if (_lights.TryGetLight(uid, out var light)) - { - _lights.SetEnabled(uid, args.Supply, light); - } + _lights.SetEnabled(uid, args.Supply); } } } diff --git a/Content.Shared/Ashfall/Animations/EmoteAnimationComponent.cs b/Content.Shared/Ashfall/Animations/EmoteAnimationComponent.cs new file mode 100644 index 00000000000..6a57e659f81 --- /dev/null +++ b/Content.Shared/Ashfall/Animations/EmoteAnimationComponent.cs @@ -0,0 +1,16 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Ashfall.Animations; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)] +public sealed partial class EmoteAnimationComponent : Component +{ + [DataField, AutoNetworkedField] + public string AnimationId = string.Empty; + + [DataField, AutoNetworkedField] + public uint CurAnimationIndex; + + [ViewVariables] + public uint LastClientAnimationIndex; +} diff --git a/Content.Shared/Ashfall/Animations/SharedEmoteAnimationSystem.cs b/Content.Shared/Ashfall/Animations/SharedEmoteAnimationSystem.cs new file mode 100644 index 00000000000..4b148dbdc44 --- /dev/null +++ b/Content.Shared/Ashfall/Animations/SharedEmoteAnimationSystem.cs @@ -0,0 +1,13 @@ +namespace Content.Shared.Ashfall.Animations; + +public abstract class SharedEmoteAnimationSystem : EntitySystem +{ + public const string AnimationFlip = "Flip"; + public const string AnimationJump = "Jump"; + public const string AnimationTurn = "Turn"; + public const string AnimationSpin = "Spin"; + public const string AnimationTremble = "Tremble"; + public const string AnimationShiver = "Shiver"; + public const string AnimationTailWag = "TailWag"; + public const string AnimationTailStop = "TailStop"; +} diff --git a/Content.Shared/Ashfall/Overlays/Sandevistan/SandevistanVisionComponent.cs b/Content.Shared/Ashfall/Overlays/Sandevistan/SandevistanVisionComponent.cs new file mode 100644 index 00000000000..5c33b41c223 --- /dev/null +++ b/Content.Shared/Ashfall/Overlays/Sandevistan/SandevistanVisionComponent.cs @@ -0,0 +1,14 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Ashfall.Overlays.Sandevistan; + +/// +/// Attached to entities under adrenaline surge or Sandevistan reflex boost. +/// Causes the client to render chromatic distortion and fast-action motion trails. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SandevistanVisionComponent : Component +{ + [DataField, AutoNetworkedField] + public bool Enabled = true; +} diff --git a/Content.Shared/Ashfall/Overlays/ShockWave/ShockWaveComponent.cs b/Content.Shared/Ashfall/Overlays/ShockWave/ShockWaveComponent.cs new file mode 100644 index 00000000000..3fce4ae49fc --- /dev/null +++ b/Content.Shared/Ashfall/Overlays/ShockWave/ShockWaveComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Ashfall.Overlays.ShockWave; + +/// +/// Displays a propagating refractive screen shockwave ring. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ShockWaveComponent : Component +{ + [DataField, AutoNetworkedField] + public float WaveSpeed = 15.3f; + + [DataField, AutoNetworkedField] + public float WaveStrength = 1.0f; + + [DataField, AutoNetworkedField] + public float DownScale = 1.5f; + + [DataField, AutoNetworkedField] + public float FadeTime = 1.5f; + + [DataField, AutoNetworkedField] + public TimeSpan InitTime; +} diff --git a/Content.Shared/Ashfall/Weapons/Ranged/Tracer/TracerComponent.cs b/Content.Shared/Ashfall/Weapons/Ranged/Tracer/TracerComponent.cs new file mode 100644 index 00000000000..3893d26dd18 --- /dev/null +++ b/Content.Shared/Ashfall/Weapons/Ranged/Tracer/TracerComponent.cs @@ -0,0 +1,40 @@ +using System.Numerics; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Ashfall.Weapons.Ranged.Tracer; + +/// +/// Added to projectiles to give them tracer effects. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class TracerComponent : Component +{ + /// + /// How long the tracer effect should remain active. + /// + [DataField, AutoNetworkedField] + public float Lifetime = 5f; + + /// + /// The maximum length of the tracer trail in meters. + /// + [DataField, AutoNetworkedField] + public float Length = 2.5f; + + /// + /// Color of the tracer line effect. + /// + [DataField, AutoNetworkedField] + public Color Color = Color.FromHex("#FFE082"); + + [ViewVariables] + public TracerData? Data; +} + +[Serializable, NetSerializable, DataRecord] +public partial struct TracerData(List positionHistory, TimeSpan endTime) +{ + public List PositionHistory = positionHistory; + public TimeSpan EndTime = endTime; +} diff --git a/Content.Shared/Chat/MsgChatMessage.cs b/Content.Shared/Chat/MsgChatMessage.cs index 4666648624d..6f98e73b42a 100644 --- a/Content.Shared/Chat/MsgChatMessage.cs +++ b/Content.Shared/Chat/MsgChatMessage.cs @@ -37,11 +37,13 @@ public sealed class ChatMessage public Color? MessageColorOverride; public string? AudioPath; public float AudioVolume; + public string? SpeechStyleClass; + public string? LanguageIcon; [NonSerialized] public bool Read; - public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, int? senderKey, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0) + public ChatMessage(ChatChannel channel, string message, string wrappedMessage, NetEntity source, int? senderKey, bool hideChat = false, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, string? speechStyleClass = null, string? languageIcon = null) { Channel = channel; Message = message; @@ -52,6 +54,8 @@ public ChatMessage(ChatChannel channel, string message, string wrappedMessage, N MessageColorOverride = colorOverride; AudioPath = audioPath; AudioVolume = audioVolume; + SpeechStyleClass = speechStyleClass; + LanguageIcon = languageIcon; } public ChatMessage(ChatMessage copyFrom) @@ -65,6 +69,8 @@ public ChatMessage(ChatMessage copyFrom) MessageColorOverride = copyFrom.MessageColorOverride; AudioPath = copyFrom.AudioPath; AudioVolume = copyFrom.AudioVolume; + SpeechStyleClass = copyFrom.SpeechStyleClass; + LanguageIcon = copyFrom.LanguageIcon; Read = copyFrom.Read; } } diff --git a/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs b/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs index 651d34bbc03..4c8e469fbc5 100644 --- a/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs +++ b/Content.Shared/Light/EntitySystems/SharedPoweredLightSystem.cs @@ -297,7 +297,7 @@ protected void UpdateLight(EntityUid uid, { light.LastThunk = time; Dirty(uid, light); - _audio.PlayPredicted(light.TurnOnSound, uid, user: user, light.TurnOnSound.Params.AddVolume(-10f)); + _audio.PlayPredicted(light.TurnOnSound, uid, user: user, light.TurnOnSound.Params.AddVolume(-6f)); } } else diff --git a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs index 72bc619eeb6..523aefec7b9 100644 --- a/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs +++ b/Content.Shared/Movement/Systems/SharedMoverController.Relay.cs @@ -33,13 +33,6 @@ private void OnRelayCanMoveUpdated(Entity ent, ref Can protected virtual void OnInputMoverCanMoveUpdated(Entity ent, ref CanMoveUpdatedEvent args) { - if (!args.CanMove) - { - // Remove from active mover query when entity cannot move - RemCompDeferred(ent); - return; - } - UpdateMoverStatus((ent, ent.Comp)); } diff --git a/Content.Shared/Movement/Systems/SharedSpriteMovementSystem.cs b/Content.Shared/Movement/Systems/SharedSpriteMovementSystem.cs index eb4bbc1be63..e7dd91b8f6c 100644 --- a/Content.Shared/Movement/Systems/SharedSpriteMovementSystem.cs +++ b/Content.Shared/Movement/Systems/SharedSpriteMovementSystem.cs @@ -3,16 +3,10 @@ namespace Content.Shared.Movement.Systems; -public abstract class SharedSpriteMovementSystem : EntitySystem +public abstract partial class SharedSpriteMovementSystem : EntitySystem { - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnSpriteMoveInput); - } - - private void OnSpriteMoveInput(Entity ent, ref SpriteMoveEvent args) + [SubscribeLocalEvent] + protected virtual void OnSpriteMoveInput(Entity ent, ref SpriteMoveEvent args) { if (ent.Comp.IsMoving == args.IsMoving) return; diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs index ceac98f10b7..f74c7888b21 100644 --- a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Content.Shared.EntityEffects.Effects.Body; @@ -150,27 +150,20 @@ public bool CanConsume(EntityUid user, #region EdibleComponent + /// + /// Spawns trash for the edible entity, placing it in the same hand if held, otherwise spawning it nearby. + /// + /// Edible entity that will spawn trash. + /// Optional user that will attempt to pickup spawned trash. public void SpawnTrash(Entity entity, EntityUid? user = null) { - if (entity.Comp.Trash.Count == 0) - return; - - var position = _transform.GetMapCoordinates(entity); - var trashes = entity.Comp.Trash; - var pickup = user != null && _hands.IsHolding(user.Value, entity, out _); - - foreach (var trash in trashes) + var pickup = user is not null && _hands.TryDrop(user.Value, entity); + foreach (var trash in entity.Comp.Trash) { - var spawnedTrash = EntityManager.PredictedSpawn(trash, position); - - // If the user is holding the item - if (!pickup) - continue; + var spawnedTrash = PredictedSpawnNextToOrDrop(trash, entity); - // Put the trash in the user's hand - // I am 100% confident we don't need this check but rider gets made at me if it's not here. - if (user != null) - _hands.TryPickupAnyHand(user.Value, spawnedTrash); + if (pickup) + _hands.TryPickupAnyHand(user!.Value, spawnedTrash); } } diff --git a/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs b/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs index 51031c930e3..45f1ec63e0c 100644 --- a/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs @@ -1,4 +1,4 @@ -using Content.Shared.Containers.ItemSlots; +using Content.Shared.Containers.ItemSlots; using Robust.Shared.GameStates; using Robust.Shared.Serialization; @@ -29,31 +29,68 @@ public sealed partial class MMIComponent : Component /// The sprite state when the brain inserted has a mind. /// [DataField] - public string HasMindState = "mmi_alive"; + public string HasMindState = "mmi_on"; /// /// The sprite state when the brain inserted doesn't have a mind. /// [DataField] - public string NoMindState = "mmi_dead"; + public string NoMindState = "mmi_on"; /// /// The sprite state when there is no brain inserted. /// [DataField] public string NoBrainState = "mmi_off"; + + /// + /// The color of the layer when the brain inserted has a mind. + /// + [DataField] + public Color HasMindLightColor = Color.FromHex("#0094ff"); + + /// + /// The color of the layer when the brain inserted doesn't have a mind. + /// + [DataField] + public Color NoMindLightColor = Color.FromHex("#ff3033"); } +/// +/// AppearanceData keys for the MMI. +/// [Serializable, NetSerializable] public enum MMIVisuals : byte { + /// + /// bool: Whether or not there is a brain in the MMI. + /// BrainPresent, + + /// + /// bool: Whether or not there is an active mind (a player) in the MMI. + /// HasMind } +/// +/// Sprite map keys for MMI visuals. +/// [Serializable, NetSerializable] public enum MMIVisualLayers : byte { + /// + /// The layer of the brain. + /// Brain, - Base + + /// + /// The layer of the housing. + /// + Base, + + /// + /// The optional layer of an indicator light. + /// + Unlit, } diff --git a/Content.Shared/Vehicle/Systems/VehicleSystem.cs b/Content.Shared/Vehicle/Systems/VehicleSystem.cs index 7073277622c..076a28fe0ef 100644 --- a/Content.Shared/Vehicle/Systems/VehicleSystem.cs +++ b/Content.Shared/Vehicle/Systems/VehicleSystem.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Content.Shared.Access.Components; using Content.Shared.ActionBlocker; using Content.Shared.Damage; @@ -31,6 +31,7 @@ public sealed partial class VehicleSystem : EntitySystem [Dependency] private SharedContainerSystem _container = default!; [Dependency] private DamageableSystem _damageable = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; + [Dependency] private SharedEyeSystem _eye = default!; [Dependency] private EntityWhitelistSystem _entityWhitelist = default!; [Dependency] private SharedHandsSystem _hands = default!; [Dependency] private SharedMoverController _mover = default!; @@ -166,6 +167,7 @@ public bool TrySetOperator(Entity entity, EntityUid operatorUi Dirty(operatorUid, vehicleOperator); _mover.SetRelay(operatorUid, entity); + _eye.SetTarget(operatorUid, entity.Owner); var enterEvent = new OnVehicleEnteredEvent(entity, operatorUid); RaiseLocalEvent(operatorUid, ref enterEvent); @@ -190,9 +192,9 @@ public bool TryRemoveOperator(Entity entity) if (entity.Comp.Operator is not { } currentOperator) return false; - _operatorQuery.TryComp(currentOperator, out var currentOperatorComponent); + _eye.SetTarget(currentOperator, null); - if (currentOperatorComponent != null) + if (_operatorQuery.TryComp(currentOperator, out var currentOperatorComponent)) { var exitEvent = new OnVehicleExitedEvent(entity, currentOperator); RaiseLocalEvent(currentOperator, ref exitEvent); @@ -211,7 +213,7 @@ public bool TryRemoveOperator(Entity entity) var setEvent = new VehicleOperatorSetEvent(null, currentOperator); RaiseLocalEvent(entity, ref setEvent); - Dirty(entity); + DirtyFields(entity.Owner, entity.Comp, null, nameof(VehicleComponent.Operator)); return true; } @@ -254,6 +256,7 @@ public bool TryRemoveOperator(Entity operatorEntity) if (_vehicleQuery.TryComp(vehicleUid, out var vehicle)) return TryRemoveOperator((vehicleUid.Value, vehicle)); + _eye.SetTarget(operatorEntity.Owner, null); UnblockHands(vehicleUid.Value, operatorEntity.Owner); ClearOperatorRelays(operatorEntity.Owner, vehicleUid.Value); operatorEntity.Comp.Vehicle = null; diff --git a/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs b/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs index 3656fab2fb7..0b3c68dc6d5 100644 --- a/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs +++ b/Content.Shared/Weapons/Misc/SharedGrapplingGunSystem.cs @@ -333,7 +333,7 @@ joint is not DistanceJoint distance || if (grapplerBodyB.Mass < BaseWeightMass) massFactorB *= grapplerBodyB.Mass / BaseWeightMass; - if (sameGrid && physicalHook != _transform.GetGrid(joint.BodyAUid)) + if (!sameGrid || sameGrid && physicalHook != _transform.GetGrid(joint.BodyAUid)) _physics.ApplyLinearImpulse(grapplerUidA, -targetDirection * massFactorA * grappling.ReelForce * frameTime, grapplerOffsetA, body: grapplerBodyA); _physics.ApplyLinearImpulse(grapplerUidB, targetDirection * massFactorB * grappling.ReelForce * frameTime, grapplerOffsetB, body: grapplerBodyB); diff --git a/Resources/Audio/Machines/light_tube_on.ogg b/Resources/Audio/Machines/light_tube_on.ogg index deeffa7339897db4942c2bd3e758078d0ef86360..6f00a777c553113c3d1716ac98f36d375d8698a5 100644 GIT binary patch literal 11585 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 diff --git a/Resources/Locale/en-US/Ashfall/animations/emotes.ftl b/Resources/Locale/en-US/Ashfall/animations/emotes.ftl new file mode 100644 index 00000000000..27eeb31a56e --- /dev/null +++ b/Resources/Locale/en-US/Ashfall/animations/emotes.ftl @@ -0,0 +1,14 @@ +chat-emote-name-flip = Do a flip +chat-emote-msg-flip = does a flip! + +chat-emote-name-jump = Jump +chat-emote-msg-jump = jumps! + +chat-emote-name-spin = Spin +chat-emote-msg-spin = spins in place! + +chat-emote-name-tremble = Tremble +chat-emote-msg-tremble = trembles slightly. + +chat-emote-name-tail-wag = Wag tail +chat-emote-msg-tail-wag = wags { POSS-ADJ($entity) } tail. diff --git a/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl b/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl new file mode 100644 index 00000000000..c98e95c1ef7 --- /dev/null +++ b/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl @@ -0,0 +1,14 @@ +chat-emote-name-flip = Сделать сальто +chat-emote-msg-flip = делает сальто! + +chat-emote-name-jump = Подпрыгнуть +chat-emote-msg-jump = подпрыгивает! + +chat-emote-name-spin = Покружиться +chat-emote-msg-spin = кружится на месте! + +chat-emote-name-tremble = Дрожать +chat-emote-msg-tremble = мелко дрожит. + +chat-emote-name-tail-wag = Вилять хвостом +chat-emote-msg-tail-wag = виляет хвостом. diff --git a/Resources/Prototypes/Ashfall/Entities/Effects/shockwave.yml b/Resources/Prototypes/Ashfall/Entities/Effects/shockwave.yml new file mode 100644 index 00000000000..90e0e9ccc7f --- /dev/null +++ b/Resources/Prototypes/Ashfall/Entities/Effects/shockwave.yml @@ -0,0 +1,11 @@ +- type: entity + id: AshfallEffectShockWave + categories: [ HideSpawnMenu ] + components: + - type: TimedDespawn + lifetime: 2.0 + - type: ShockWave + - type: EffectVisuals + - type: Tag + tags: + - HideContextMenu diff --git a/Resources/Prototypes/Ashfall/Shaders/shaders.yml b/Resources/Prototypes/Ashfall/Shaders/shaders.yml index e42027eb5f9..ef73b85bc73 100644 --- a/Resources/Prototypes/Ashfall/Shaders/shaders.yml +++ b/Resources/Prototypes/Ashfall/Shaders/shaders.yml @@ -22,3 +22,8 @@ id: AshfallSuppression kind: source path: "/Textures/Ashfall/Shaders/suppression.swsl" + +- type: shader + id: AshfallSandevistanVision + kind: source + path: "/Textures/Ashfall/Shaders/sandevistan_vision.swsl" diff --git a/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml b/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml index 549658f780f..5b3feec91c0 100644 --- a/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml +++ b/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml @@ -43,3 +43,60 @@ - миу - мьяу - миау + +- type: emote + id: Flip + name: chat-emote-name-flip + category: Hands + chatMessages: ["chat-emote-msg-flip"] + chatTriggers: + - flip + - flips + - сальто + - кувырок + +- type: emote + id: Jump + name: chat-emote-name-jump + category: Hands + chatMessages: ["chat-emote-msg-jump"] + chatTriggers: + - jump + - jumps + - прыгает + - подпрыгивает + - прыжок + +- type: emote + id: Spin + name: chat-emote-name-spin + category: Hands + chatMessages: ["chat-emote-msg-spin"] + chatTriggers: + - spin + - spins + - кружится + - вертится + +- type: emote + id: Tremble + name: chat-emote-name-tremble + category: Hands + chatMessages: ["chat-emote-msg-tremble"] + chatTriggers: + - tremble + - shiver + - shudder + - дрожит + - трясется + +- type: emote + id: TailWag + name: chat-emote-name-tail-wag + category: Hands + chatMessages: ["chat-emote-msg-tail-wag"] + chatTriggers: + - wag + - wags + - виляет хвостом + - виляет diff --git a/Resources/Prototypes/Body/species_base.yml b/Resources/Prototypes/Body/species_base.yml index 0f35554ea40..39b5dbc1992 100644 --- a/Resources/Prototypes/Body/species_base.yml +++ b/Resources/Prototypes/Body/species_base.yml @@ -44,6 +44,7 @@ - type: Footprint - type: CPRTraining - type: OfferItem + - type: EmoteAnimation - type: Carriable - type: RangedDamageSound soundGroups: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index bebb412eed0..a1c8e0c9c92 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -340,8 +340,7 @@ trash: - TrashBananaPeel - type: ToolRefinable - refineResult: - - id: TrashBananaPeel + refineResult: [] # Spawns trash on refine - type: Solution solution: reagents: @@ -385,8 +384,7 @@ trash: - TrashMimanaPeel - type: ToolRefinable - refineResult: - - id: TrashMimanaPeel + refineResult: [] # Spawns trash on refine - type: Solution solution: reagents: @@ -1188,8 +1186,7 @@ trash: - FoodCornTrash - type: ToolRefinable - refineResult: - - id: FoodCornTrash #no idea why you'd ever do this, but it's just here for consistency + refineResult: [] # Spawns trash on refine - type: Solution solution: reagents: @@ -1865,8 +1862,7 @@ trash: - WeaponRevolverPython - type: ToolRefinable - refineResult: - - id: WeaponRevolverPython + refineResult: [] # Spawns trash on refine - type: Tag tags: - Fruit # It's in the name @@ -1903,8 +1899,7 @@ trash: - RevolverCapGun - type: ToolRefinable - refineResult: - - id: RevolverCapGun + refineResult: [] # Spawns trash on refine - type: Tag tags: - Fruit @@ -1925,8 +1920,7 @@ trash: - RevolverCapGunFake - type: ToolRefinable - refineResult: - - id: RevolverCapGunFake + refineResult: [] # Spawns trash on refine - type: entity name: rice bushel @@ -2335,8 +2329,7 @@ trash: - FoodBungoPit - type: ToolRefinable - refineResult: - - id: FoodBungoPit + refineResult: [] # Spawns trash on refine - type: Solution solution: reagents: @@ -2639,8 +2632,7 @@ trash: - TrashCherryPit - type: ToolRefinable - refineResult: - - id: TrashCherryPit + refineResult: [] # Spawns trash on refine - type: Solution solution: maxVol: 15 diff --git a/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml b/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml index 5ec8b87ae00..e50147a76c5 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/travel_camera.yml @@ -39,13 +39,15 @@ parent: BasePaper id: BasePhotograph name: photograph + components: + - type: Sprite + sprite: Objects/Misc/photograph.rsi - type: entity parent: BasePhotograph id: PhotographBlack components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: black @@ -54,7 +56,6 @@ id: PhotographRed components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: red @@ -63,7 +64,6 @@ id: PhotographBlue components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: blue @@ -72,7 +72,6 @@ id: PhotographGreen components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: green @@ -81,7 +80,6 @@ id: PhotographYellow components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: yellow @@ -90,7 +88,6 @@ id: PhotographPurple components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: purple @@ -99,7 +96,6 @@ id: PhotographRainbow components: - type: Sprite - sprite: Objects/Misc/photograph.rsi layers: - state: rainbow diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml index 69d84810c6b..0ff23c68209 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml @@ -12,6 +12,10 @@ visible: false - state: mmi_off map: ["enum.MMIVisualLayers.Base"] + - state: mmi_light + map: ["enum.MMIVisualLayers.Unlit"] + shader: unshaded + visible: false - type: Input context: human - type: MMI @@ -76,7 +80,8 @@ sprite: Objects/Specific/Robotics/mmi.rsi layers: - state: posibrain - map: ["base"] + - map: ["light"] + shader: unshaded - type: Input context: human - type: ToggleableGhostRole @@ -125,8 +130,8 @@ - type: GenericVisualizer visuals: enum.ToggleableGhostRoleVisuals.Status: - base: - Off: { state: posibrain } + light: + Off: { state: empty } # TODO: fix null states in SpriteComp.LayerSetData, use a null state Searching: { state: posibrain-searching } On: { state: posibrain-occupied } - type: GuideHelp diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml index 4025008f6ca..502ca212864 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml @@ -45,6 +45,9 @@ path: /Audio/Weapons/Guns/Hits/bullet_hit.ogg - type: TimedDespawn lifetime: 10 + - type: Tracer + color: "#FFE082" + length: 2.2 - type: entity parent: BaseBullet @@ -103,6 +106,9 @@ energy: 7.0 - type: IgniteOnCollide fireStacks: 0.25 + - type: Tracer + color: "#FF7043" + length: 3.0 - type: entity parent: BaseBullet diff --git a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml index c3af9de5c8e..8fa681f9f36 100644 --- a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml +++ b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml @@ -31,11 +31,10 @@ shader: unshaded - type: PointLight color: "#FFE4CE" # 5000K color temp - lightMask: TubeBloomLight # AshFall-Tweak energy: 0.8 radius: 10 softness: 1 - offset: "0, 0.45" +# offset: "0, 0.25" - type: RCDDeconstructable cost: 4 delay: 2 @@ -318,6 +317,7 @@ light_bulb: !type:ContainerSlot - type: PoweredLight bulb: Bulb + - type: SpookyPoweredLight - type: ApcPowerReceiver - type: ExtensionCableReceiver - type: DeviceNetwork diff --git a/Resources/Textures/Ashfall/Shaders/sandevistan_vision.swsl b/Resources/Textures/Ashfall/Shaders/sandevistan_vision.swsl new file mode 100644 index 00000000000..9a66752705d --- /dev/null +++ b/Resources/Textures/Ashfall/Shaders/sandevistan_vision.swsl @@ -0,0 +1,14 @@ +uniform sampler2D SCREEN_TEXTURE; + +void fragment() { + highp vec4 color = zTextureSpec(SCREEN_TEXTURE, UV); + + highp mat3 m = mat3( + vec3(0.150, 0.150, 0.150), + vec3(0.350, 1.000, 0.350), + vec3(0.150, 0.050, 0.150) + ); + highp vec3 result = color.rgb * m; + + COLOR = vec4(result, 1.0); +} diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/meta.json b/Resources/Textures/Objects/Misc/photograph.rsi/meta.json index a76a07d169e..f0b5176c40d 100644 --- a/Resources/Textures/Objects/Misc/photograph.rsi/meta.json +++ b/Resources/Textures/Objects/Misc/photograph.rsi/meta.json @@ -1,32 +1,40 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Made by ketufaispikinut (Github)", + "copyright": "Made by ketufaispikinut (Github). Inhands by whatston3 (Github)", "size": { "x": 32, "y": 32 }, "states": [ { - "name": "black" + "name": "black" }, { - "name": "red" + "name": "red" }, { - "name": "blue" + "name": "blue" }, { - "name": "green" + "name": "green" }, { - "name": "yellow" + "name": "yellow" }, { - "name": "purple" + "name": "purple" }, { - "name": "rainbow" + "name": "rainbow" + }, + { + "name": "paper-inhand-left", + "directions": 4 + }, + { + "name": "paper-inhand-right", + "directions": 4 } ] } diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-left.png b/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..2dc05baac70dacc6baf989e7c5dc2d8984106ab4 GIT binary patch literal 256 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|c6quuhE&XX zdut=_VFMnQK+O|tW;rAWaAz3%Ee>JSIjqSf-)r$}{o-zIj#<1@g@8I37$SVbT4vm| z_!XBu_1WrA+vcrWaPN1>^q`Q7?LW2pCtQ){%yu}k!!4qYDaHE0-Q`y4Zy9#%wlUU} zz7RD#%UJr<{e=1aU~_;(gZ{~U-|)Sn0Z&i+B}a$v7qytIzn|~^Q=Na?{#5?am--MK xoSv5a{_>-sr}Iy8M>bvD{^!MZ4xmQw3P$0tQJoUnC43;WJYD@<);T3K0RV0iVEF(5 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-right.png b/Resources/Textures/Objects/Misc/photograph.rsi/paper-inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..3ec584c65e48d4324d5632c68b37d27f1ce2030f GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|PI|gHhE&XX zd+RjsVFdx#z>Xko(Ezr~408i{BU2Mk3(h#tGO1ccKPT~v$)O5E31*-c28MsVy1X2U zPgXy__xsWA`yb9l&wd|itZ}SjNBOy}OM?ZH_e_0nXnK~di#4KjW&a_r6_QhetYWlx z&tAWSvrS-A$Cs3#t-)bnlYqp6?3?@MhehvOWn1T}yudAb-%f_K&Be^~bFRwHySOS} z@4)&w(l36oq*-s@wN^I%^E%1mbM3tWE^TXh*6w%soU(ha9MDk*PIA9baVdJM<2N5< NuBWS?%Q~loCIH3-X+Hn} literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/empty.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..7244b37f5c785d78eee7767f9a4aa6bef3263b07 GIT binary patch literal 96 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}Ql2i3ArY-_ n4>B?Wc})uc*XMaSfSB*u1QZw;-5D2j067evu6{1-oD!MP)Px%O-V#SR9J=Wm9cBvKorJ5mCik~wdztbB;c011yw_72M;1(vKUOeg@g*P9YSaz z(5Y@6aEC%@zz7_?H8C|^)CdnMl-6+7l41uE$YgLuw(~!59g-80e3G5a`hh?vefQpX z@99oJMn*w%P)fj zk2mfC`xmP%-YO_z?R;epj@g6I1K{10t|AGW4k3gSLb$PL@Wao&i51ynm%o1To+p)D z=*3(SPo|w{0U?A}YgRD+{=)?TP@kf{Zt?oM#T~PU`N|yP6%bIL0s#7FgWy@UW0RY{Tu7Z8}oYhwwywh#~0EWZ!wD7>Rk8sb~2K6bZPtiXc z;O?_WpgzUU?!m-Lc6JXqA%tt14qR4Y&)G&UUr05OT7mlyp9c5C;W;`^1J#-ZRaNoz z+ph@|)tZH^O*1m@&%dA7H!%47JGg&x=Q#jiYtx)C7}m+gX(YCw*|Gruj4_2VrW_x+ zSY3Jp{k9Iqm?9F1WI`9e!m(sXE8uw)nx>=Kvaz;O!2oJSsIH-A05EUMg3hX*yI@#o9_S^1f8A;PkW~dA6|d0OIQxgp+sL z4a5y5R)8_4blMFLAgDx`di|ZW&vD(fH~_Za;|~A;002ovPDHLkV1kKoDTV+5 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-occupied.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-occupied.png index c4ea2b81775e604c05633e362323a535c9816503..2fdd54f8c9c51448ed153c9c26b5d25f0a96050d 100644 GIT binary patch delta 584 zcmew+_>g6SNGZx^prw85o$PJzX3_D(1Ysd$Cu_P=F!e_ksx~4ps~MCof?4 z;LmZ9T__^8!^z)p4Xo1&h-==q{ zJ9KCEPt1M4ebY-nro@+PPJT}0-%!8=b_P1$yxuk_r_blX98XIk=*Ki=z2~0yYSI?DPs>hc$<|rsuh&17^I%`Xw5(N|=XXWjlQ*37 zqSx`o1Rs-=`y1DG-Rs~}WYEe=P_E+0lSlCrgwwFFj>$9V*`go8%f5j`JYD@<);T3K F0RZeF5&r-H literal 2294 zcmai0doC7nC+{Phy8ikpP4Ca^_b1_GK-`82Kv)1|k*zexYT6^vF?&p2>UT>LD+`wx{Fj2zONuK> zZ3N@vxRFemGp^Y;q@%OPtW?Y^`|?U1WOh63ssCkn9Y554sjTvr&!pSZ-DsS;>g8%p z-J+BuNjHMer;F|_aiTXBqH6D(X_4Bb?W{BzLJeQ@8d!L+Uz5_@`;>*phivOOrw@P|)TXrO$P7~EY~E~tkZC6-qUU*^A? z7(5y4DF(v^6lfuH15u7O=sGo0SrvN1Q+aRd&v(Nb4Q{ng1=sn|x@=RW&d4yCLo zXn?X3YD{Oi7bfSF>{D;bHcsSY^pT3hFepA8MWF}zT6(Nrg!Xz&Uo}8Cg!&mIbGfFn zOsQUcO4#_FeP|jj(9mk<6Rl+dorr+u6-myBPTiRQfw1VVi^`+~I zpRDSVqa=vRAJ0b3F*<>8vF)j}3|@fz+aU$v5OcI*y9eH)ySI18y&hFLCcD5%ihmdu z`f4UCx_gOJ55qz`DKou(7v4`Y*j%DPQ$SRk{ZHCpUYPk(sZdbW@YqTFeYlsBshdVT zLcCHA6*oK?{3{3dds^>`2mb^wF&}LQQ^NCf2fssROP} zgJ2d`QxJu8SS)aU5~ht^AwT9pWpX_DfTzsT+u#F{w%&9E+n)`8p*a-suK{KPA6q3nKLfM z#j?Pg4~Be$o8uxG)Fcp{s+OFOm3MVEOE?#@X`n*X$VXC~o)p7kR~sh`%6K5mM^n~T zz<`L4huGU_<2ctWZ1Q#P;t;|23n@Mj8#(nUxpFsw9KkDfTFo%VZbY#_&&~u{6rYm0y^M<-6O)>IrX@bM6tWq zOgm=gb?tWg{SyQ8(5Ak&l_nB>ab|kl*Dhq393(^ycLgBOj{I?iS<($MnciK?j=eR$ zlDntAn~eDNnLP{Cwj?p=z@vF+xz^VJ_btf9IsNZ72m+vUbsp-}8-zfIn=!;){@yV^ zES!IU9|{z5A(v(b-oSDhmGCASdl(1Tp?V?Et&D07geR%W59gG|r{e*ew+^5TA$jHfR37y_LeSt+#^ zd?q)7HKPL$y&h_3#Bt+7UK+ZLOpW)2&z|`NMN`I$hB!3}pEaKJ!xnX6Th+R<5GO1J zqU(7x5-QzD-!lW~^h;zianziPx-)_n!quVMTb#fAsx!lkg->j@jB5 z!zd3$?hybo-Sp-hqe2r*dT61M_D}WzI?VMr0KFL0X}VBYksJQ*P`E3w*iQe8+QqnWigNa#xnzm5b zrzvzqL&QE0r8NYK1Qu*;_hW(fvx^@WlqCf*8*gBA@2#-L`2Ko2S#9k z9s%x;Fz=yFKVyFH`JGJp{~iAS68K$XUsooP;nr zw|AXp5tRdCd068-(L!qDhT`X+$B()Jr%6cDT5_ck+|Dk+A; zlAkLU^iHp`etTc7pq!|ExEzTxfw<7B7|;eNp-BEbn1kg00iQTAYNJJC7MH+_Hq*1N zi0!~0n&1&j1J9^`dzyIolSKGkLGdFTdxI60c)M)e;%%L(7GyMS+K zXvTvap#>dRu4r8OHk@CH`^NlLk?bGFagLm^^FZB)M;_euj|^aIecbBt5ua;+0iRNH A3jhEB diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-searching.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/posibrain-searching.png index 026a2bc7c52a2e5a7e2cd95022347bf7baf614cd..f0b62a41ba271f719558e430da69afe981e86256 100644 GIT binary patch literal 739 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 MaxBrightness) + finalColor *= MaxBrightness / peak; - // Add some noise for realism. - lowp float noise = zRandom(UV + sin(TIME / 10.0)).x * noise_amount; + highp float afterLum = dot(finalColor, lumaWeights); + highp vec3 cool = finalColor * coolTint; + highp float coolLum = max(dot(cool, lumaWeights), 0.0001); + cool *= afterLum / coolLum; + finalColor = mix(finalColor, cool, darkFactor * 0.25); - // Blend the generated noise into the image. - color.rgb += mix(color.rgb, color.rgb * noise, noise) * noise_multiplier; + // Add noise if enabled + if (noise_amount > 0.001) + { + lowp float noise = zRandom(UV + sin(TIME / 10.0)).x * noise_amount; + finalColor += mix(finalColor, finalColor * noise, noise) * noise_multiplier * 0.5; + } - COLOR = color; + COLOR = vec4(finalColor, texColor.a); } diff --git a/Resources/Textures/Structures/Machines/biofabricator.rsi/meta.json b/Resources/Textures/Structures/Machines/biofabricator.rsi/meta.json index b6aa4d49906..44d01160da2 100644 --- a/Resources/Textures/Structures/Machines/biofabricator.rsi/meta.json +++ b/Resources/Textures/Structures/Machines/biofabricator.rsi/meta.json @@ -1,56 +1,57 @@ { - "version": 1, - "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/338c826d1c3172eecd86d8430608669543dce0cc", - "size": { - "x": 32, - "y": 32 - }, - "states": [ - { - "name": "icon" + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/338c826d1c3172eecd86d8430608669543dce0cc", + "size": { + "x": 32, + "y": 32 }, - { - "name": "panel" - }, - { - "name": "unlit", - "delays": [ - [ - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1 - ] - ] - }, - { - "name": "building", - "delays": [ - [ - 0.1, - 0.1, - 0.1, - 0.1 - ] - ] - }, - { - "name": "inserting", - "delays": [ - [ - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1, - 0.1 - ] - ] - } - ] + "states": [ + { + "name": "icon" + }, + { + "name": "panel" + }, + { + "name": "unlit", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "building", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "inserting", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + } + ] } diff --git a/Resources/Textures/Structures/Machines/biofabricator.rsi/unlit.png b/Resources/Textures/Structures/Machines/biofabricator.rsi/unlit.png index 10cb4358779749fd22d6c0c3a00ddacece9f07a6..4ac908b9efc18e679a6ad0283e6e084646033daa 100644 GIT binary patch delta 401 zcmbQryq$T1K|N!kr;B4q#hkZy*XA8I5NUh3IrG4U>B12b84(f@5fKs=H@rD`On7*{ zEahRFRk`5*r7bFx>X*H#y|Hpp-SVQ)5jW4>e0m zL)WlxUVY?^*2cPjH-FeWMtK+8?7tG3sr#)&EZQR8MbGxhId+JBF!I0TZ$|0U;-T_Q zuZ}PHWmxgPc@@86^pD_u-|fBx&-@ep?O(Q?{mZ%olRmx)*;0D+)7H{UJb4#4pX5t; zS@UOF{oU;?brVzU9oW>redEcY zvHO{)v)AoS>DT-Jmr?tueZ%Yf*Y)T2-%t7{<@t?yi|+4-y;@THy*B-Am~N%M)>WSC hUOdde9|z5IQkJwoJ!N`}fq{#G!PC{xWt~$(69BNds?@iIb;J_E4GRej`~yU47!@YLS%Gn9OZ_?5jOyP5oO}e*f#G zuKHb1^q#$D0vgL;(BJ-Xmha}u_s*N8+?#K-?zo+H{^!~HOP}PAXWPY!-+CL(@p^eN z@8ig&d%V9fefq3_fHz})yZC;_*y>t2SFr`T%UY*@F?_Qxs@OwX;i_Gq`M(D|C)SkN zfy@Sjg6m(}(!%zdZ;9Oh?d-dPRT9bbk3C)-xnxhQ`0umVcV<0V{(aw>hf#GG-!m`U zl0JRHt;gTnAE&R(ig?@q@M&>W*+$kGx8&oUelg}9oWct-`?@;_2$=vd$@? F2>=(nrpy2U From 94293ed02fb21802a3d36e7f48c02d25edaaa011 Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Sun, 20 Sep 2026 00:42:59 +0200 Subject: [PATCH 02/11] Remove obsolete mmi_alive and mmi_dead png files from mmi.rsi --- .../Specific/Robotics/mmi.rsi/mmi_alive.png | Bin 669 -> 0 bytes .../Specific/Robotics/mmi.rsi/mmi_dead.png | Bin 672 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_alive.png delete mode 100644 Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_dead.png diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_alive.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_alive.png deleted file mode 100644 index 1869540a853910ec29f0c052be4f7f92a69acb21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 669 zcmV;O0%HA%P)Px%S4l)cR9J=WmA`A-KorNnDxG^|Yu%+}h`}v&3#x|D4jx3nbTOD_3keloJ0zij zlumW)fIAdI14iKBEs3e=qDFXdp)`i8mJ~aXKqiAKvYr2d*C9D$*^=yJ)&~T-)A!!{ z-n$h>dk)G5^PwHg#TVxyUP@QeGVf9-i_0z<@Jx%h3my3f0 zPuCv+`XLxW`dmHL1L6l5MmxTbRLU1ti?#0D#VUchFZXnFulw zxdf?n1}Ddt2KGK3;`5jH0D$IcQ^9`lnB~{&)Tmbg0KMKtRCr+0Cfv2Rz&#G`adghR zxc~eKxW}=*b1*iO?VST+jFGBpz*QA??JcCznMebX6?ph~e(>JwU7%rCP%N29Bog@c z{r8xOV#&njhT$9c=ijfJ8|eN$8oWQd_W}U0xnYbM9M%Q3qX=DsYRv)w2q6?9gmQA+ z!t%mfXm>RTArzTNCX>3+XE;_6ffaBZ4pr4qtyx%E%90R5xjN63N@u`5PPu%+H>Odq zkW46Hj8U~_!F63Y4i7XCc#Ifhl*<>Osu~iB1Xh-^zVEqw0cU3&U*Fu^Bgow^8BX4) zS1@WY)IgAlVanwXI9l|Vb$&04wY7J?zDdauwF!j~N~2yO0FXy(60Ym|+LMwassfVH z+pAbI;W#{S2~<_{of0{bsQB0ZQH0&;(ujA00000NkvXXu0mjf Do?A3L diff --git a/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_dead.png b/Resources/Textures/Objects/Specific/Robotics/mmi.rsi/mmi_dead.png deleted file mode 100644 index 8baa79fa7a9aff2ca605277f721bc721dd8db830..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 672 zcmV;R0$=@!P)Px%T1iAfR9J=WmCtJvK@`WoD_(WzMRdJL4-G^y3TqDjIOMPtf|o$hTWN90)k9k> zR6H3?As!0FLRd)2%{CaktX*F|&{JStdYkwMJP+-JY_i#;J?{qsJG0+; z?>leiZGf1Vn3$LgVs#z5%)3!snM9-~bkY+#SzU*0qcu`z8!cE}hopXz*jFb>KKuM- zFTum*JHYnYZnM`jO22lxFa_IaLFxfe^QftWgpGzVMvO7i6FTjFZVm0o>N@oL#RnQy za;cYVWxO!$&=D}ks8}+4@{$ogZmuq zlLOp-`T*SL*x1||+R4V|4l%|^)6&3s6}Id(BvYA41CbTDd;f9oxzjmC!>*uMGNGy} zzJA*qGEppwf?Jd3gf|e}46zAKiKe09aiyh79)W!p2dIJc4S?0ssgh6d{Ci zxbI+o_6^cE(-1-^GLcLsb%VeDSYd=#z;!t^Esbi;!opmZgb>PkKT|T50rxrO@`b>f zM!iBZp@cC;)tUv*^WeHX)Iew(F~%sDFF@1MP*oKRbJ@WAT)u$g<91+fX67E`?U(c? zZ`3OoG&s^gn2CPM<@eb87i_U7*KS~G>1|+cTyjKRLLr3Gs8 Date: Sun, 20 Sep 2026 01:16:00 +0200 Subject: [PATCH 03/11] Fix duplicate subscription on ExplosionVisualsComponent in ShockWaveSystem --- .../Overlays/ShockWave/ShockWaveSystem.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs index 68f96b47a1b..423e0b93c46 100644 --- a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs +++ b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs @@ -1,8 +1,9 @@ +using Content.Client.Explosion; using Content.Shared.Ashfall.Overlays.ShockWave; using Content.Shared.Explosion; using Content.Shared.Explosion.Components; using Robust.Client.Graphics; -using Robust.Shared.GameStates; +using Robust.Shared.Map; using Robust.Shared.Timing; namespace Content.Client.Ashfall.Overlays.ShockWave; @@ -25,7 +26,8 @@ public override void Initialize() SubscribeLocalEvent(OnShockWaveStartup); SubscribeLocalEvent(OnShockWaveRemoved); - SubscribeLocalEvent(OnExplosionState); + SubscribeLocalEvent(OnExplosionTexturesStartup); + SubscribeLocalEvent(OnExplosionTexturesRemove); } public override void Shutdown() @@ -36,17 +38,21 @@ public override void Shutdown() base.Shutdown(); } - private void OnExplosionState(EntityUid uid, ExplosionVisualsComponent comp, ref ComponentHandleState args) + private void OnExplosionTexturesStartup(EntityUid uid, ExplosionVisualsTexturesComponent comp, ComponentStartup args) { - if (args.Current is not ExplosionVisualsState state) - return; - - if (state.Epicenter != Robust.Shared.Map.MapCoordinates.Nullspace && _spawnedForExplosion.Add(uid)) + if (TryComp(uid, out var visuals) && + visuals.Epicenter != MapCoordinates.Nullspace && + _spawnedForExplosion.Add(uid)) { - Spawn("AshfallEffectShockWave", state.Epicenter); + Spawn("AshfallEffectShockWave", visuals.Epicenter); } } + private void OnExplosionTexturesRemove(EntityUid uid, ExplosionVisualsTexturesComponent comp, ComponentRemove args) + { + _spawnedForExplosion.Remove(uid); + } + private void OnShockWaveStartup(Entity ent, ref ComponentStartup args) { if (!_registered.Add(ent.Owner)) From 07064c094aa1658348d511e064c171895dbeeaf5 Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Sun, 20 Sep 2026 01:41:54 +0200 Subject: [PATCH 04/11] Address review feedback on tracers, night vision shader, speech bubbles, and tail emotes --- .../Weapons/Ranged/Tracer/TracerSystem.cs | 37 +++++++++++-------- Content.Client/Chat/UI/SpeechBubble.cs | 26 +++++++------ .../Sheetlets/Hud/TooltipSheetlet.cs | 12 ++++++ Content.Client/Stylesheets/StyleNano.cs | 30 +++++++++++++++ .../en-US/Ashfall/animations/emotes.ftl | 3 ++ .../ru-RU/Ashfall/animations/emotes.ftl | 3 ++ .../Ashfall/Voice/speech_emotes.yml | 12 ++++++ Resources/Textures/Shaders/nightvision.swsl | 2 +- 8 files changed, 96 insertions(+), 29 deletions(-) diff --git a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs index f80d773c3ca..b368a0c3b78 100644 --- a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs +++ b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs @@ -32,7 +32,7 @@ public override void Shutdown() private void OnTracerStart(Entity ent, ref ComponentStartup args) { var xform = Transform(ent); - var pos = xform.Coordinates.Position; + var pos = _transform.GetWorldPosition(xform); ent.Comp.Data = new TracerData( new List { pos }, @@ -49,10 +49,12 @@ public override void Update(float frameTime) while (query.MoveNext(out var uid, out var tracer, out var xform)) { + var currentPos = _transform.GetWorldPosition(xform); + if (tracer.Data == null) { tracer.Data = new TracerData( - new List { xform.Coordinates.Position }, + new List { currentPos }, curTime + TimeSpan.FromSeconds(tracer.Lifetime) ); } @@ -64,7 +66,6 @@ public override void Update(float frameTime) continue; } - var currentPos = xform.Coordinates.Position; data.PositionHistory.Add(currentPos); while (data.PositionHistory.Count > 2 && @@ -72,6 +73,22 @@ public override void Update(float frameTime) { data.PositionHistory.RemoveAt(0); } + + if (data.PositionHistory.Count >= 2) + { + var trailLen = GetTrailLength(data.PositionHistory); + if (trailLen > tracer.Length) + { + var excess = trailLen - tracer.Length; + var seg = data.PositionHistory[1] - data.PositionHistory[0]; + var segLen = seg.Length(); + if (segLen > 0.0001f) + { + var t = MathF.Min(excess / segLen, 1f); + data.PositionHistory[0] = Vector2.Lerp(data.PositionHistory[0], data.PositionHistory[1], t); + } + } + } } } @@ -99,23 +116,11 @@ public void Draw(DrawingHandleWorld handle, MapId currentMap) if (positions.Count < 2) continue; - var parentPos = Vector2.Zero; - var parentRot = Angle.Zero; - - if (xform.ParentUid.IsValid()) - { - var parent = Transform(xform.ParentUid); - parentPos = _transform.GetWorldPosition(parent); - parentRot = _transform.GetWorldRotation(parent); - } - handle.SetTransform(Matrix3x2.Identity); for (var i = 1; i < positions.Count; i++) { - var start = parentPos + parentRot.RotateVec(positions[i - 1]); - var end = parentPos + parentRot.RotateVec(positions[i]); - handle.DrawLine(start, end, tracer.Color); + handle.DrawLine(positions[i - 1], positions[i], tracer.Color); } } } diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index 1396b128609..84f0770c8dc 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -327,9 +327,19 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl if (!ConfigManager.GetCVar(CCVars.ChatEnableFancyBubbles)) { - var container = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal }; + var label = new RichTextLabel + { + MaxWidth = SpeechMaxWidth, + StyleClasses = { "bubbleContent" }, + OutlineColorOverride = TextOutline.Default.Color, + }; + + label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); + + Control content = label; if (TryGetLanguageIcon(message, out var iconTexture)) { + var container = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal }; var textureRect = new TextureRect { Texture = iconTexture, @@ -338,22 +348,14 @@ protected override Control BuildBubble(ChatMessage message, string speechStyleCl Margin = new Thickness(0, 0, 4, 0) }; container.AddChild(textureRect); + container.AddChild(label); + content = container; } - var label = new RichTextLabel - { - MaxWidth = SpeechMaxWidth, - StyleClasses = { "bubbleContent" }, - OutlineColorOverride = TextOutline.Default.Color, - }; - - label.SetMessage(ExtractAndFormatSpeechSubstring(message, "BubbleContent", fontColor)); - container.AddChild(label); - var unfanciedPanel = new PanelContainer { StyleClasses = { "speechBox", speechStyleClass }, - Children = { container }, + Children = { content }, ModulateSelfOverride = Color.White.WithAlpha(ConfigManager.GetCVar(CCVars.SpeechBubbleBackgroundOpacity)), }; return unfanciedPanel; diff --git a/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs b/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs index 0cfa1f3336b..1d670b09f02 100644 --- a/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs +++ b/Content.Client/Stylesheets/Sheetlets/Hud/TooltipSheetlet.cs @@ -55,6 +55,10 @@ public override StyleRule[] GetRules(T sheet, object config) .Class("speechBox", "whisperBox") .ParentOf(E().Class("bubbleContent")) .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(12, FontKind.Italic)), + E() + .Class("speechBox", "whisperBox") + .ParentOf(E().ParentOf(E().Class("bubbleContent"))) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(12, FontKind.Italic)), E() .Class("speechBox", "emoteBox") .ParentOf(E().Class("bubbleContent")) @@ -66,6 +70,10 @@ public override StyleRule[] GetRules(T sheet, object config) .Class("speechBox", "commanderSpeech") .ParentOf(E().Class("bubbleContent")) .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(16, FontKind.Bold)), + E() + .Class("speechBox", "commanderSpeech") + .ParentOf(E().ParentOf(E().Class("bubbleContent"))) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(16, FontKind.Bold)), E() .Class("speechBox", "megaphoneSpeech") .Panel(tooltipBox), @@ -73,6 +81,10 @@ public override StyleRule[] GetRules(T sheet, object config) .Class("speechBox", "megaphoneSpeech") .ParentOf(E().Class("bubbleContent")) .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(20, FontKind.Bold)), + E() + .Class("speechBox", "megaphoneSpeech") + .ParentOf(E().ParentOf(E().Class("bubbleContent"))) + .Prop(Label.StylePropertyFont, sheet.BaseFont.GetFont(20, FontKind.Bold)), ]; } } diff --git a/Content.Client/Stylesheets/StyleNano.cs b/Content.Client/Stylesheets/StyleNano.cs index aef7606aff0..a6246425861 100644 --- a/Content.Client/Stylesheets/StyleNano.cs +++ b/Content.Client/Stylesheets/StyleNano.cs @@ -948,6 +948,16 @@ public StyleNano(IResourceCache resCache) : base(resCache) new StyleProperty("font", notoSansItalic12), }), + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "whisperBox"}, null, null), + new SelectorChild( + new SelectorElement(typeof(BoxContainer), null, null, null), + new SelectorElement(typeof(RichTextLabel), new[] {"bubbleContent"}, null, null))), + new[] + { + new StyleProperty("font", notoSansItalic12), + }), + new StyleRule(new SelectorChild( new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "emoteBox"}, null, null), new SelectorElement(typeof(RichTextLabel), null, null, null)), @@ -964,6 +974,16 @@ public StyleNano(IResourceCache resCache) : base(resCache) new StyleProperty("font", notoSansBold16), }), + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] { "speechBox", "commanderSpeech" }, null, null), + new SelectorChild( + new SelectorElement(typeof(BoxContainer), null, null, null), + new SelectorElement(typeof(RichTextLabel), new[] { "bubbleContent" }, null, null))), + new[] + { + new StyleProperty("font", notoSansBold16), + }), + new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "commanderSpeech"}, null, null), new[] { new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox) @@ -977,6 +997,16 @@ public StyleNano(IResourceCache resCache) : base(resCache) new StyleProperty("font", notoSansBold20), }), + new StyleRule(new SelectorChild( + new SelectorElement(typeof(PanelContainer), new[] { "speechBox", "megaphoneSpeech" }, null, null), + new SelectorChild( + new SelectorElement(typeof(BoxContainer), null, null, null), + new SelectorElement(typeof(RichTextLabel), new[] { "bubbleContent" }, null, null))), + new[] + { + new StyleProperty("font", notoSansBold20), + }), + new StyleRule(new SelectorElement(typeof(PanelContainer), new[] {"speechBox", "megaphoneSpeech"}, null, null), new[] { new StyleProperty(PanelContainer.StylePropertyPanel, tooltipBox) diff --git a/Resources/Locale/en-US/Ashfall/animations/emotes.ftl b/Resources/Locale/en-US/Ashfall/animations/emotes.ftl index 27eeb31a56e..cdd0185a3ac 100644 --- a/Resources/Locale/en-US/Ashfall/animations/emotes.ftl +++ b/Resources/Locale/en-US/Ashfall/animations/emotes.ftl @@ -12,3 +12,6 @@ chat-emote-msg-tremble = trembles slightly. chat-emote-name-tail-wag = Wag tail chat-emote-msg-tail-wag = wags { POSS-ADJ($entity) } tail. + +chat-emote-name-tail-stop = Stop wagging tail +chat-emote-msg-tail-stop = stops wagging { POSS-ADJ($entity) } tail. diff --git a/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl b/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl index c98e95c1ef7..22f0a2cab89 100644 --- a/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl +++ b/Resources/Locale/ru-RU/Ashfall/animations/emotes.ftl @@ -12,3 +12,6 @@ chat-emote-msg-tremble = мелко дрожит. chat-emote-name-tail-wag = Вилять хвостом chat-emote-msg-tail-wag = виляет хвостом. + +chat-emote-name-tail-stop = Перестать вилять хвостом +chat-emote-msg-tail-stop = перестает вилять хвостом. diff --git a/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml b/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml index 5b3feec91c0..417d7e45116 100644 --- a/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml +++ b/Resources/Prototypes/Ashfall/Voice/speech_emotes.yml @@ -100,3 +100,15 @@ - wags - виляет хвостом - виляет + +- type: emote + id: TailStop + name: chat-emote-name-tail-stop + category: Hands + chatMessages: ["chat-emote-msg-tail-stop"] + chatTriggers: + - stop wag + - stop wagging + - stopwag + - перестает вилять хвостом + - прекращает вилять хвостом diff --git a/Resources/Textures/Shaders/nightvision.swsl b/Resources/Textures/Shaders/nightvision.swsl index 3ebccbc3be8..7bbd05571af 100644 --- a/Resources/Textures/Shaders/nightvision.swsl +++ b/Resources/Textures/Shaders/nightvision.swsl @@ -47,7 +47,7 @@ void fragment() if (noise_amount > 0.001) { lowp float noise = zRandom(UV + sin(TIME / 10.0)).x * noise_amount; - finalColor += mix(finalColor, finalColor * noise, noise) * noise_multiplier * 0.5; + finalColor += finalColor * noise * noise_multiplier * 0.5; } COLOR = vec4(finalColor, texColor.a); From 5ab5133722632088ca2c1bf75fa4d15b489b46b2 Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Sun, 20 Sep 2026 02:08:36 +0200 Subject: [PATCH 05/11] Remove Pacifist trait from character generation pool and quirks --- .../Ashfall/CharacterGen/AshfallCharacterGenerator.cs | 1 - Resources/Prototypes/Traits/quirks.yml | 7 ------- 2 files changed, 8 deletions(-) diff --git a/Content.Shared/Ashfall/CharacterGen/AshfallCharacterGenerator.cs b/Content.Shared/Ashfall/CharacterGen/AshfallCharacterGenerator.cs index 01ae9152caa..97f31beccaa 100644 --- a/Content.Shared/Ashfall/CharacterGen/AshfallCharacterGenerator.cs +++ b/Content.Shared/Ashfall/CharacterGen/AshfallCharacterGenerator.cs @@ -159,7 +159,6 @@ public HumanoidCharacterProfile BuildProfile( "AshfallTraitServiceServiceability", "AshfallTraitAgroGreenThumb", "AshfallTraitBureaucraticPatience", - "Pacifist", "LightweightDrunk", "Snoring" }; diff --git a/Resources/Prototypes/Traits/quirks.yml b/Resources/Prototypes/Traits/quirks.yml index f51264e6bce..56fad4425f3 100644 --- a/Resources/Prototypes/Traits/quirks.yml +++ b/Resources/Prototypes/Traits/quirks.yml @@ -1,12 +1,5 @@ # If you add a new trait, make sure to add the corresponding component to the whitelist in \Resources\Prototypes\Entities\Mobs\Player\clone.yml so it gets copied to clones correctly! -- type: trait - id: Pacifist - name: trait-pacifist-name - description: trait-pacifist-desc - category: Quirks - components: - - type: Pacified - type: trait id: LightweightDrunk From 7d18695fbf150dc38d2fa977c08500b317191b16 Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Sun, 20 Sep 2026 02:24:35 +0200 Subject: [PATCH 06/11] Fix shockwave spawning on state apply and set MMI NoMindState default to mmi_off --- .../Overlays/ShockWave/ShockWaveSystem.cs | 18 ++++++++++++++---- .../Explosion/ExplosionOverlaySystem.cs | 9 +++++++++ .../Silicons/Borgs/Components/MMIComponent.cs | 2 +- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs index 423e0b93c46..d7b779a93da 100644 --- a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs +++ b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs @@ -26,6 +26,7 @@ public override void Initialize() SubscribeLocalEvent(OnShockWaveStartup); SubscribeLocalEvent(OnShockWaveRemoved); + SubscribeLocalEvent(OnExplosionStateApplied); SubscribeLocalEvent(OnExplosionTexturesStartup); SubscribeLocalEvent(OnExplosionTexturesRemove); } @@ -38,16 +39,25 @@ public override void Shutdown() base.Shutdown(); } - private void OnExplosionTexturesStartup(EntityUid uid, ExplosionVisualsTexturesComponent comp, ComponentStartup args) + private void TrySpawnShockWave(EntityUid uid, ExplosionVisualsComponent visuals) { - if (TryComp(uid, out var visuals) && - visuals.Epicenter != MapCoordinates.Nullspace && - _spawnedForExplosion.Add(uid)) + if (visuals.Epicenter != MapCoordinates.Nullspace && _spawnedForExplosion.Add(uid)) { Spawn("AshfallEffectShockWave", visuals.Epicenter); } } + private void OnExplosionStateApplied(EntityUid uid, ExplosionVisualsComponent comp, ref ExplosionVisualsStateAppliedEvent args) + { + TrySpawnShockWave(uid, comp); + } + + private void OnExplosionTexturesStartup(EntityUid uid, ExplosionVisualsTexturesComponent comp, ComponentStartup args) + { + if (TryComp(uid, out var visuals)) + TrySpawnShockWave(uid, visuals); + } + private void OnExplosionTexturesRemove(EntityUid uid, ExplosionVisualsTexturesComponent comp, ComponentRemove args) { _spawnedForExplosion.Remove(uid); diff --git a/Content.Client/Explosion/ExplosionOverlaySystem.cs b/Content.Client/Explosion/ExplosionOverlaySystem.cs index 5cde7b5f4ef..1aba551359f 100644 --- a/Content.Client/Explosion/ExplosionOverlaySystem.cs +++ b/Content.Client/Explosion/ExplosionOverlaySystem.cs @@ -47,6 +47,9 @@ private void OnExplosionHandleState(EntityUid uid, ExplosionVisualsComponent com component.ExplosionType = state.ExplosionType; component.SpaceMatrix = state.SpaceMatrix; component.SpaceTileSize = state.SpaceTileSize; + + var ev = new ExplosionVisualsStateAppliedEvent(component); + RaiseLocalEvent(uid, ref ev); } private void OnCompRemove(EntityUid uid, ExplosionVisualsComponent component, ComponentRemove args) @@ -97,3 +100,9 @@ public override void Shutdown() _overlayMan.RemoveOverlay(); } } + +/// +/// Event raised on an entity with ExplosionVisualsComponent after state has been handled and applied. +/// +[ByRefEvent] +public readonly record struct ExplosionVisualsStateAppliedEvent(ExplosionVisualsComponent Component); diff --git a/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs b/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs index 45f1ec63e0c..cf84f7aa339 100644 --- a/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs +++ b/Content.Shared/Silicons/Borgs/Components/MMIComponent.cs @@ -35,7 +35,7 @@ public sealed partial class MMIComponent : Component /// The sprite state when the brain inserted doesn't have a mind. /// [DataField] - public string NoMindState = "mmi_on"; + public string NoMindState = "mmi_off"; /// /// The sprite state when there is no brain inserted. From e96dfe1715454acef56afc5e952b69b5ba176cb5 Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Sun, 20 Sep 2026 23:07:38 +0200 Subject: [PATCH 07/11] Fix torso gibbing limb detachment and polish combat, visual overlays and chat --- .../Animations/EmoteAnimationSystem.cs | 66 +- .../Ashfall/Chat/RunechatSpeechBubble.cs | 1158 +++++++++++++++++ .../Sandevistan/SandevistanVisionOverlay.cs | 8 +- .../Sandevistan/SandevistanVisionSystem.cs | 23 +- .../Overlays/ShockWave/ShockWaveOverlay.cs | 18 +- .../Overlays/ShockWave/ShockWaveSystem.cs | 17 +- .../Weapons/Ranged/Tracer/TracerOverlay.cs | 3 +- .../Weapons/Ranged/Tracer/TracerSystem.cs | 171 ++- Content.Client/Chat/UI/SpeechBubble.cs | 31 +- .../Systems/ClientSpriteMovementSystem.cs | 2 +- .../Prediction/PredictedProjectileSystem.cs | 40 +- .../Tests/Ashfall/TraumaMedicalTests.cs | 56 + .../Combat/Concussion/ConcussionSystem.cs | 56 +- .../Projectiles/LagCompProjectileSystem.cs | 8 +- .../Ashfall/Audio/SharedDeafnessSystem.cs | 18 +- .../Ashfall/Chat/AshfallRunechatStyles.cs | 17 + .../ConcussionThresholdComponent.cs | 2 +- Content.Shared/CCVar/CCVars.Chat.cs | 14 +- Content.Shared/Gibbing/GibbingSystem.cs | 16 +- .../EntitySystems/IngestionSystem.API.cs | 3 + .../Projectiles/ProjectileComponent.cs | 2 +- .../Shared/Body/Systems/BodyPartSystem.cs | 93 +- .../Body/Systems/UnremoveableOrganSystem.cs | 16 +- .../entities/clothing/head/helmets.ftl | 4 +- .../Catalog/Fills/Lockers/security.yml | 2 + .../VendingMachines/Inventories/secdrobe.yml | 2 + .../Weapons/Guns/Projectiles/Bullets/base.yml | 1 + .../Structures/Lighting/base_lighting.yml | 3 +- .../Roles/Jobs/Security/security_officer.yml | 1 + .../_WL/Entities/Clothing/Head/helmets.yml | 23 + .../Helmets/secheavy.rsi/equipped-HELMET.png | Bin 0 -> 785 bytes .../Head/Helmets/secheavy.rsi/icon.png | Bin 0 -> 331 bytes .../Head/Helmets/secheavy.rsi/inhand-left.png | Bin 0 -> 489 bytes .../Helmets/secheavy.rsi/inhand-right.png | Bin 0 -> 508 bytes .../Head/Helmets/secheavy.rsi/meta.json | 26 + 35 files changed, 1768 insertions(+), 132 deletions(-) create mode 100644 Content.Client/Ashfall/Chat/RunechatSpeechBubble.cs create mode 100644 Content.Shared/Ashfall/Chat/AshfallRunechatStyles.cs create mode 100644 Resources/Prototypes/_WL/Entities/Clothing/Head/helmets.yml create mode 100644 Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/equipped-HELMET.png create mode 100644 Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/icon.png create mode 100644 Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/inhand-left.png create mode 100644 Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/inhand-right.png create mode 100644 Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/meta.json diff --git a/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs b/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs index 73ceab8a227..a9985aad1eb 100644 --- a/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs +++ b/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs @@ -1,9 +1,12 @@ using System.Numerics; using Content.Shared.Ashfall.Animations; +using Content.Shared.Mobs; using Robust.Client.Animations; using Robust.Client.GameObjects; using Robust.Shared.Animations; +using Content.Shared.Mobs.Components; + namespace Content.Client.Ashfall.Animations; public sealed partial class EmoteAnimationSystem : SharedEmoteAnimationSystem @@ -16,6 +19,36 @@ public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnHandleState); + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnShutdown); + } + + private void StopRunningAnimation(EntityUid uid) + { + if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + { + _animationPlayer.Stop(uid, EmoteAnimKey); + if (TryComp(uid, out var sprite)) + { + sprite.Offset = Vector2.Zero; + sprite.Rotation = Angle.Zero; + } + } + } + + private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) + { + if (args.NewMobState != MobState.Alive) + { + StopRunningAnimation(ent.Owner); + PlayEmoteTail(ent.Owner, false); + } + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + StopRunningAnimation(ent.Owner); + PlayEmoteTail(ent.Owner, false); } private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) @@ -25,6 +58,9 @@ private void OnHandleState(Entity ent, ref AfterAutoHan ent.Comp.LastClientAnimationIndex = ent.Comp.CurAnimationIndex; + if (TryComp(ent.Owner, out var mobState) && mobState.CurrentState != MobState.Alive) + return; + switch (ent.Comp.AnimationId) { case AnimationFlip: @@ -87,6 +123,10 @@ public void PlayEmoteJump(EntityUid uid) if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) return; + var baseOffset = Vector2.Zero; + if (TryComp(uid, out var sprite)) + baseOffset = sprite.Offset; + var anim = new Animation { Length = TimeSpan.FromMilliseconds(250), @@ -99,9 +139,9 @@ public void PlayEmoteJump(EntityUid uid) InterpolationMode = AnimationInterpolationMode.Cubic, KeyFrames = { - new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0f), - new AnimationTrackProperty.KeyFrame(new Vector2(0, 0.65f), 0.125f), - new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0.25f), + new AnimationTrackProperty.KeyFrame(baseOffset, 0f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(0, 0.65f), 0.125f), + new AnimationTrackProperty.KeyFrame(baseOffset, 0.25f), } } } @@ -147,6 +187,10 @@ public void PlayEmoteTremble(EntityUid uid) if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) return; + var baseOffset = Vector2.Zero; + if (TryComp(uid, out var sprite)) + baseOffset = sprite.Offset; + var anim = new Animation { Length = TimeSpan.FromMilliseconds(400), @@ -159,14 +203,14 @@ public void PlayEmoteTremble(EntityUid uid) InterpolationMode = AnimationInterpolationMode.Linear, KeyFrames = { - new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0f), - new AnimationTrackProperty.KeyFrame(new Vector2(-0.06f, 0), 0.05f), - new AnimationTrackProperty.KeyFrame(new Vector2(0.06f, 0), 0.10f), - new AnimationTrackProperty.KeyFrame(new Vector2(-0.05f, 0), 0.15f), - new AnimationTrackProperty.KeyFrame(new Vector2(0.05f, 0), 0.20f), - new AnimationTrackProperty.KeyFrame(new Vector2(-0.03f, 0), 0.25f), - new AnimationTrackProperty.KeyFrame(new Vector2(0.03f, 0), 0.30f), - new AnimationTrackProperty.KeyFrame(Vector2.Zero, 0.40f), + new AnimationTrackProperty.KeyFrame(baseOffset, 0f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(-0.06f, 0), 0.05f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(0.06f, 0), 0.10f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(-0.05f, 0), 0.15f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(0.05f, 0), 0.20f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(-0.03f, 0), 0.25f), + new AnimationTrackProperty.KeyFrame(baseOffset + new Vector2(0.03f, 0), 0.30f), + new AnimationTrackProperty.KeyFrame(baseOffset, 0.40f), } } } diff --git a/Content.Client/Ashfall/Chat/RunechatSpeechBubble.cs b/Content.Client/Ashfall/Chat/RunechatSpeechBubble.cs new file mode 100644 index 00000000000..d2a55c43425 --- /dev/null +++ b/Content.Client/Ashfall/Chat/RunechatSpeechBubble.cs @@ -0,0 +1,1158 @@ +using System.Globalization; +using System.Numerics; +using System.Text; +using Content.Client.Chat.UI; +using Content.Client.Resources; +using Content.Client.Stylesheets.Fonts; +using Content.Shared.Ashfall.Chat; +using Content.Shared.CCVar; +using Content.Shared.Chat; +using Content.Shared.Ghost.Components; +using Robust.Client.Graphics; +using Robust.Client.ResourceManagement; +using Robust.Client.UserInterface; +using Robust.Shared.Configuration; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Client.Ashfall.Chat; + +public sealed partial class RunechatSpeechBubble : SpeechBubble +{ + private const string SayStyle = AshfallRunechatStyles.Say; + private const string WhisperStyle = AshfallRunechatStyles.Whisper; + private const string RadioStyle = AshfallRunechatStyles.Radio; + private const string EmoteStyle = AshfallRunechatStyles.Emote; + private const string LoocStyle = AshfallRunechatStyles.Looc; + + private const int LongestText = 80; + private const int ContinueTextLength = LongestText - 5; + private const float SplitChunkSeconds = 4f; + private const float SplitFinalSeconds = 6f; + private const float MinimumRunechatScale = 0.5f; + private const float MaximumRunechatScale = 2.5f; + private const float DefaultLangchatWidth = 140f; + private const float SplitLangchatWidth = 240f; + + private static readonly Color DefaultColor = Color.White; + private static readonly Color ObserverColor = Color.FromHex("#c51fb7"); + private static readonly Color LoocColor = Color.FromHex("#48d1cc"); + private static readonly Color PainColor = Color.FromHex("#c83232"); + private static readonly Color RadioColor = Color.FromHex("#73d48f"); + + /// + /// A run of text that shares the same bold/italic formatting and optional color override. + /// + private readonly record struct TextRun(string Text, bool Bold, bool Italic, Color? ColorOverride = null); + + public RunechatSpeechBubble(SpeechType type, ChatMessage message, EntityUid senderEntity) + : base( + message, + senderEntity, + GetStyleClass(type, message), + GetTextColor(type, message, senderEntity), + GetLifetime(GetRunPages(GetRuns(message, GetStyleClass(type, message))))) + { + RectClipContent = false; + VerticalOffsetAchieved = 0f; + SetHeight = ContentSize.Y; + } + + protected override Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null) + { + var runs = GetRuns(message, speechStyleClass); + var (style, forceBold) = GetVisualStyle(message, speechStyleClass, runs); + var pages = GetRunPages(runs); + + if (forceBold) + { + for (int i = 0; i < pages.Count; i++) + pages[i] = ForceBold(pages[i]); + } + + Texture? languageIcon = null; + if (speechStyleClass is SayStyle or WhisperStyle) + { + TryGetLanguageIcon(message, out languageIcon); + } + + return new RunechatTextControl(pages, fontColor ?? DefaultColor, style, languageIcon); + } + + private static string GetStyleClass(SpeechType type, ChatMessage message) + { + return type switch + { + SpeechType.Emote => EmoteStyle, + SpeechType.Say => SayStyle, + SpeechType.Whisper => WhisperStyle, + SpeechType.Looc => LoocStyle, + _ => SayStyle, + }; + } + + private static Color GetTextColor(SpeechType type, ChatMessage message, EntityUid senderEntity) + { + if (message.MessageColorOverride is { } color) + return color; + + if (AshfallRunechatStyles.IsInterrupting(message.SpeechStyleClass)) + return PainColor; + + if (type == SpeechType.Looc) + return LoocColor; + + if (message.Channel == ChatChannel.Radio) + return RadioColor; + + var entityManager = IoCManager.Resolve(); + if (entityManager.HasComponent(senderEntity)) + return ObserverColor; + + return DefaultColor; + } + + private static (RunechatVisualStyle Style, bool ForceBold) GetVisualStyle( + ChatMessage message, + string speechStyleClass, + List runs) + { + if (message.SpeechStyleClass == AshfallRunechatStyles.Scream) + return (RunechatVisualStyle.Scream, true); + + if (message.SpeechStyleClass == AshfallRunechatStyles.Pain) + return (RunechatVisualStyle.Pain, true); + + if (speechStyleClass == EmoteStyle) + { + return IsYellEmote(RunsToPlainText(runs)) + ? (RunechatVisualStyle.EmoteYell, true) + : (RunechatVisualStyle.Emote, false); + } + + if (message.SpeechStyleClass == "megaphoneSpeech") + return (RunechatVisualStyle.Announce, true); + + if (message.SpeechStyleClass == "commanderSpeech") + return (RunechatVisualStyle.Bolded, true); + + if (speechStyleClass == SayStyle && IsWhollyBold(runs)) + return (RunechatVisualStyle.Bolded, false); + + var baseStyle = speechStyleClass switch + { + RadioStyle => RunechatVisualStyle.Radio, + WhisperStyle => RunechatVisualStyle.Whisper, + _ => RunechatVisualStyle.Normal, + }; + + return (baseStyle, false); + } + + private static bool IsYellEmote(string text) + { + return text.Contains("scream", StringComparison.OrdinalIgnoreCase) + || text.Contains("крик", StringComparison.OrdinalIgnoreCase) + || text.Contains("pain", StringComparison.OrdinalIgnoreCase) + || text.Contains("боль", StringComparison.OrdinalIgnoreCase) + || text.Contains("medic", StringComparison.OrdinalIgnoreCase) + || text.Contains("медик", StringComparison.OrdinalIgnoreCase); + } + + private static TimeSpan GetLifetime(IReadOnlyList> pages) + { + if (pages.Count <= 1) + { + var length = pages.Count == 0 ? 0 : RunsLength(pages[0]); + return TimeSpan.FromSeconds(length / (float)LongestText * SplitChunkSeconds + 2f); + } + + return TimeSpan.FromSeconds((pages.Count - 1) * SplitChunkSeconds + SplitFinalSeconds); + } + + private static List GetRuns(ChatMessage message, string speechStyleClass) + { + var raw = speechStyleClass switch + { + EmoteStyle => message.Message, + SayStyle => GetBubbleContent(message), + WhisperStyle => GetBubbleContent(message), + RadioStyle => FormatRadioText(message), + LoocStyle => $"LOOC: {message.Message}", + _ => message.WrappedMessage, + }; + + if (string.IsNullOrWhiteSpace(raw)) + raw = message.Message; + + var runs = ParseFormattingRuns(raw); + return NormalizeRuns(runs); + } + + private static string GetBubbleContent(ChatMessage message) + { + return SharedChatSystem.GetStringInsideTag(message, "BubbleContent"); + } + + private static string FormatRadioText(ChatMessage message) + { + return message.Message; + } + + private static List ParseFormattingRuns(string markupText) + { + var runs = new List(); + var boldDepth = 0; + var italicDepth = 0; + var colorStack = new Stack(); + var i = 0; + var sb = new StringBuilder(); + + Color? CurrentColor() => colorStack.Count > 0 ? colorStack.Peek() : (Color?)null; + + void Flush() + { + if (sb.Length > 0) + { + runs.Add(new TextRun(sb.ToString(), boldDepth > 0, italicDepth > 0, CurrentColor())); + sb.Clear(); + } + } + + while (i < markupText.Length) + { + var c = markupText[i]; + + if (c == '[') + { + var close = markupText.IndexOf(']', i); + if (close < 0) + { + sb.Append(c); + i++; + continue; + } + + var tag = markupText.Substring(i + 1, close - i - 1); + var lowerTag = tag.ToLowerInvariant(); + + switch (lowerTag) + { + case "bold": + Flush(); + boldDepth++; + i = close + 1; + continue; + case "/bold": + Flush(); + boldDepth = Math.Max(0, boldDepth - 1); + i = close + 1; + continue; + case "italic": + Flush(); + italicDepth++; + i = close + 1; + continue; + case "/italic": + Flush(); + italicDepth = Math.Max(0, italicDepth - 1); + i = close + 1; + continue; + case "bolditalic": + Flush(); + boldDepth++; + italicDepth++; + i = close + 1; + continue; + case "/bolditalic": + Flush(); + boldDepth = Math.Max(0, boldDepth - 1); + italicDepth = Math.Max(0, italicDepth - 1); + i = close + 1; + continue; + case "/color": + Flush(); + if (colorStack.Count > 0) + colorStack.Pop(); + i = close + 1; + continue; + default: + var equalsIndex = tag.IndexOf('='); + var tagName = (equalsIndex >= 0 ? tag[..equalsIndex] : tag).Trim().ToLowerInvariant(); + + if (tagName == "color") + { + Flush(); + var value = equalsIndex >= 0 ? tag[(equalsIndex + 1)..].Trim() : null; + if (!string.IsNullOrEmpty(value) && TryParseColor(value, out var parsedColor)) + colorStack.Push(parsedColor); + else + colorStack.Push(colorStack.Count > 0 ? colorStack.Peek() : DefaultColor); + + i = close + 1; + continue; + } + + if (tagName is "font" or "/font") + { + i = close + 1; + continue; + } + + sb.Append(markupText, i, close - i + 1); + i = close + 1; + continue; + } + } + + sb.Append(c); + i++; + } + + Flush(); + return runs; + } + + private static bool TryParseColor(string value, out Color color) + { + value = value.Trim('"', '\''); + + if (Color.TryFromHex(value, out var hexColor)) + { + color = hexColor; + return true; + } + + if (Color.TryFromName(value, out var namedColor)) + { + color = namedColor; + return true; + } + + color = default; + return false; + } + + private static List NormalizeRuns(List runs) + { + var result = new List(); + var previousWasWhitespace = true; + + foreach (var run in runs) + { + var sb = new StringBuilder(); + foreach (var ch in run.Text) + { + if (char.IsWhiteSpace(ch)) + { + if (!previousWasWhitespace) + sb.Append(' '); + + previousWasWhitespace = true; + } + else + { + sb.Append(ch); + previousWasWhitespace = false; + } + } + + if (sb.Length > 0) + result.Add(run with { Text = sb.ToString() }); + } + + return TrimRunsEnd(result); + } + + private static int RunsLength(IReadOnlyList runs) + { + var length = 0; + foreach (var run in runs) + length += run.Text.Length; + return length; + } + + private static string RunsToPlainText(IReadOnlyList runs) + { + var sb = new StringBuilder(); + foreach (var run in runs) + sb.Append(run.Text); + return sb.ToString(); + } + + private static bool IsWhollyBold(IReadOnlyList runs) + { + var hasContent = false; + foreach (var run in runs) + { + if (run.Text.Trim().Length == 0) + continue; + + hasContent = true; + if (!run.Bold) + return false; + } + + return hasContent; + } + + private static List ForceBold(IReadOnlyList runs) + { + var result = new List(runs.Count); + foreach (var run in runs) + result.Add(run with { Bold = true }); + return result; + } + + private static char GetCharAt(IReadOnlyList runs, int index) + { + var remaining = index; + foreach (var run in runs) + { + if (remaining < run.Text.Length) + return run.Text[remaining]; + + remaining -= run.Text.Length; + } + + throw new ArgumentOutOfRangeException(nameof(index)); + } + + private static int FindLastSpaceIndex(IReadOnlyList runs, int windowStart, int windowEnd) + { + var index = windowEnd - 1; + while (index >= windowStart) + { + if (GetCharAt(runs, index) == ' ') + return index; + + index--; + } + + return -1; + } + + private static (List Left, List Right) SplitRuns(IReadOnlyList runs, int index) + { + var left = new List(); + var right = new List(); + var remainingIndex = index; + + foreach (var run in runs) + { + if (remainingIndex <= 0) + { + right.Add(run); + continue; + } + + if (remainingIndex >= run.Text.Length) + { + left.Add(run); + remainingIndex -= run.Text.Length; + continue; + } + + left.Add(run with { Text = run.Text[..remainingIndex] }); + right.Add(run with { Text = run.Text[remainingIndex..] }); + remainingIndex = 0; + } + + return (left, right); + } + + private static List TrimRunsEnd(List runs) + { + var result = new List(runs); + while (result.Count > 0) + { + var last = result[^1]; + var trimmed = last.Text.TrimEnd(' '); + + if (trimmed.Length == last.Text.Length) + break; + + if (trimmed.Length == 0) + { + result.RemoveAt(result.Count - 1); + continue; + } + + result[^1] = last with { Text = trimmed }; + break; + } + + return result; + } + + private static List TrimRunsStart(List runs) + { + var result = new List(runs); + while (result.Count > 0) + { + var first = result[0]; + var trimmed = first.Text.TrimStart(' '); + + if (trimmed.Length == first.Text.Length) + break; + + if (trimmed.Length == 0) + { + result.RemoveAt(0); + continue; + } + + result[0] = first with { Text = trimmed }; + break; + } + + return result; + } + + private static List PrependPlainRun(List runs, string text) + { + var result = new List { new(text, false, false) }; + result.AddRange(runs); + return result; + } + + private static List AppendPlainRun(List runs, string text) + { + var result = new List(runs) { new(text, false, false) }; + return result; + } + + private static List> GetRunPages(List runs) + { + var pages = new List>(); + var length = RunsLength(runs); + + if (length <= LongestText) + { + pages.Add(runs); + return pages; + } + + var remaining = runs; + while (RunsLength(remaining) > LongestText) + { + var split = GetRunSplitIndex(remaining); + var (left, right) = SplitRuns(remaining, split); + pages.Add(AppendPlainRun(TrimRunsEnd(left), "...")); + remaining = PrependPlainRun(TrimRunsStart(right), "..."); + } + + pages.Add(remaining); + return pages; + } + + private static int GetRunSplitIndex(IReadOnlyList runs) + { + var length = RunsLength(runs); + var max = Math.Min(ContinueTextLength, length); + var split = FindLastSpaceIndex(runs, 0, max); + + return split >= LongestText / 2 + ? split + : max; + } + + private static int ScaleFontSize(int fontSize, float scale) + { + return Math.Max(7, (int)MathF.Round(fontSize * scale)); + } + + private readonly record struct RunechatVisualStyle( + int FontSize, + bool PrefixEmoteIcon, + float MaxWidth, + float LineHeightOffset = 0f, + bool UsePanicShake = false) + { + public static readonly RunechatVisualStyle Normal = new(11, false, DefaultLangchatWidth); + public static readonly RunechatVisualStyle Whisper = new(9, false, DefaultLangchatWidth, -1f); + public static readonly RunechatVisualStyle Radio = new(10, false, SplitLangchatWidth); + public static readonly RunechatVisualStyle Emote = new(10, true, DefaultLangchatWidth, -1f); + public static readonly RunechatVisualStyle EmoteYell = new(12, true, DefaultLangchatWidth); + public static readonly RunechatVisualStyle Bolded = new(12, false, DefaultLangchatWidth); + public static readonly RunechatVisualStyle Announce = new(14, false, SplitLangchatWidth); + public static readonly RunechatVisualStyle Pain = new(11, false, DefaultLangchatWidth); + public static readonly RunechatVisualStyle Scream = new(12, false, DefaultLangchatWidth, UsePanicShake: true); + + public int GetScaledFontSize(float scale) + { + return ScaleFontSize(FontSize, scale); + } + + public float GetScaledMaxWidth(float scale) + { + return MaxWidth * scale; + } + + public float GetScaledLineHeightOffset(float scale) + { + return LineHeightOffset * scale; + } + } + + private sealed partial class RunechatTextControl : Control + { + private const float TextStrokeAlpha = 0.9f; + private const float DefaultEmoteIconPixelSize = 1.4f; + private const float PanicShakeDuration = 0.85f; + private const float PanicShakeFrequency = 18f; + private const float PanicShakeSize = 5f; + private const float EmoteIconVisibleLeft = 3f; + private const float EmoteIconVisibleRight = 8f; + private const float EmoteIconVisibleTop = 3f; + private const float EmoteIconVisibleBottom = 8f; + private const float EmoteIconAlpha = 200f / 255f; + + private static readonly Color EmoteIconBlue = Color.FromHex("#3399ff"); + + private static readonly string[] EmoteIcon = + { + ".........", + ".........", + ".........", + "...B#B#B.", + "...##B##.", + "...BBBBB.", + "...##B##.", + "...B#B#B.", + ".........", + }; + + [Dependency] private IConfigurationManager _configManager = default!; + [Dependency] private IResourceCache _resourceCache = default!; + + private readonly IReadOnlyList> _pages; + private readonly Color _color; + private readonly RunechatVisualStyle _style; + private readonly Texture? _languageIcon; + private readonly float _scale; + private readonly Font _regularFont; + private readonly Font _boldFont; + private readonly Font _italicFont; + private readonly Font _boldItalicFont; + + private readonly List _layouts = new(); + private Vector2 _cachedSize; + private bool _layoutDirty = true; + private int _currentPage; + private float _pageTime; + private float _animationTime; + + public RunechatTextControl(IReadOnlyList> pages, Color color, RunechatVisualStyle style, Texture? languageIcon = null) + { + IoCManager.InjectDependencies(this); + + MouseFilter = MouseFilterMode.Ignore; + _pages = pages; + _color = color; + _style = style; + _languageIcon = languageIcon; + _scale = Math.Clamp(_configManager.GetCVar(CCVars.ChatRunechatBubbleScale), MinimumRunechatScale, MaximumRunechatScale); + + var fontSize = style.GetScaledFontSize(_scale); + var stack = new NotoFontFamilyStack(_resourceCache); + _regularFont = stack.GetFont(fontSize, FontKind.Regular); + _boldFont = stack.GetFont(fontSize, FontKind.Bold); + _italicFont = stack.GetFont(fontSize, FontKind.Italic); + _boldItalicFont = stack.GetFont(fontSize, FontKind.BoldItalic); + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + _animationTime += args.DeltaSeconds; + + if (_pages.Count <= 1 || _currentPage >= _pages.Count - 1) + return; + + _pageTime += args.DeltaSeconds; + while (_pageTime >= SplitChunkSeconds && _currentPage < _pages.Count - 1) + { + _pageTime -= SplitChunkSeconds; + _currentPage++; + } + } + + protected override Vector2 MeasureOverride(Vector2 availableSize) + { + EnsureLayout(); + return _cachedSize; + } + + protected override void Draw(DrawingHandleScreen handle) + { + base.Draw(handle); + + if (_pages.Count == 0) + return; + + EnsureLayout(); + + var layout = _layouts[Math.Min(_currentPage, _layouts.Count - 1)]; + var textOpacity = _configManager.GetCVar(CCVars.SpeechBubbleTextOpacity); + var textColor = _color.WithAlpha(_color.A * textOpacity); + var lineHeight = GetLineHeight(); + var y = (PixelSize.Y - layout.Height) / 2f; + var shakeOffset = GetPanicShakeOffset(); + + for (var i = 0; i < layout.Lines.Count; i++) + { + var line = layout.Lines[i]; + var visibleBounds = GetVisibleBounds(line); + Texture? languageIcon = i == 0 ? _languageIcon : null; + var languageIconWidth = languageIcon != null ? GetLanguageIconSize() : 0f; + var iconWidth = _style.PrefixEmoteIcon && i == 0 ? GetVisibleIconWidth() : 0f; + var iconGap = iconWidth + languageIconWidth > 0f ? GetIconGap() : 0f; + + var contentWidth = iconWidth + languageIconWidth + iconGap + visibleBounds.Width; + var x = (PixelSize.X - contentWidth) / 2f + iconWidth + languageIconWidth + iconGap - visibleBounds.Left + shakeOffset; + var position = new Vector2(x, y); + + if (languageIcon != null) + { + var iconY = position.Y + + visibleBounds.Top + + visibleBounds.Height / 2f - + languageIconWidth / 2f; + + handle.DrawTextureRect( + languageIcon, + UIBox2.FromDimensions(position.X - iconGap - languageIconWidth, iconY, languageIconWidth, languageIconWidth), + Color.White.WithAlpha(textOpacity)); + } + + if (_style.PrefixEmoteIcon && i == 0) + { + var iconY = position.Y + + visibleBounds.Top + + visibleBounds.Height / 2f - + GetVisibleIconHeight() / 2f - + GetVisibleIconTop(); + + DrawEmoteIcon( + handle, + new Vector2(position.X - iconGap - iconWidth - GetVisibleIconLeft(), iconY), + textColor); + } + + DrawOutlinedLine(handle, position, line, textColor, textOpacity); + y += lineHeight; + } + } + + protected override void UIScaleChanged() + { + _layoutDirty = true; + base.UIScaleChanged(); + } + + private void EnsureLayout() + { + if (!_layoutDirty) + return; + + _layouts.Clear(); + + var width = 0f; + var height = 0f; + foreach (var page in _pages) + { + var layout = LayoutPage(page); + _layouts.Add(layout); + width = MathF.Max(width, layout.Width); + height = MathF.Max(height, layout.Height); + } + + var horizontalPadding = (GetPadding() + GetHorizontalSafetyPadding()) * UIScale; + var verticalPadding = GetPadding() * UIScale; + _cachedSize = new Vector2( + (width + horizontalPadding * 2f) / UIScale, + (height + verticalPadding * 2f) / UIScale); + _layoutDirty = false; + } + + private RunechatPageLayout LayoutPage(List pageRuns) + { + var lines = new List>(); + var lineWidths = new List(); + + WrapRunPage(pageRuns, lines, lineWidths); + + if (lines.Count == 0) + { + lines.Add(new List()); + lineWidths.Add(0f); + } + + var width = 0f; + foreach (var lineWidth in lineWidths) + { + width = MathF.Max(width, lineWidth); + } + + var height = GetLineHeight() * lines.Count; + return new RunechatPageLayout(lines, lineWidths, width, height); + } + + private void WrapRunPage(List pageRuns, List> lines, List lineWidths) + { + var words = SplitIntoWords(pageRuns); + var currentLine = new List(); + var currentWidth = 0f; + var spaceWidth = MeasureRunWidth(new TextRun(" ", false, false)); + + foreach (var word in words) + { + var wordWidth = MeasureRunsWidth(word); + var hasContent = currentLine.Count > 0; + var neededWidth = hasContent ? currentWidth + spaceWidth + wordWidth : wordWidth; + + if (neededWidth <= GetMaxWidth()) + { + if (hasContent) + currentLine.Add(new TextRun(" ", false, false)); + + currentLine.AddRange(word); + currentWidth = neededWidth; + continue; + } + + if (hasContent) + { + lines.Add(currentLine); + lineWidths.Add(currentWidth); + currentLine = new List(); + currentWidth = 0f; + } + + if (wordWidth <= GetMaxWidth()) + { + currentLine.AddRange(word); + currentWidth = wordWidth; + } + else + { + var brokenLines = BreakWordAcrossLines(word); + for (var i = 0; i < brokenLines.Count - 1; i++) + { + lines.Add(brokenLines[i]); + lineWidths.Add(MeasureRunsWidth(brokenLines[i])); + } + + currentLine = brokenLines[^1]; + currentWidth = MeasureRunsWidth(currentLine); + } + } + + if (currentLine.Count > 0) + { + lines.Add(currentLine); + lineWidths.Add(currentWidth); + } + } + + private static List> SplitIntoWords(IReadOnlyList runs) + { + var words = new List>(); + var current = new List(); + + void FlushWord() + { + if (current.Count > 0) + { + words.Add(current); + current = new List(); + } + } + + foreach (var run in runs) + { + var text = run.Text; + var start = 0; + + for (var i = 0; i < text.Length; i++) + { + if (text[i] != ' ') + continue; + + if (i > start) + current.Add(run with { Text = text[start..i] }); + + FlushWord(); + start = i + 1; + } + + if (start < text.Length) + current.Add(run with { Text = text[start..] }); + } + + FlushWord(); + return words; + } + + private List> BreakWordAcrossLines(List word) + { + var lines = new List>(); + var current = new List(); + var currentWidth = 0f; + + foreach (var piece in word) + { + var pieceStartWidth = currentWidth; + var builder = new StringBuilder(); + + foreach (var rune in piece.Text.EnumerateRunes()) + { + var candidateText = builder.ToString() + rune; + var candidateWidth = MeasureRunWidth(new TextRun(candidateText, piece.Bold, piece.Italic, piece.ColorOverride)); + + if (builder.Length > 0 && pieceStartWidth + candidateWidth > GetMaxWidth()) + { + current.Add(new TextRun(builder.ToString(), piece.Bold, piece.Italic, piece.ColorOverride)); + lines.Add(current); + + current = new List(); + builder.Clear(); + builder.Append(rune); + pieceStartWidth = 0f; + currentWidth = MeasureRunWidth(new TextRun(builder.ToString(), piece.Bold, piece.Italic, piece.ColorOverride)); + continue; + } + + builder.Append(rune); + currentWidth = pieceStartWidth + candidateWidth; + } + + if (builder.Length > 0) + current.Add(new TextRun(builder.ToString(), piece.Bold, piece.Italic, piece.ColorOverride)); + } + + lines.Add(current); + return lines; + } + + private float GetMaxWidth() + { + return _style.GetScaledMaxWidth(_scale) * UIScale; + } + + private Font GetFont(bool bold, bool italic) + { + if (bold && italic) + return _boldItalicFont; + if (bold) + return _boldFont; + if (italic) + return _italicFont; + return _regularFont; + } + + private float MeasureRunWidth(TextRun run) + { + var width = 0f; + var font = GetFont(run.Bold, run.Italic); + + foreach (var rune in run.Text.EnumerateRunes()) + { + var metrics = font.GetCharMetrics(rune, UIScale); + if (metrics == null) + continue; + + width += metrics.Value.Advance; + } + + return width; + } + + private float MeasureRunsWidth(IReadOnlyList runs) + { + var width = 0f; + foreach (var run in runs) + width += MeasureRunWidth(run); + return width; + } + + private (float Left, float Width, float Top, float Height) GetVisibleBounds(List lineRuns) + { + var cursor = 0f; + var left = 0f; + var right = 0f; + var top = 0f; + var bottom = 0f; + var foundGlyph = false; + + foreach (var run in lineRuns) + { + var font = GetFont(run.Bold, run.Italic); + var ascent = font.GetAscent(UIScale); + + foreach (var rune in run.Text.EnumerateRunes()) + { + var metrics = font.GetCharMetrics(rune, UIScale); + if (metrics == null) + continue; + + var glyphLeft = cursor + metrics.Value.BearingX; + var glyphRight = glyphLeft + metrics.Value.Width; + var glyphTop = ascent - metrics.Value.BearingY; + var glyphBottom = glyphTop + metrics.Value.Height; + + if (!foundGlyph) + { + left = glyphLeft; + right = glyphRight; + top = glyphTop; + bottom = glyphBottom; + foundGlyph = true; + } + else + { + left = MathF.Min(left, glyphLeft); + right = MathF.Max(right, glyphRight); + top = MathF.Min(top, glyphTop); + bottom = MathF.Max(bottom, glyphBottom); + } + + cursor += metrics.Value.Advance; + } + } + + return foundGlyph + ? (left, right - left, top, bottom - top) + : (0f, 0f, 0f, 0f); + } + + private float GetLineHeight() + { + return MathF.Max(1f, _regularFont.GetLineHeight(UIScale) + _style.GetScaledLineHeightOffset(_scale) * UIScale); + } + + private float GetPadding() + { + return _scale * 4f; + } + + private float GetHorizontalSafetyPadding() + { + return _scale * 16f; + } + + private float GetIconPixelSize() + { + return DefaultEmoteIconPixelSize * _scale; + } + + private float GetPanicShakeOffset() + { + if (!_style.UsePanicShake || _animationTime >= PanicShakeDuration) + return 0f; + + var amount = PanicShakeSize * _scale * UIScale; + return MathF.Sin(_animationTime * MathF.PI * PanicShakeFrequency) * amount; + } + + private float GetIconGap() + { + return _scale * 2f * UIScale; + } + + private float GetLanguageIconSize() + => 14f * _scale * UIScale; + + private float GetVisibleIconLeft() + { + return EmoteIconVisibleLeft * GetIconPixelSize() * UIScale; + } + + private float GetVisibleIconWidth() + { + return (EmoteIconVisibleRight - EmoteIconVisibleLeft) * GetIconPixelSize() * UIScale; + } + + private float GetVisibleIconTop() + { + return EmoteIconVisibleTop * GetIconPixelSize() * UIScale; + } + + private float GetVisibleIconHeight() + { + return (EmoteIconVisibleBottom - EmoteIconVisibleTop) * GetIconPixelSize() * UIScale; + } + + private void DrawOutlinedLine( + DrawingHandleScreen handle, + Vector2 position, + List lineRuns, + Color textColor, + float textOpacity) + { + var outline = new TextOutline(1.25f, Color.Black.WithAlpha(textColor.A * TextStrokeAlpha)); + var cursor = position; + + foreach (var run in lineRuns) + { + var font = GetFont(run.Bold, run.Italic); + var drawColor = run.ColorOverride is { } runColor + ? runColor.WithAlpha(runColor.A * textOpacity) + : textColor; + + handle.DrawString(font, cursor, run.Text, UIScale, drawColor, outline); + cursor += new Vector2(MeasureRunWidth(run), 0f); + } + } + + private void DrawEmoteIcon( + DrawingHandleScreen handle, + Vector2 position, + Color textColor) + { + var scale = MathF.Max(1f, GetIconPixelSize() * UIScale); + var iconOrigin = position; + + var iconAlpha = textColor.A * EmoteIconAlpha; + + for (var y = 0; y < EmoteIcon.Length; y++) + { + for (var x = 0; x < EmoteIcon[y].Length; x++) + { + var color = EmoteIcon[y][x] switch + { + '#' => Color.Black.WithAlpha(iconAlpha), + 'B' => EmoteIconBlue.WithAlpha(iconAlpha), + _ => (Color?)null, + }; + + if (color is { } iconColor) + DrawIconPixel(handle, iconOrigin, x, y, scale, iconColor); + } + } + } + + private static void DrawIconPixel( + DrawingHandleScreen handle, + Vector2 iconOrigin, + int x, + int y, + float scale, + Color color) + { + var position = iconOrigin + new Vector2(x * scale, y * scale); + var size = new Vector2(scale, scale); + handle.DrawRect(UIBox2.FromDimensions(position, size), color); + } + + private sealed record RunechatPageLayout( + IReadOnlyList> Lines, + IReadOnlyList LineWidths, + float Width, + float Height); + } +} diff --git a/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs index a72f83e539e..76954597b85 100644 --- a/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs +++ b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionOverlay.cs @@ -46,6 +46,9 @@ protected override bool BeforeDraw(in OverlayDrawArgs args) return false; } + if (!_entityManager.TryGetComponent(player, out EyeComponent? eyeComp) || args.Viewport.Eye != eyeComp.Eye) + return false; + return base.BeforeDraw(in args); } @@ -58,6 +61,9 @@ protected override void Draw(in OverlayDrawArgs args) if (player == null || !_entityManager.TryGetComponent(player.Value, out var playerSprite)) return; + if (!_entityManager.TryGetComponent(player.Value, out var playerXform)) + return; + var worldHandle = args.WorldHandle; var viewport = args.WorldBounds; var eye = args.Viewport.Eye; @@ -68,7 +74,6 @@ protected override void Draw(in OverlayDrawArgs args) worldHandle.DrawRect(viewport, Color.White); worldHandle.UseShader(null); - var playerXform = _entityManager.GetComponent(player.Value); var playerPos = _transformSystem.GetWorldPosition(playerXform); var playerRot = _transformSystem.GetWorldRotation(playerXform); var curTime = _timing.CurTime; @@ -94,5 +99,6 @@ protected override void Draw(in OverlayDrawArgs args) // Render player cleanly on top playerSprite.Render(worldHandle, eye.Rotation, playerRot, null, playerPos); + worldHandle.SetTransform(Matrix3x2.Identity); } } diff --git a/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs index 25199b78785..acc03e52655 100644 --- a/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs +++ b/Content.Client/Ashfall/Overlays/Sandevistan/SandevistanVisionSystem.cs @@ -22,33 +22,48 @@ public override void Initialize() SubscribeLocalEvent(OnComponentShutdown); SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); + + if (_playerMan.LocalEntity is { Valid: true } player + && HasComp(player) + && !_overlayMan.HasOverlay()) + { + _overlayMan.AddOverlay(_overlay); + } } public override void Shutdown() { - _overlayMan.RemoveOverlay(_overlay); + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); base.Shutdown(); } private void OnComponentStartup(Entity ent, ref ComponentStartup args) { if (ent.Owner == _playerMan.LocalEntity) + { + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); _overlayMan.AddOverlay(_overlay); + } } private void OnComponentShutdown(Entity ent, ref ComponentShutdown args) { - if (ent.Owner == _playerMan.LocalEntity) - _overlayMan.RemoveOverlay(_overlay); + if (ent.Owner == _playerMan.LocalEntity && _overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); } private void OnPlayerAttached(Entity ent, ref LocalPlayerAttachedEvent args) { + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); _overlayMan.AddOverlay(_overlay); } private void OnPlayerDetached(Entity ent, ref LocalPlayerDetachedEvent args) { - _overlayMan.RemoveOverlay(_overlay); + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); } } diff --git a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs index c3102a80b0b..c5c3e8b3b34 100644 --- a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs +++ b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveOverlay.cs @@ -45,7 +45,7 @@ public ShockWaveOverlay() protected override bool BeforeDraw(in OverlayDrawArgs args) { - if (args.Viewport.Eye == null || ScreenTexture == null) + if (args.Viewport.Eye == null) return false; if (_xformSystem is null && !_entMan.TrySystem(out _xformSystem)) @@ -53,14 +53,13 @@ protected override bool BeforeDraw(in OverlayDrawArgs args) _currentCount = 0; - _cached.RemoveAll(entry => (float)(_timing.CurTime - entry.Instance.InitTime).TotalSeconds > entry.Instance.FadeTime); + _cached.RemoveAll(entry => (float)(_timing.RealTime - entry.Instance.InitTime).TotalSeconds > entry.Instance.FadeTime || !_entMan.EntityExists(entry.Entity)); foreach (var (entityUid, distortion) in _cached) { - if (!_entMan.EntityExists(entityUid)) + if (!_entMan.TryGetComponent(entityUid, out var xform)) continue; - var xform = _entMan.GetComponent(entityUid); if (xform.MapID != args.MapId) continue; @@ -70,7 +69,7 @@ protected override bool BeforeDraw(in OverlayDrawArgs args) tempCoords.Y = 1 - tempCoords.Y / args.Viewport.Size.Y; tempCoords.X /= args.Viewport.Size.X; - var time = (float)(_timing.CurTime - distortion.InitTime).TotalSeconds; + var time = Math.Max(0f, (float)(_timing.RealTime - distortion.InitTime).TotalSeconds); var fade = 1f - Math.Clamp(time / distortion.FadeTime, 0f, 1f); var i = _currentCount; @@ -105,6 +104,7 @@ protected override void Draw(in OverlayDrawArgs args) _shader.SetParameter("renderScale", args.Viewport.RenderScale * args.Viewport.Eye.Scale); var worldHandle = args.WorldHandle; + worldHandle.SetTransform(Matrix3x2.Identity); worldHandle.UseShader(_shader); worldHandle.DrawRect(args.WorldBounds, Color.White); worldHandle.UseShader(null); @@ -118,10 +118,16 @@ public void Register(Entity ent) WaveStrength = ent.Comp.WaveStrength, DownScale = ent.Comp.DownScale, FadeTime = ent.Comp.FadeTime, - InitTime = ent.Comp.InitTime == TimeSpan.Zero ? _timing.CurTime : ent.Comp.InitTime + InitTime = ent.Comp.InitTime == TimeSpan.Zero ? _timing.RealTime : ent.Comp.InitTime })); } + public void Clear() + { + _cached.Clear(); + _currentCount = 0; + } + private struct InnerShaderInstance { public float WaveSpeed; diff --git a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs index d7b779a93da..25e2f3d9af9 100644 --- a/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs +++ b/Content.Client/Ashfall/Overlays/ShockWave/ShockWaveSystem.cs @@ -21,19 +21,23 @@ public override void Initialize() { base.Initialize(); + if (_overlayMan.HasOverlay()) + _overlayMan.RemoveOverlay(); _overlay = new ShockWaveOverlay(); _overlayMan.AddOverlay(_overlay); SubscribeLocalEvent(OnShockWaveStartup); SubscribeLocalEvent(OnShockWaveRemoved); SubscribeLocalEvent(OnExplosionStateApplied); + SubscribeLocalEvent(OnExplosionVisualsShutdown); SubscribeLocalEvent(OnExplosionTexturesStartup); - SubscribeLocalEvent(OnExplosionTexturesRemove); + SubscribeLocalEvent(OnExplosionTexturesShutdown); } public override void Shutdown() { - _overlayMan.RemoveOverlay(_overlay); + _overlay.Clear(); + _overlayMan.RemoveOverlay(); _registered.Clear(); _spawnedForExplosion.Clear(); base.Shutdown(); @@ -58,7 +62,12 @@ private void OnExplosionTexturesStartup(EntityUid uid, ExplosionVisualsTexturesC TrySpawnShockWave(uid, visuals); } - private void OnExplosionTexturesRemove(EntityUid uid, ExplosionVisualsTexturesComponent comp, ComponentRemove args) + private void OnExplosionVisualsShutdown(EntityUid uid, ExplosionVisualsComponent comp, ComponentShutdown args) + { + _spawnedForExplosion.Remove(uid); + } + + private void OnExplosionTexturesShutdown(EntityUid uid, ExplosionVisualsTexturesComponent comp, ComponentShutdown args) { _spawnedForExplosion.Remove(uid); } @@ -69,7 +78,7 @@ private void OnShockWaveStartup(Entity ent, ref ComponentSta return; if (ent.Comp.InitTime == TimeSpan.Zero) - ent.Comp.InitTime = _timing.CurTime; + ent.Comp.InitTime = _timing.RealTime; _overlay.Register(ent); } diff --git a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs index 7fad5fe6672..c4c29368759 100644 --- a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs +++ b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerOverlay.cs @@ -7,11 +7,12 @@ public sealed class TracerOverlay : Overlay { private readonly TracerSystem _tracer; - public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities; + public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV; public TracerOverlay(TracerSystem tracer) { _tracer = tracer; + ZIndex = 10; IoCManager.InjectDependencies(this); } diff --git a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs index b368a0c3b78..44351f35b09 100644 --- a/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs +++ b/Content.Client/Ashfall/Weapons/Ranged/Tracer/TracerSystem.cs @@ -2,6 +2,9 @@ using Content.Shared.Ashfall.Weapons.Ranged.Tracer; using Robust.Client.Graphics; using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Physics.Components; +using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Client.Ashfall.Weapons.Ranged.Tracer; @@ -11,81 +14,129 @@ public sealed partial class TracerSystem : EntitySystem [Dependency] private IGameTiming _timing = default!; [Dependency] private IOverlayManager _overlay = default!; [Dependency] private SharedTransformSystem _transform = default!; + [Dependency] private IPrototypeManager _proto = default!; + + private static readonly ProtoId UnshadedShaderId = "unshaded"; private TracerOverlay _tracerOverlay = default!; + private ShaderInstance _unshadedShader = default!; + private readonly List _activeTrails = new(); + + private const float TracerThickness = 0.08f; + private const float TrailFadeDuration = 0.22f; public override void Initialize() { base.Initialize(); + _unshadedShader = _proto.Index(UnshadedShaderId).Instance(); + if (_overlay.HasOverlay()) + _overlay.RemoveOverlay(); _tracerOverlay = new TracerOverlay(this); _overlay.AddOverlay(_tracerOverlay); SubscribeLocalEvent(OnTracerStart); + SubscribeLocalEvent(OnTracerRemove); } public override void Shutdown() { + _overlay.RemoveOverlay(); + _activeTrails.Clear(); base.Shutdown(); - _overlay.RemoveOverlay(_tracerOverlay); } private void OnTracerStart(Entity ent, ref ComponentStartup args) { var xform = Transform(ent); - var pos = _transform.GetWorldPosition(xform); + var currentPos = _transform.GetWorldPosition(xform); + var positions = new List { currentPos }; ent.Comp.Data = new TracerData( - new List { pos }, - _timing.CurTime + TimeSpan.FromSeconds(ent.Comp.Lifetime) + positions, + _timing.RealTime + TimeSpan.FromSeconds(ent.Comp.Lifetime) ); + + _activeTrails.Add(new ActiveTracerTrail + { + Entity = ent.Owner, + MapId = xform.MapID, + Color = ent.Comp.Color, + Length = ent.Comp.Length, + Positions = positions, + EndTime = _timing.RealTime + TimeSpan.FromSeconds(ent.Comp.Lifetime), + }); + } + + private void OnTracerRemove(Entity ent, ref ComponentRemove args) + { + foreach (var trail in _activeTrails) + { + if (trail.Entity == ent.Owner) + { + trail.Entity = null; + trail.FadeStartTime ??= _timing.RealTime; + } + } } - public override void Update(float frameTime) + public override void FrameUpdate(float frameTime) { - base.Update(frameTime); + base.FrameUpdate(frameTime); - var curTime = _timing.CurTime; - var query = EntityQueryEnumerator(); + var curTime = _timing.RealTime; - while (query.MoveNext(out var uid, out var tracer, out var xform)) + for (var i = _activeTrails.Count - 1; i >= 0; i--) { - var currentPos = _transform.GetWorldPosition(xform); + var trail = _activeTrails[i]; + + if (trail.FadeStartTime is { } fadeStart) + { + if ((float)(curTime - fadeStart).TotalSeconds >= trail.FadeDuration) + { + _activeTrails.RemoveAt(i); + } + continue; + } - if (tracer.Data == null) + if (trail.Entity == null || !Exists(trail.Entity) || Deleted(trail.Entity) || curTime > trail.EndTime) { - tracer.Data = new TracerData( - new List { currentPos }, - curTime + TimeSpan.FromSeconds(tracer.Lifetime) - ); + trail.Entity = null; + trail.FadeStartTime = curTime; + continue; } - var data = tracer.Data.Value; - if (curTime > data.EndTime) + if (!TryComp(trail.Entity.Value, out TransformComponent? xform)) { - RemCompDeferred(uid); + trail.Entity = null; + trail.FadeStartTime = curTime; continue; } - data.PositionHistory.Add(currentPos); + trail.MapId = xform.MapID; + var currentPos = _transform.GetWorldPosition(xform); + + if (trail.Positions.Count == 0 || Vector2.DistanceSquared(trail.Positions[^1], currentPos) > 0.0001f) + { + trail.Positions.Add(currentPos); + } - while (data.PositionHistory.Count > 2 && - GetTrailLength(data.PositionHistory) > tracer.Length) + while (trail.Positions.Count > 2 && GetTrailLength(trail.Positions) > trail.Length) { - data.PositionHistory.RemoveAt(0); + trail.Positions.RemoveAt(0); } - if (data.PositionHistory.Count >= 2) + if (trail.Positions.Count >= 2) { - var trailLen = GetTrailLength(data.PositionHistory); - if (trailLen > tracer.Length) + var trailLen = GetTrailLength(trail.Positions); + if (trailLen > trail.Length) { - var excess = trailLen - tracer.Length; - var seg = data.PositionHistory[1] - data.PositionHistory[0]; + var excess = trailLen - trail.Length; + var seg = trail.Positions[1] - trail.Positions[0]; var segLen = seg.Length(); if (segLen > 0.0001f) { var t = MathF.Min(excess / segLen, 1f); - data.PositionHistory[0] = Vector2.Lerp(data.PositionHistory[0], data.PositionHistory[1], t); + trail.Positions[0] = Vector2.Lerp(trail.Positions[0], trail.Positions[1], t); } } } @@ -104,24 +155,68 @@ private static float GetTrailLength(List positions) public void Draw(DrawingHandleWorld handle, MapId currentMap) { - var query = EntityQueryEnumerator(); + var curTime = _timing.RealTime; + var hasDrawnAny = false; - while (query.MoveNext(out _, out var tracer, out var xform)) + handle.SetTransform(Matrix3x2.Identity); + + foreach (var trail in _activeTrails) { - if (xform.MapID != currentMap || tracer.Data == null) + if (trail.MapId != currentMap || trail.Positions.Count < 2) continue; - var positions = tracer.Data.Value.PositionHistory; - - if (positions.Count < 2) - continue; + var fade = 1.0f; + if (trail.FadeStartTime is { } fadeStart) + { + fade = 1.0f - Math.Clamp((float)(curTime - fadeStart).TotalSeconds / trail.FadeDuration, 0f, 1f); + if (fade <= 0.001f) + continue; + } - handle.SetTransform(Matrix3x2.Identity); + if (!hasDrawnAny) + { + handle.UseShader(_unshadedShader); + hasDrawnAny = true; + } - for (var i = 1; i < positions.Count; i++) + var segCount = trail.Positions.Count - 1; + for (var i = 1; i < trail.Positions.Count; i++) { - handle.DrawLine(positions[i - 1], positions[i], tracer.Color); + var p0 = trail.Positions[i - 1]; + var p1 = trail.Positions[i]; + var diff = p1 - p0; + var len = diff.Length(); + if (len < 0.001f) + continue; + + var progress = (float)i / segCount; + var segAlpha = fade * MathHelper.Lerp(0.35f, 1.0f, progress); + var color = trail.Color.WithAlpha(trail.Color.A * segAlpha); + + var rect = new Box2Rotated( + Box2.FromDimensions(-len / 2f, -TracerThickness / 2f, len, TracerThickness), + diff.ToAngle(), + (p0 + p1) / 2f); + + handle.DrawRect(rect, color); } } + + if (hasDrawnAny) + { + handle.UseShader(null); + } + } + + private sealed class ActiveTracerTrail + { + public EntityUid? Entity; + public MapId MapId; + public Color Color; + public float Length; + public List Positions = new(); + public TimeSpan EndTime; + public TimeSpan? FadeStartTime; + public float FadeDuration = TrailFadeDuration; } } diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index 84f0770c8dc..9dc7c527975 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; +using Content.Client.Ashfall.Chat; using Content.Client.Chat.Managers; using Content.Shared.CCVar; using Content.Shared.Chat; @@ -68,7 +69,7 @@ public enum SpeechType : byte private bool _dead; public float VerticalOffset { get; set; } - private float _verticalOffsetAchieved; + protected float VerticalOffsetAchieved; public Vector2 ContentSize { get; private set; } @@ -77,6 +78,9 @@ public enum SpeechType : byte public static SpeechBubble CreateSpeechBubble(SpeechType type, ChatMessage message, EntityUid senderEntity) { + if (IoCManager.Resolve().GetCVar(CCVars.ChatEnableRunechatBubbles)) + return new RunechatSpeechBubble(type, message, senderEntity); + switch (type) { case SpeechType.Emote: @@ -96,7 +100,7 @@ public static SpeechBubble CreateSpeechBubble(SpeechType type, ChatMessage messa } } - public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null) + public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechStyleClass, Color? fontColor = null, TimeSpan? totalTime = null) { IoCManager.InjectDependencies(this); _senderEntity = senderEntity; @@ -114,8 +118,8 @@ public SpeechBubble(ChatMessage message, EntityUid senderEntity, string speechSt bubble.Measure(Vector2Helpers.Infinity); ContentSize = bubble.DesiredSize; - _verticalOffsetAchieved = -ContentSize.Y; - _deathTime = _timing.RealTime + TotalTime; + VerticalOffsetAchieved = -ContentSize.Y; + _deathTime = _timing.RealTime + (totalTime ?? TotalTime); } protected abstract Control BuildBubble(ChatMessage message, string speechStyleClass, Color? fontColor = null); @@ -169,13 +173,13 @@ protected override void FrameUpdate(FrameEventArgs args) } // Lerp to our new vertical offset if it's been modified. - if (MathHelper.CloseToPercent(_verticalOffsetAchieved - VerticalOffset, 0, 0.1)) + if (MathHelper.CloseToPercent(VerticalOffsetAchieved - VerticalOffset, 0, 0.1)) { - _verticalOffsetAchieved = VerticalOffset; + VerticalOffsetAchieved = VerticalOffset; } else { - _verticalOffsetAchieved = MathHelper.Lerp(_verticalOffsetAchieved, VerticalOffset, 10 * args.DeltaSeconds); + VerticalOffsetAchieved = MathHelper.Lerp(VerticalOffsetAchieved, VerticalOffset, 10 * args.DeltaSeconds); } if (!_entityManager.TryGetComponent(_senderEntity, out var xform) || xform.MapID != _eyeManager.CurrentEye.Position.MapId) @@ -206,13 +210,20 @@ protected override void FrameUpdate(FrameEventArgs args) var worldPos = _transformSystem.GetWorldPosition(xform) + offset + GetWorldPositionOffset(_senderEntity, xform); var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale + GetScreenPositionOffset(_senderEntity, xform); - var screenPos = lowerCenter - new Vector2(ContentSize.X / 2, ContentSize.Y + _verticalOffsetAchieved); + var screenPos = lowerCenter - new Vector2(ContentSize.X / 2, ContentSize.Y + VerticalOffsetAchieved); // Round to nearest 0.5 screenPos = (screenPos * 2).Rounded() / 2; LayoutContainer.SetPosition(this, screenPos); - var height = MathF.Ceiling(MathHelper.Clamp(lowerCenter.Y - screenPos.Y, 0, ContentSize.Y)); - SetHeight = height; + if (!RectClipContent) + { + SetHeight = ContentSize.Y; + } + else + { + var height = MathF.Ceiling(MathHelper.Clamp(lowerCenter.Y - screenPos.Y, 0, ContentSize.Y)); + SetHeight = height; + } } private void Die() diff --git a/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs b/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs index 9a8688a6291..09f08e8c806 100644 --- a/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs +++ b/Content.Client/Movement/Systems/ClientSpriteMovementSystem.cs @@ -11,7 +11,7 @@ namespace Content.Client.Movement.Systems; public sealed partial class ClientSpriteMovementSystem : SharedSpriteMovementSystem { [Dependency] private SpriteSystem _sprite = default!; - [Dependency] private EntityQuery _spriteQuery; + [Dependency] private EntityQuery _spriteQuery = default!; protected override void OnSpriteMoveInput(Entity ent, ref SpriteMoveEvent args) { diff --git a/Content.Client/Trauma/Prediction/PredictedProjectileSystem.cs b/Content.Client/Trauma/Prediction/PredictedProjectileSystem.cs index d6625146c6e..581ebeb352f 100644 --- a/Content.Client/Trauma/Prediction/PredictedProjectileSystem.cs +++ b/Content.Client/Trauma/Prediction/PredictedProjectileSystem.cs @@ -1,18 +1,18 @@ -// SPDX-License-Identifier: AGPL-3.0-or-later - using Content.Shared.GameTicking; using Content.Shared.Projectiles; using Robust.Client.Physics; +using Robust.Client.Player; using Robust.Shared.Physics.Systems; namespace Content.Client.Trauma.Prediction; /// /// Marks projectiles as participating in client-side physics prediction. -/// Only marks projectiles fired by the local player (via ShotPredictedProjectileEvent). +/// Seamlessly predicts projectiles shot by the local player without race conditions. /// public sealed partial class PredictedProjectileSystem : EntitySystem { + [Dependency] private IPlayerManager _player = default!; [Dependency] private SharedPhysicsSystem _physics = default!; private readonly HashSet _predictedProjectiles = new(); @@ -24,21 +24,47 @@ public override void Initialize() SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnPlayerShotProjectile); + SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnUpdateIsPredicted); SubscribeNetworkEvent(OnShotPredictedProjectile); SubscribeNetworkEvent(OnRoundRestart); } + private void TryPredict(EntityUid uid, ProjectileComponent comp) + { + if (_predictedProjectiles.Contains(uid)) + return; + + var netEnt = GetNetEntity(uid); + var isLocalShooter = comp.Shooter != null && comp.Shooter == _player.LocalEntity; + var isPending = _pendingNetEntities.Remove(netEnt); + + if (isLocalShooter || isPending) + { + _predictedProjectiles.Add(uid); + _physics.UpdateIsPredicted(uid); + } + } + private void OnStartup(Entity ent, ref ComponentStartup args) { - var netEnt = GetNetEntity(ent.Owner); - if (_pendingNetEntities.Remove(netEnt)) + TryPredict(ent.Owner, ent.Comp); + } + + private void OnPlayerShotProjectile(ref PlayerShotProjectileEvent args) + { + if (args.User == _player.LocalEntity && TryComp(args.Projectile, out var comp)) { - _predictedProjectiles.Add(ent.Owner); - _physics.UpdateIsPredicted(ent.Owner); + TryPredict(args.Projectile, comp); } } + private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) + { + TryPredict(ent.Owner, ent.Comp); + } + private void OnShutdown(Entity ent, ref ComponentShutdown args) { _predictedProjectiles.Remove(ent.Owner); diff --git a/Content.IntegrationTests/Tests/Ashfall/TraumaMedicalTests.cs b/Content.IntegrationTests/Tests/Ashfall/TraumaMedicalTests.cs index 12d973ce4b1..c01cf7b0b08 100644 --- a/Content.IntegrationTests/Tests/Ashfall/TraumaMedicalTests.cs +++ b/Content.IntegrationTests/Tests/Ashfall/TraumaMedicalTests.cs @@ -22,6 +22,7 @@ using Content.Shared.DoAfter; using Content.Shared.Emp; using Content.Shared.FixedPoint; +using Content.Shared.Gibbing; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Inventory; @@ -436,4 +437,59 @@ public void GunBashPrototypeDealsBluntAndStaminaDamage() Assert.That(melee.Damage.DamageDict["Blunt"], Is.GreaterThan(FixedPoint2.Zero)); Assert.That(melee.BluntStaminaDamageFactor, Is.GreaterThan(FixedPoint2.Zero)); } + + [Test] + public async Task TorsoGibbingDropsAllLimbs() + { + var pair = Pair; + var server = pair.Server; + var map = await pair.CreateTestMap(); + + EntityUid human = default; + EntityUid torso = default; + EntityUid head = default; + EntityUid leftArm = default; + EntityUid rightArm = default; + EntityUid leftLeg = default; + EntityUid rightLeg = default; + EntityUid leftHand = default; + EntityUid rightHand = default; + EntityUid leftFoot = default; + EntityUid rightFoot = default; + + await server.WaitAssertion(() => + { + human = server.EntMan.Spawn("MobHuman", map.MapCoords); + var body = server.System(); + torso = body.GetOrgan(human, "Torso")!.Value; + head = body.GetOrgan(human, "Head")!.Value; + leftArm = body.GetOrgan(human, "ArmLeft")!.Value; + rightArm = body.GetOrgan(human, "ArmRight")!.Value; + leftLeg = body.GetOrgan(human, "LegLeft")!.Value; + rightLeg = body.GetOrgan(human, "LegRight")!.Value; + leftHand = body.GetOrgan(human, "HandLeft")!.Value; + rightHand = body.GetOrgan(human, "HandRight")!.Value; + leftFoot = body.GetOrgan(human, "FootLeft")!.Value; + rightFoot = body.GetOrgan(human, "FootRight")!.Value; + + server.System().Gib(torso); + }); + + await pair.RunTicksSync(5); + + await server.WaitAssertion(() => + { + Assert.That(server.EntMan.Deleted(torso), Is.True); + Assert.That(server.EntMan.Deleted(human), Is.True); + Assert.That(server.EntMan.Deleted(head), Is.False, "Head was deleted!"); + Assert.That(server.EntMan.Deleted(leftArm), Is.False, "Left arm was deleted!"); + Assert.That(server.EntMan.Deleted(rightArm), Is.False, "Right arm was deleted!"); + Assert.That(server.EntMan.Deleted(leftLeg), Is.False, "Left leg was deleted!"); + Assert.That(server.EntMan.Deleted(rightLeg), Is.False, "Right leg was deleted!"); + Assert.That(server.EntMan.Deleted(leftHand), Is.False, "Left hand was deleted!"); + Assert.That(server.EntMan.Deleted(rightHand), Is.False, "Right hand was deleted!"); + Assert.That(server.EntMan.Deleted(leftFoot), Is.False, "Left foot was deleted!"); + Assert.That(server.EntMan.Deleted(rightFoot), Is.False, "Right foot was deleted!"); + }); + } } diff --git a/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs b/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs index fa61b8317a0..84f300275cd 100644 --- a/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs +++ b/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later using Content.Shared.Alert; +using Content.Shared.Armor; using Content.Shared.Ashfall.Audio; using Content.Shared.Ashfall.Combat.Concussion; using Content.Shared.Explosion; @@ -11,6 +12,7 @@ using Content.Shared.Rejuvenate; using Content.Shared.Damage; using Content.Shared.Damage.Systems; +using Content.Shared.Inventory; using Content.Shared.Speech.Components; using Robust.Shared.Prototypes; using Robust.Shared.Timing; @@ -23,6 +25,7 @@ public sealed partial class ConcussionSystem : SharedConcussionSystem [Dependency] private IGameTiming _timing = default!; [Dependency] private AlertsSystem _alertsSystem = default!; [Dependency] private SharedDeafnessSystem _deafness = default!; + [Dependency] private InventorySystem _inventory = default!; private static readonly ProtoId ConcussionAlert = "Concussion"; @@ -32,7 +35,7 @@ public override void Initialize() SubscribeLocalEvent(OnDamageChanged); SubscribeLocalEvent(OnBeforeExplode); - SubscribeLocalEvent(OnFlashAttempt); + SubscribeLocalEvent(OnAfterFlashed); SubscribeLocalEvent(OnConcussionStateChanged); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnRejuvenate); @@ -47,16 +50,22 @@ private void OnDamageChanged(EntityUid uid, ConcussionThresholdComponent comp, D if (args.DamageDelta == null) return; + var helmetProtection = 1.0f; + if (_inventory.TryGetSlotEntity(uid, "head", out var headItem) && HasComp(headItem)) + { + helmetProtection = 0.5f; + } + // Heavy blunt trauma (batons, hammers, impacts) causes concussion shock if (args.DamageDelta.DamageDict.TryGetValue("Blunt", out var blunt) && blunt.Float() >= 15f) { - AddConcussionDamage(uid, comp, FixedPoint2.New(blunt.Float() * 1.2f)); + AddConcussionDamage(uid, comp, FixedPoint2.New(blunt.Float() * 1.2f * helmetProtection)); } // Heavy caliber piercing rounds deliver hydraulic/kinetic shock if (args.DamageDelta.DamageDict.TryGetValue("Piercing", out var piercing) && piercing.Float() >= 25f) { - AddConcussionDamage(uid, comp, FixedPoint2.New(piercing.Float() * 0.8f)); + AddConcussionDamage(uid, comp, FixedPoint2.New(piercing.Float() * 0.8f * helmetProtection)); } } @@ -111,18 +120,43 @@ private void OnBeforeExplode(EntityUid uid, ConcussionThresholdComponent comp, r if (totalDmg <= 0) return; - var concussionDmg = FixedPoint2.New(totalDmg * 1.5f); - AddConcussionDamage(uid, comp, concussionDmg); + var earProt = _deafness.GetEarProtection(uid); + var acousticMultiplier = MathF.Max(0.0f, 1.0f - earProt); + var concussionDmg = FixedPoint2.New(totalDmg * 1.5f * acousticMultiplier); - var deafDuration = TimeSpan.FromSeconds(Math.Clamp(totalDmg * 0.25f, 15f, 25f)); - _deafness.TryDeafen(uid, deafDuration); + if (concussionDmg > 0) + { + AddConcussionDamage(uid, comp, concussionDmg); + } + + if (earProt < 0.8f) + { + var deafDuration = TimeSpan.FromSeconds(Math.Clamp(totalDmg * 0.25f, 15f, 25f)); + _deafness.TryDeafen(uid, deafDuration); + } } - private void OnFlashAttempt(EntityUid uid, ConcussionThresholdComponent comp, ref FlashAttemptEvent args) + private void OnAfterFlashed(EntityUid uid, ConcussionThresholdComponent comp, ref AfterFlashedEvent args) { - // Flashbang or flash in close proximity causes head disorientation and deafening - AddConcussionDamage(uid, comp, FixedPoint2.New(35)); - _deafness.TryDeafen(uid, TimeSpan.FromSeconds(20)); + if (args.Target != uid) + return; + + if (_deafness.HasEarProtection(uid)) + return; + + var isFlashbang = args.Used is { } used && HasComp(used); + + if (isFlashbang) + { + // Flashbang explosive detonation causes acute acoustic shock and disorientation + AddConcussionDamage(uid, comp, FixedPoint2.New(35)); + _deafness.TryDeafen(uid, TimeSpan.FromSeconds(20)); + } + else + { + // Optical flash causes minor disorientation without permanent acoustic deafness + AddConcussionDamage(uid, comp, FixedPoint2.New(10)); + } } private void OnRefreshSpeed(EntityUid uid, ConcussionThresholdComponent comp, RefreshMovementSpeedModifiersEvent args) diff --git a/Content.Server/Projectiles/LagCompProjectileSystem.cs b/Content.Server/Projectiles/LagCompProjectileSystem.cs index 7991685aef4..79b51d501d1 100644 --- a/Content.Server/Projectiles/LagCompProjectileSystem.cs +++ b/Content.Server/Projectiles/LagCompProjectileSystem.cs @@ -37,13 +37,16 @@ public override void Update(float frameTime) var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { + if (Deleted(uid)) + continue; + if (comp.Targets.Count == 0 || comp.ShooterSession == null) continue; var pos = _transform.GetMapCoordinates(uid); foreach (var target in comp.Targets) { - if (Deleted(target)) + if (Deleted(target) || target == comp.Shooter) continue; var lagPos = _transform.ToMapCoordinates(_lag.GetCoordinates(target, comp.ShooterSession)); @@ -80,6 +83,9 @@ private void OnStartCollide(Entity ent, ref StartCol return; var target = args.OtherEntity; + if (target == ent.Comp.Shooter) + return; + if (_lagQuery.HasComp(target)) ent.Comp.Targets.Add(target); } diff --git a/Content.Shared/Ashfall/Audio/SharedDeafnessSystem.cs b/Content.Shared/Ashfall/Audio/SharedDeafnessSystem.cs index 11d4712b425..407254f224c 100644 --- a/Content.Shared/Ashfall/Audio/SharedDeafnessSystem.cs +++ b/Content.Shared/Ashfall/Audio/SharedDeafnessSystem.cs @@ -20,21 +20,27 @@ public override void Initialize() base.Initialize(); } - public bool HasEarProtection(EntityUid uid) + public float GetEarProtection(EntityUid uid) { - if (TryComp(uid, out var selfProt) && selfProt.Protection >= 0.8f) - return true; + var maxProt = 0f; + if (TryComp(uid, out var selfProt)) + maxProt = MathF.Max(maxProt, selfProt.Protection); if (_inventory.TryGetContainerSlotEnumerator(uid, out var slots, SlotFlags.EARS | SlotFlags.HEAD)) { while (slots.NextItem(out var item, out _)) { - if (TryComp(item, out var prot) && prot.Protection >= 0.8f) - return true; + if (TryComp(item, out var prot)) + maxProt = MathF.Max(maxProt, prot.Protection); } } - return false; + return maxProt; + } + + public bool HasEarProtection(EntityUid uid) + { + return GetEarProtection(uid) >= 0.8f; } public bool TryDeafen(EntityUid uid, TimeSpan duration, bool ignoreProtection = false, bool showPopup = true) diff --git a/Content.Shared/Ashfall/Chat/AshfallRunechatStyles.cs b/Content.Shared/Ashfall/Chat/AshfallRunechatStyles.cs new file mode 100644 index 00000000000..b8238f11c38 --- /dev/null +++ b/Content.Shared/Ashfall/Chat/AshfallRunechatStyles.cs @@ -0,0 +1,17 @@ +namespace Content.Shared.Ashfall.Chat; + +public static class AshfallRunechatStyles +{ + public const string Say = "runechatSay"; + public const string Whisper = "runechatWhisper"; + public const string Radio = "runechatRadio"; + public const string Emote = "runechatEmote"; + public const string Looc = "runechatLooc"; + public const string Scream = "scream"; + public const string Pain = "pain"; + + public static bool IsInterrupting(string? style) + { + return style == Scream || style == Pain; + } +} diff --git a/Content.Shared/Ashfall/Combat/Concussion/ConcussionThresholdComponent.cs b/Content.Shared/Ashfall/Combat/Concussion/ConcussionThresholdComponent.cs index 9941141e38f..a10fc4af98c 100644 --- a/Content.Shared/Ashfall/Combat/Concussion/ConcussionThresholdComponent.cs +++ b/Content.Shared/Ashfall/Combat/Concussion/ConcussionThresholdComponent.cs @@ -15,7 +15,7 @@ public sealed partial class ConcussionThresholdComponent : Component public FixedPoint2 AbsoluteCap = FixedPoint2.New(200); [DataField] - public FixedPoint2 HealRate = FixedPoint2.New(1.5f); + public FixedPoint2 HealRate = FixedPoint2.New(5.0f); [DataField] public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1); diff --git a/Content.Shared/CCVar/CCVars.Chat.cs b/Content.Shared/CCVar/CCVars.Chat.cs index 9f31e0527e6..5744905d571 100644 --- a/Content.Shared/CCVar/CCVars.Chat.cs +++ b/Content.Shared/CCVar/CCVars.Chat.cs @@ -1,4 +1,4 @@ -using Robust.Shared.Configuration; +using Robust.Shared.Configuration; namespace Content.Shared.CCVar; @@ -54,6 +54,18 @@ public sealed partial class CCVars CVar.CLIENTONLY | CVar.ARCHIVE, "Toggles displaying a background under the speaking character's name."); + public static readonly CVarDef ChatEnableRunechatBubbles = + CVarDef.Create("chat.enable_runechat_bubbles", + true, + CVar.CLIENTONLY | CVar.ARCHIVE, + "Toggles displaying runechat-style text speech bubbles overhead."); + + public static readonly CVarDef ChatRunechatBubbleScale = + CVarDef.Create("chat.runechat_bubble_scale", + 1.0f, + CVar.CLIENTONLY | CVar.ARCHIVE, + "The scale factor applied to runechat speech bubbles."); + /// /// A message broadcast to each player that joins the lobby. /// May be changed by admins ingame through use of the "set-motd" command. diff --git a/Content.Shared/Gibbing/GibbingSystem.cs b/Content.Shared/Gibbing/GibbingSystem.cs index 794c62c9798..565b43d2644 100644 --- a/Content.Shared/Gibbing/GibbingSystem.cs +++ b/Content.Shared/Gibbing/GibbingSystem.cs @@ -1,6 +1,8 @@ +using Content.Shared.Body; using Content.Shared.Destructible; using Robust.Shared.Audio.Systems; using Robust.Shared.Audio; +using Robust.Shared.Containers; using Robust.Shared.Network; using Robust.Shared.Physics.Systems; using Robust.Shared.Random; @@ -12,6 +14,7 @@ public sealed partial class GibbingSystem : EntitySystem [Dependency] private INetManager _net = default!; [Dependency] private IRobustRandom _random = default!; [Dependency] private SharedAudioSystem _audio = default!; + [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedDestructibleSystem _destructible = default!; [Dependency] private SharedPhysicsSystem _physics = default!; [Dependency] private SharedTransformSystem _transform = default!; @@ -49,9 +52,20 @@ public HashSet Gib(EntityUid ent, bool dropGiblets = true, EntityUid? if (dropGiblets) { + var dropTarget = ent; + while (_container.TryGetContainingContainer(dropTarget, out var container)) + { + if (HasComp(container.Owner)) + { + dropTarget = container.Owner; + continue; + } + break; + } + foreach (var giblet in gibbed) { - _transform.DropNextTo(giblet, ent); + _transform.DropNextTo(giblet, dropTarget); FlingDroppedEntity(giblet); } } diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs index f74c7888b21..89a0121b8ec 100644 --- a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.API.cs @@ -157,6 +157,9 @@ public bool CanConsume(EntityUid user, /// Optional user that will attempt to pickup spawned trash. public void SpawnTrash(Entity entity, EntityUid? user = null) { + if (entity.Comp.Trash.Count == 0) + return; + var pickup = user is not null && _hands.TryDrop(user.Value, entity); foreach (var trash in entity.Comp.Trash) { diff --git a/Content.Shared/Projectiles/ProjectileComponent.cs b/Content.Shared/Projectiles/ProjectileComponent.cs index 7a3a98769d2..1060389aeaa 100644 --- a/Content.Shared/Projectiles/ProjectileComponent.cs +++ b/Content.Shared/Projectiles/ProjectileComponent.cs @@ -7,7 +7,7 @@ namespace Content.Shared.Projectiles; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true), AutoGenerateComponentPause] public sealed partial class ProjectileComponent : Component { /// diff --git a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyPartSystem.cs b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyPartSystem.cs index 6770e3147eb..62ed2d2e158 100644 --- a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyPartSystem.cs +++ b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyPartSystem.cs @@ -6,7 +6,8 @@ using Content.Shared.FixedPoint; using Content.Shared.Fluids; using Content.Shared.Gibbing; -using Content.Shared.Throwing; +using Content.Medical.Common.Wounds; +using Content.Medical.Shared.Wounds; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; @@ -22,12 +23,11 @@ public sealed partial class BodyPartSystem : CommonBodyPartSystem { [Dependency] private BodySystem _body = default!; [Dependency] private BodyCacheSystem _cache = default!; + [Dependency] private OrganRelationSystem _organRelation = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedPuddleSystem _puddle = default!; - [Dependency] private SharedTransformSystem _transform = default!; - [Dependency] private ThrowingSystem _throwing = default!; [Dependency] private EntityQuery _query = default!; [Dependency] private EntityQuery _childQuery = default!; [Dependency] private EntityQuery _organQuery = default!; @@ -42,6 +42,7 @@ public override void Initialize() SubscribeLocalEvent(OnPartInserted); SubscribeLocalEvent(OnPartRemoved); SubscribeLocalEvent(OnBeingGibbed); + SubscribeLocalEvent>(OnBodyRelayedBeingGibbed); } private void OnPartInserted(Entity ent, ref OrganGotInsertedEvent args) @@ -64,6 +65,9 @@ private void OnPartInserted(Entity ent, ref OrganGotInsertedE if (!_container.Remove(organ, container)) Log.Error($"Organ {ToPrettyString(organ)} got stuck inside of {ToPrettyString(ent)} after being inserted into {ToPrettyString(args.Target)}"); } + + // need to mark the part so it renders! + Dirty(ent); } private void OnPartRemoved(Entity ent, ref OrganGotRemovedEvent args) @@ -87,8 +91,9 @@ private void OnPartRemoved(Entity ent, ref OrganGotRemovedEve var container = EnsureSeveredOrgansContainer(ent); foreach (var (category, organ) in ent.Comp.Children.ToArray()) { + _body.RemoveOrgan(body, organ); // slot has an organ so try to put it in the container - if (!_container.Insert(organ, container)) + if (!_container.Insert(organ, container, force: true)) { // probably from failing to be removed, suspicious Log.Error($"Failed to store {ToPrettyString(ent)}'s {category} organ {ToPrettyString(organ)}!"); @@ -98,49 +103,99 @@ private void OnPartRemoved(Entity ent, ref OrganGotRemovedEve } private void OnBeingGibbed(Entity ent, ref BeingGibbedEvent args) + { + SpillBodyPartOrgans(ent, args.Giblets); + } + + private void OnBodyRelayedBeingGibbed(Entity ent, ref BodyRelayedEvent args) + { + // Only root body parts (like Torso) need to spill when relayed from body; + // child limbs will be detached by the root part. + if (!HasComp(ent)) + SpillBodyPartOrgans(ent, args.Args.Giblets); + } + + private void SpillBodyPartOrgans(Entity ent, HashSet giblets) { var organsToSpill = new List(); if (GetSeveredOrgansContainer(ent.AsNullable()) is {} container) { - foreach (var organ in container.ContainedEntities) + foreach (var organ in container.ContainedEntities.ToArray()) { organsToSpill.Add(organ); } } + Entity? body = _body.GetBody(ent.Owner) is { } bodyUid ? bodyUid : null; + foreach (var (category, organ) in ent.Comp.Children.ToArray()) { if (Deleted(organ) || organsToSpill.Contains(organ)) continue; - if (_organQuery.TryComp(organ, out var organComp) && organComp.Body is { } body) + // If the child is a body part itself (e.g. arm, leg, head attached to torso), + // sever it: gather its sub-children into its own container so they survive with the limb. + if (_query.TryComp(organ, out var childPartComp)) { - _body.RemoveOrgan(body, organ); + if (body != null) + { + var subChildren = _organRelation.AllChildren(organ).ToList(); + var partContainer = EnsureSeveredOrgansContainer((organ, childPartComp)); + + _body.RemoveOrgan(body.Value, organ); + + var delimbedEvent = new BodyPartDelimbedEvent(body.Value.Owner, organ, Crude: true); + RaiseLocalEvent(body.Value.Owner, ref delimbedEvent); + + foreach (var subChild in subChildren) + { + _body.RemoveOrgan(body.Value, subChild.Owner); + // Clear severed container of sub-child parts if any, to avoid nested OnPartInserted + if (_container.Insert(subChild.Owner, partContainer, force: true)) + { + if (_body.GetCategory(subChild.Owner) is { } subCat) + childPartComp.Children[subCat] = subChild.Owner; + } + } + DirtyField(organ, childPartComp, nameof(BodyPartComponent.Children)); + } + + if (TryComp(organ, out var woundable)) + { + woundable.WoundableSeverity = WoundableSeverity.Severed; + DirtyField(organ, woundable, nameof(WoundableComponent.WoundableSeverity)); + } + + organsToSpill.Add(organ); + } + else + { + // Internal organ without BodyPartComponent (e.g. heart, lungs, etc.) + if (body != null && _organQuery.TryComp(organ, out var organComp) && organComp.Body is { }) + { + _body.RemoveOrgan(body.Value, organ); + } + organsToSpill.Add(organ); } - organsToSpill.Add(organ); } + ent.Comp.Children.Clear(); + DirtyField(ent.Owner, ent.Comp, nameof(BodyPartComponent.Children)); + if (organsToSpill.Count == 0) return; - _audio.PlayPvs(GibSound, ent.Owner); + var dropTarget = body != null ? body.Value.Owner : ent.Owner; + _audio.PlayPvs(GibSound, dropTarget); var bloodSolution = new Solution(); bloodSolution.AddReagent(BloodReagent, FixedPoint2.New(15)); - _puddle.TrySpillAt(ent.Owner, bloodSolution, out _, sound: false); + _puddle.TrySpillAt(dropTarget, bloodSolution, out _, sound: false); - var rand = new System.Random(); foreach (var organ in organsToSpill) { - args.Giblets.Add(organ); - - _transform.DropNextTo(organ, ent.Owner); - - var angle = rand.NextSingle() * MathF.PI * 2f; - var dir = new System.Numerics.Vector2(MathF.Cos(angle), MathF.Sin(angle)); - var dist = 1.0f + rand.NextSingle() * 1.5f; - _throwing.TryThrow(organ, dir * dist, 1.5f, pushbackRatio: 0.2f); + giblets.Add(organ); } } diff --git a/Content.Shared/Trauma/Medical/Shared/Body/Systems/UnremoveableOrganSystem.cs b/Content.Shared/Trauma/Medical/Shared/Body/Systems/UnremoveableOrganSystem.cs index 8b243134259..2e4baf7f6dd 100644 --- a/Content.Shared/Trauma/Medical/Shared/Body/Systems/UnremoveableOrganSystem.cs +++ b/Content.Shared/Trauma/Medical/Shared/Body/Systems/UnremoveableOrganSystem.cs @@ -20,7 +20,7 @@ public override void Initialize() SubscribeLocalEvent(OnRemoveAttempt); SubscribeLocalEvent(OnRemoved); - SubscribeLocalEvent(OnBeingGibbed); + SubscribeLocalEvent(OnBeingGibbed, after: new[] { typeof(BodyPartSystem) }); } private void OnRemoveAttempt(Entity ent, ref OrganRemoveAttemptEvent args) @@ -30,14 +30,18 @@ private void OnRemoveAttempt(Entity ent, ref OrganRe private void OnRemoved(Entity ent, ref OrganGotRemovedEvent args) { - if (TerminatingOrDeleted(args.Target) || Transform(args.Target).MapID == MapId.Nullspace || _timing.ApplyingState) + if (TerminatingOrDeleted(args.Target) || EntityManager.IsQueuedForDeletion(args.Target) || Transform(args.Target).MapID == MapId.Nullspace || _timing.ApplyingState) return; // all good if it's being deleted or leaving pvs range // if you intentionally deleted the root part, please delete the body instead chud - if (!TerminatingOrDeleted(ent) && !HasComp(ent)) + if (TerminatingOrDeleted(ent) || EntityManager.IsQueuedForDeletion(ent)) { - Log.Warning($"{ToPrettyString(ent)} was deleted instead of the body, {ToPrettyString(args.Target)}!"); - PredictedQueueDel(args.Target); + if (!HasComp(ent)) + { + Log.Warning($"{ToPrettyString(ent)} was deleted instead of the body, {ToPrettyString(args.Target)}!"); + PredictedQueueDel(args.Target); + } + return; } Log.Warning($"{ToPrettyString(ent)} somehow got removed from {ToPrettyString(args.Target)}!"); @@ -49,7 +53,7 @@ private void OnBeingGibbed(Entity ent, ref BeingGibb if (HasComp(ent) || _body.GetBody(ent.Owner) is not {} body) return; - Log.Info($"Root part {ToPrettyString(ent)} was gibbed, gibbing {ToPrettyString(ent)} too!"); + Log.Info($"Root part {ToPrettyString(ent)} was gibbed, gibbing {ToPrettyString(body)} too!"); _gibbing.Gib(body); } } diff --git a/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/clothing/head/helmets.ftl b/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/clothing/head/helmets.ftl index 52b95bda8d7..5cc01875f51 100644 --- a/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/clothing/head/helmets.ftl +++ b/Resources/Locale/ru-RU/ss14-ru/prototypes/entities/clothing/head/helmets.ftl @@ -2,7 +2,7 @@ ent-ClothingHeadHelmetBase = { ent-ClothingHeadBase } .desc = { ent-ClothingHeadBase.desc } ent-ClothingHeadHelmetArmoredBase = { ent-ClothingHeadHelmetBase } .desc = { ent-ClothingHeadHelmetBase.desc } -ent-ClothingHeadHelmetBasic = шлем +ent-ClothingHeadHelmetBasic = шлем СБ .desc = Стандартная защитная экипировка. Защищает голову от ударов. ent-ClothingHeadHelmetMerc = шлем наёмника .desc = Боевой шлем обычно используется наёмниками, он прочный, лёгкий и пахнет порохом и джунглями. @@ -58,3 +58,5 @@ ent-ActionToggleJusticeHelm = Переключить шлем правосуди .desc = Включает или выключает шлем правосудия. ent-CardHelmet = картонный шлем .desc = Средневековый шлем, сделанный из картона. +ent-ClothingHeadHelmetHeavyDSS = армированный шлем + .desc = Улучшенная версия обычного шлема Службы Безопасности, оснащённая противоосколочным забралом. diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml index 6ae3e82f05d..7d211b58000 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/security.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/security.yml @@ -78,6 +78,8 @@ - id: ClothingUniformJumpsuitSecGrey prob: 0.3 - id: ClothingHeadHelmetBasic + - id: ClothingHeadHelmetHeavyDSS + prob: 0.5 - id: ClothingOuterArmorBasic - id: ClothingBeltSecurityFilled - id: Flash diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/secdrobe.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/secdrobe.yml index c9b9e3ffda3..cc6cb6b9a41 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/secdrobe.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/secdrobe.yml @@ -24,6 +24,8 @@ ClothingEyesBlindfold: 1 ClothingShoesBootsCombat: 1 ClothingShoesBootsWinterSec: 2 + ClothingHeadHelmetBasic: 3 + ClothingHeadHelmetHeavyDSS: 2 ClothingHeadHelmetJustice: 1 contrabandInventory: ClothingMaskClownSecurity: 1 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml index 502ca212864..fae342a0c29 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/Bullets/base.yml @@ -6,6 +6,7 @@ description: If you can see this you're probably dead! components: - type: Sprite + drawdepth: Effects noRot: false sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi layers: diff --git a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml index 8fa681f9f36..a9eb2066117 100644 --- a/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml +++ b/Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml @@ -31,10 +31,11 @@ shader: unshaded - type: PointLight color: "#FFE4CE" # 5000K color temp + lightMask: TubeBloomLight # AshFall-Tweak energy: 0.8 radius: 10 softness: 1 -# offset: "0, 0.25" + offset: "0, 0.45" - type: RCDDeconstructable cost: 4 delay: 2 diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml index d7dc05f8c90..cec95c63ec2 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml @@ -27,6 +27,7 @@ - type: startingGear id: SecurityOfficerGear equipment: + head: ClothingHeadHelmetBasic eyes: ClothingEyesGlassesSecurity ears: ClothingHeadsetSecurity pocket1: WeaponPistolMk58 diff --git a/Resources/Prototypes/_WL/Entities/Clothing/Head/helmets.yml b/Resources/Prototypes/_WL/Entities/Clothing/Head/helmets.yml new file mode 100644 index 00000000000..2b779e8025d --- /dev/null +++ b/Resources/Prototypes/_WL/Entities/Clothing/Head/helmets.yml @@ -0,0 +1,23 @@ +- type: entity + parent: [ClothingHeadHelmetArmoredBase, BaseSecurityContraband] + id: ClothingHeadHelmetHeavyDSS + name: reinforced helmet + description: An improved version of a regular Security helmet, equipped with a shatterproof visor. + components: + - type: Sprite + sprite: _WL/Clothing/Head/Helmets/secheavy.rsi + - type: Clothing + sprite: _WL/Clothing/Head/Helmets/secheavy.rsi + - type: Armor + modifiers: + coefficients: + Blunt: 0.85 + Slash: 0.85 + Piercing: 0.85 + Heat: 0.9 + - type: ExplosionResistance + damageCoefficient: 0.95 + - type: Tag + tags: + - WhitelistChameleon + - SecurityHelmet diff --git a/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/equipped-HELMET.png b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/equipped-HELMET.png new file mode 100644 index 0000000000000000000000000000000000000000..cf0bb21fb1f404e666d7de0c234dde36d2abf554 GIT binary patch literal 785 zcmV+s1Md8ZP)Px#1ZP1_K>z@;j|==^1poj7p-DtRRCr$Pn$2ocK@`WQQWBwwVroAGu?_gK>O;8l zSp?TUgBwBULv-aExO3%F1VJ~gU-6?gP?HoBkjQVi=XNMTXU2PP7yf^6nL9Vl{LeXa zW^TzD_rJ4Lv`K7UyD=|rOTK)5pEXtv=(pP!i=K1S>CbHaYc~z-V;r zou*|b!hRE6>UPVZ+eM$}IV1&OHk-PKSJq2sKMg+mmi~_G`OoVHXIb;KmVwsKAvptJ z_j=F0xHa*?;JVjdEk3CiG7e`+cV;|nd2V|{3P}NIwc5qqV*d8|zru{0v)qR*&$&+} zD+A68c6oD_*|y7qlJgcw{z7kcV@~=%J?i`Z%adPjFrB+sZs>;Jzt;X5ghji3WB&&L zK?u{G&95ovg%CmrA%qY@2qAY7(2Lb1)2QJd5G?p5Kq|#XE{DYHk{yJNtrf5?9dF3!R8fclW*?~>imU>1Y zIsgo81xSQHOqTrx7(|Jja(SHvQpQHZ&Ln`@flb(kjVVEhP8S;#0haQBz1&jK6w?$YVn zN89W$o2*(KOLPECgu%}Wb-R~++a#tLP7L>97Q_%808)y9xrn?SOgL|_0b9asNO==f zvQDjB{(Nib&kb;9U~llc(mmkO&7HZOMR?Pic!#Sf$ya71K_D$HD{}rR`4izL%v(U` z5rhyz2qAPx$1xZ9fR9J=Wl)*{^F%X77MVb}%;?~+-(8V4+diBwK5uZwL(l@YL^65jr^mZ!y}eD6k4{H;@4v+JJO>D| zCZY_KRUMqm@ZK{QL3Ig$b6LBVRUKtwH^#s==xm^@>R_g*E}4rU(l9t2;9Q1C(^kYx zf1HtD88A~W4~{TXL>d4druXe$q`^!%r@&0R%Ek}j)o}s9#r}kww-Y9dPl?;tcL1i^ z`SCzR1$sT#Sv2PC-xX?HgXNp0Jf|`ETAid-p8}%5`H{@Ku^Aq#%2F_H}LV z&Y9Ov3Zz9X)=`}@^LLhH!*TNimi8+WZqy%tS;wrtXx2)%y zNa#iCedVtvMUtjFPn?Nj*8KjVpgx@8(b3%>Omcgk>B`<)w)&k3UjjFG%q!1H?&r3! z2`oR6`ShD`!IC@70{?>(&e(Kby?p8^!#aEZ$v^G}E<9B6B_hJy>HJ^04W57eTwMO& a6<_>%sn-HUI~QQwF?hQAxvXyB%N$~^5a)k*t++x!Opk|qx|*JgZc@S`qsbEr z7hYbljaMtQ-%-F5l_yE}vEHmc4aD zcfe}%9s#~X8)UzWx8*9gOuqR!>e7O1T<%M``sCj|_!-K~5X2{V$T^(<+5NwBL}JC2 zH6Oc5cy=y&trYb5%B&;833GEQUu=;p3|aNS;YV>^`M##2h86O)QvJUKn~EkxO|M|O zcHK40@!IzNB43p`9y+Z2xLa2IkY|j<>IvTteqO!oa@CQ{dhzG}!ZF&-dTbJR?k~Ic z$MiQJ(53^gMFX;WX5S0VIzMyr$3Lq!TCdw0_2Oi-y!@UuCxrzOp!Q@*)1Drq!x#IP zEqiDdGMBPAA{zH#nB$LbeMDC%H-m|UMM z+WypgOI78zS^YEDHv~Pd5X`yvZrALcrr)B|>=@1Wu69~`&mjM}&A!jqPch6$_+2S| tz?Dz;+3VxwWmZn{$IKf#I{s}I&&ink+i9+A3@|zwJYD@<);T3K0RZ8C-kJaa literal 0 HcmV?d00001 diff --git a/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/meta.json b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/meta.json new file mode 100644 index 00000000000..888093b8647 --- /dev/null +++ b/Resources/Textures/_WL/Clothing/Head/Helmets/secheavy.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by MureixIoL for CorvaxWL", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-HELMET", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} From 8f18b97ce3d61cc3f73b2daba6e50760465b24c3 Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Mon, 21 Sep 2026 00:04:06 +0200 Subject: [PATCH 08/11] Address review comments on emote transforms and concussion protection --- .../Animations/EmoteAnimationSystem.cs | 61 +++++++++++++------ .../Combat/Concussion/ConcussionSystem.cs | 8 +-- .../Entities/Clothing/Head/helmets.yml | 2 + 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs b/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs index a9985aad1eb..cbeec20a720 100644 --- a/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs +++ b/Content.Client/Ashfall/Animations/EmoteAnimationSystem.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Numerics; using Content.Shared.Ashfall.Animations; using Content.Shared.Mobs; @@ -14,6 +15,7 @@ public sealed partial class EmoteAnimationSystem : SharedEmoteAnimationSystem [Dependency] private AnimationPlayerSystem _animationPlayer = default!; private const string EmoteAnimKey = "AshfallEmoteAnimation"; + private readonly Dictionary _savedTransforms = new(); public override void Initialize() { @@ -21,6 +23,19 @@ public override void Initialize() SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnAnimationCompleted); + } + + private bool TryStartAnimation(EntityUid uid, [NotNullWhen(true)] out SpriteComponent? sprite) + { + if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey) || !TryComp(uid, out sprite)) + { + sprite = null; + return false; + } + + _savedTransforms[uid] = (sprite.Offset, sprite.Rotation); + return true; } private void StopRunningAnimation(EntityUid uid) @@ -28,12 +43,28 @@ private void StopRunningAnimation(EntityUid uid) if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) { _animationPlayer.Stop(uid, EmoteAnimKey); - if (TryComp(uid, out var sprite)) + if (_savedTransforms.Remove(uid, out var saved) && TryComp(uid, out var sprite)) { - sprite.Offset = Vector2.Zero; - sprite.Rotation = Angle.Zero; + sprite.Offset = saved.Offset; + sprite.Rotation = saved.Rotation; } } + else + { + _savedTransforms.Remove(uid); + } + } + + private void OnAnimationCompleted(EntityUid uid, EmoteAnimationComponent comp, AnimationCompletedEvent args) + { + if (args.Key != EmoteAnimKey) + return; + + if (_savedTransforms.Remove(uid, out var saved) && TryComp(uid, out var sprite)) + { + sprite.Offset = saved.Offset; + sprite.Rotation = saved.Rotation; + } } private void OnMobStateChanged(Entity ent, ref MobStateChangedEvent args) @@ -88,12 +119,10 @@ private void OnHandleState(Entity ent, ref AfterAutoHan public void PlayEmoteFlip(EntityUid uid) { - if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + if (!TryStartAnimation(uid, out var sprite)) return; - var baseAngle = Angle.Zero; - if (TryComp(uid, out var sprite)) - baseAngle = sprite.Rotation; + var baseAngle = sprite.Rotation; var anim = new Animation { @@ -120,12 +149,10 @@ public void PlayEmoteFlip(EntityUid uid) public void PlayEmoteJump(EntityUid uid) { - if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + if (!TryStartAnimation(uid, out var sprite)) return; - var baseOffset = Vector2.Zero; - if (TryComp(uid, out var sprite)) - baseOffset = sprite.Offset; + var baseOffset = sprite.Offset; var anim = new Animation { @@ -152,12 +179,10 @@ public void PlayEmoteJump(EntityUid uid) public void PlayEmoteTurn(EntityUid uid) { - if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + if (!TryStartAnimation(uid, out var sprite)) return; - var baseAngle = Angle.Zero; - if (TryComp(uid, out var sprite)) - baseAngle = sprite.Rotation; + var baseAngle = sprite.Rotation; var anim = new Animation { @@ -184,12 +209,10 @@ public void PlayEmoteTurn(EntityUid uid) public void PlayEmoteTremble(EntityUid uid) { - if (_animationPlayer.HasRunningAnimation(uid, EmoteAnimKey)) + if (!TryStartAnimation(uid, out var sprite)) return; - var baseOffset = Vector2.Zero; - if (TryComp(uid, out var sprite)) - baseOffset = sprite.Offset; + var baseOffset = sprite.Offset; var anim = new Animation { diff --git a/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs b/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs index 84f300275cd..076e36dcf8d 100644 --- a/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs +++ b/Content.Server/Ashfall/Combat/Concussion/ConcussionSystem.cs @@ -51,7 +51,7 @@ private void OnDamageChanged(EntityUid uid, ConcussionThresholdComponent comp, D return; var helmetProtection = 1.0f; - if (_inventory.TryGetSlotEntity(uid, "head", out var headItem) && HasComp(headItem)) + if (_inventory.TryGetSlotEntity(uid, "head", out var headItem) && HasComp(headItem)) { helmetProtection = 0.5f; } @@ -141,13 +141,13 @@ private void OnAfterFlashed(EntityUid uid, ConcussionThresholdComponent comp, re if (args.Target != uid) return; - if (_deafness.HasEarProtection(uid)) - return; - var isFlashbang = args.Used is { } used && HasComp(used); if (isFlashbang) { + if (_deafness.HasEarProtection(uid)) + return; + // Flashbang explosive detonation causes acute acoustic shock and disorientation AddConcussionDamage(uid, comp, FixedPoint2.New(35)); _deafness.TryDeafen(uid, TimeSpan.FromSeconds(20)); diff --git a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml index 8d9e6fd1e49..7e4ae69c89d 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml @@ -28,6 +28,8 @@ Heat: 0.9 - type: EarProtection protection: 0.95 + - type: ConcussionProtection + protection: 0.5 #Security Helmet - type: entity From 3d123b3e1bfea1989ef3a48d1306f42b460cbed7 Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Mon, 21 Sep 2026 00:35:58 +0200 Subject: [PATCH 09/11] Fix collection modification and client transform lerp error during body gibbing --- Content.Shared/Body/BodySystem.Relay.cs | 17 +++++++++++++++-- .../Inventory/InventorySystem.Trauma.cs | 5 +++++ .../Shared/Body/Systems/BodyEquipmentSystem.cs | 8 ++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/Content.Shared/Body/BodySystem.Relay.cs b/Content.Shared/Body/BodySystem.Relay.cs index 2a69a6f49d1..7fc351e84e8 100644 --- a/Content.Shared/Body/BodySystem.Relay.cs +++ b/Content.Shared/Body/BodySystem.Relay.cs @@ -1,3 +1,4 @@ +using System.Linq; using Content.Shared.Body.Events; using Content.Shared.Gibbing; using Content.Shared.Humanoid; @@ -39,9 +40,15 @@ public void RelayBodyEvent(EntityUid uid, BodyComponent component, T args) wh [PublicAPI] public void RelayEvent(Entity ent, ref T args) where T : struct { + if (ent.Comp.Organs is not { } organs) + return; + var ev = new BodyRelayedEvent(ent, args); - foreach (var organ in ent.Comp.Organs?.ContainedEntities ?? []) + foreach (var organ in organs.ContainedEntities.ToArray()) { + if (Deleted(organ)) + continue; + RaiseLocalEvent(organ, ref ev); } args = ev.Args; @@ -56,9 +63,15 @@ public void RelayEvent(Entity ent, ref T args) where T : struc [PublicAPI] public void RelayEvent(Entity ent, T args) where T : class { + if (ent.Comp.Organs is not { } organs) + return; + var ev = new BodyRelayedEvent(ent, args); - foreach (var organ in ent.Comp.Organs?.ContainedEntities ?? []) + foreach (var organ in organs.ContainedEntities.ToArray()) { + if (Deleted(organ)) + continue; + RaiseLocalEvent(organ, ref ev); } } diff --git a/Content.Shared/Inventory/InventorySystem.Trauma.cs b/Content.Shared/Inventory/InventorySystem.Trauma.cs index 2c01e1a6f28..ab3004ee971 100644 --- a/Content.Shared/Inventory/InventorySystem.Trauma.cs +++ b/Content.Shared/Inventory/InventorySystem.Trauma.cs @@ -1,11 +1,13 @@ using Content.Shared.Random; using Robust.Shared.Map; +using Robust.Shared.Network; using Robust.Shared.Timing; namespace Content.Shared.Inventory; public partial class InventorySystem : EntitySystem { + [Dependency] private readonly INetManager _net = default!; [Dependency] private RandomHelperSystem _randomHelper = default!; /// @@ -14,6 +16,9 @@ public partial class InventorySystem : EntitySystem /// public void DropSlotContents(Entity ent, string slotName) { + if (!_net.IsServer) + return; + if (!Resolve(ent, ref ent.Comp) || Transform(ent).MapID == MapId.Nullspace) return; diff --git a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs index 00e63145b61..44947b53638 100644 --- a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs +++ b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs @@ -7,11 +7,13 @@ using Content.Shared.Inventory; using Content.Shared.Inventory.Events; using Content.Shared.Popups; +using Robust.Shared.Network; namespace Content.Medical.Shared.Body; public sealed partial class BodyEquipmentSystem : EntitySystem { + [Dependency] private readonly INetManager _net = default!; [Dependency] private BodyPartSystem _part = default!; [Dependency] private InventorySystem _inventory = default!; [Dependency] private SharedPopupSystem _popup = default!; @@ -67,6 +69,9 @@ private void OnCheckEquipmentPart(Entity ent, ref CheckE private void OnOrganRemovedFrom(Entity ent, ref OrganRemovedFromEvent args) { + if (!_net.IsServer) + return; + DropPartItems(ent.Owner, args.Organ); } @@ -89,6 +94,9 @@ private bool HasBodyPart(EntityUid body, BodyPartType part) /// public void DropPartItems(Entity ent, Entity part) { + if (!_net.IsServer) + return; + // don't drop for mobs being deleted, gibbing etc would handle it themselves if (!TerminatingOrDeleted(ent) && _inventoryQuery.Resolve(ent, ref ent.Comp) && From a305e3caf7dd65f2391d890aa2bb900efa3a5c37 Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Mon, 21 Sep 2026 00:56:46 +0200 Subject: [PATCH 10/11] Remove readonly modifier from INetManager dependency fields --- Content.Shared/Inventory/InventorySystem.Trauma.cs | 2 +- .../Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Content.Shared/Inventory/InventorySystem.Trauma.cs b/Content.Shared/Inventory/InventorySystem.Trauma.cs index ab3004ee971..037015f9c04 100644 --- a/Content.Shared/Inventory/InventorySystem.Trauma.cs +++ b/Content.Shared/Inventory/InventorySystem.Trauma.cs @@ -7,7 +7,7 @@ namespace Content.Shared.Inventory; public partial class InventorySystem : EntitySystem { - [Dependency] private readonly INetManager _net = default!; + [Dependency] private INetManager _net = default!; [Dependency] private RandomHelperSystem _randomHelper = default!; /// diff --git a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs index 44947b53638..0a36faeb08d 100644 --- a/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs +++ b/Content.Shared/Trauma/Medical/Shared/Body/Systems/BodyEquipmentSystem.cs @@ -13,7 +13,7 @@ namespace Content.Medical.Shared.Body; public sealed partial class BodyEquipmentSystem : EntitySystem { - [Dependency] private readonly INetManager _net = default!; + [Dependency] private INetManager _net = default!; [Dependency] private BodyPartSystem _part = default!; [Dependency] private InventorySystem _inventory = default!; [Dependency] private SharedPopupSystem _popup = default!; From a92e60ec961ccefa93ef8a93fb192268a919a275 Mon Sep 17 00:00:00 2001 From: ss14-ps Date: Mon, 21 Sep 2026 01:25:09 +0200 Subject: [PATCH 11/11] Separate StandardSoapsTable from AllSoapsTable to fix cargo arbitrage and janitor belt fill --- .../Prototypes/Catalog/Fills/Crates/janitorial.yml | 2 +- Resources/Prototypes/Catalog/Fills/Items/belt.yml | 2 +- .../Entities/Markers/Spawners/Random/soap.yml | 10 +++++++++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Resources/Prototypes/Catalog/Fills/Crates/janitorial.yml b/Resources/Prototypes/Catalog/Fills/Crates/janitorial.yml index ada194e1a20..b87de4a78ad 100644 --- a/Resources/Prototypes/Catalog/Fills/Crates/janitorial.yml +++ b/Resources/Prototypes/Catalog/Fills/Crates/janitorial.yml @@ -9,7 +9,7 @@ entity_storage: !type:AllSelector children: - !type:NestedSelector - tableId: AllSoapsTable + tableId: StandardSoapsTable - id: MopItem - id: MopBucketCubeWrapped - id: Bucket diff --git a/Resources/Prototypes/Catalog/Fills/Items/belt.yml b/Resources/Prototypes/Catalog/Fills/Items/belt.yml index 181808861e7..4d301049063 100644 --- a/Resources/Prototypes/Catalog/Fills/Items/belt.yml +++ b/Resources/Prototypes/Catalog/Fills/Items/belt.yml @@ -87,7 +87,7 @@ storagebase: !type:AllSelector children: - !type:NestedSelector - tableId: AllSoapsTable + tableId: StandardSoapsTable - id: SprayBottleSpaceCleaner - id: CleanerGrenade amount: 2 diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/soap.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/soap.yml index 1f4f4b4abab..22d7fe1669b 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/soap.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/soap.yml @@ -16,7 +16,7 @@ weight: 0.2 - type: entityTable # max size 1x2 - id: AllSoapsTable + id: StandardSoapsTable table: !type:GroupSelector children: - id: SoapNT @@ -24,6 +24,14 @@ - id: Soap - id: SoapHomemade - id: SoapDeluxe + +- type: entityTable # max size 1x2 + id: AllSoapsTable + table: !type:GroupSelector + children: + - !type:NestedSelector + tableId: StandardSoapsTable + weight: 5 - !type:GroupSelector # Rare soaps weight: 0.02 # 1 in 250 children: