diff --git a/Content.Client/Chat/UI/SpeechBubble.cs b/Content.Client/Chat/UI/SpeechBubble.cs index 32e9f4ae9b..aa61e73e31 100644 --- a/Content.Client/Chat/UI/SpeechBubble.cs +++ b/Content.Client/Chat/UI/SpeechBubble.cs @@ -2,6 +2,7 @@ using System.Numerics; using Content.Client.Chat.Managers; using Content.Shared.CCVar; using Content.Shared.Chat; +using Content.Shared.Speech; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; @@ -141,7 +142,12 @@ namespace Content.Client.Chat.UI Modulate = Color.White; } - var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -EntityVerticalOffset; + var baseOffset = 0f; + + if (_entityManager.TryGetComponent(_senderEntity, out var speech)) + baseOffset = speech.SpeechBubbleOffset; + + var offset = (-_eyeManager.CurrentEye.Rotation).ToWorldVec() * -(EntityVerticalOffset + baseOffset); var worldPos = _transformSystem.GetWorldPosition(xform) + offset; var lowerCenter = _eyeManager.WorldToScreen(worldPos) / UIScale; diff --git a/Content.Client/Holopad/HolopadBoundUserInterface.cs b/Content.Client/Holopad/HolopadBoundUserInterface.cs new file mode 100644 index 0000000000..20b55ea8c7 --- /dev/null +++ b/Content.Client/Holopad/HolopadBoundUserInterface.cs @@ -0,0 +1,101 @@ +using Content.Shared.Holopad; +using Content.Shared.Silicons.StationAi; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Shared.Player; +using System.Numerics; + +namespace Content.Client.Holopad; + +public sealed class HolopadBoundUserInterface : BoundUserInterface +{ + [Dependency] private readonly ISharedPlayerManager _playerManager = default!; + [Dependency] private readonly IClyde _displayManager = default!; + + [ViewVariables] + private HolopadWindow? _window; + + public HolopadBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + IoCManager.InjectDependencies(this); + } + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + _window.Title = Loc.GetString("holopad-window-title", ("title", EntMan.GetComponent(Owner).EntityName)); + + if (this.UiKey is not HolopadUiKey) + { + Close(); + return; + } + + var uiKey = (HolopadUiKey)this.UiKey; + + // AIs will see a different holopad interface to crew when interacting with them in the world + if (uiKey == HolopadUiKey.InteractionWindow && EntMan.HasComponent(_playerManager.LocalEntity)) + uiKey = HolopadUiKey.InteractionWindowForAi; + + _window.SetState(Owner, uiKey); + _window.UpdateState(new Dictionary()); + + // Set message actions + _window.SendHolopadStartNewCallMessageAction += SendHolopadStartNewCallMessage; + _window.SendHolopadAnswerCallMessageAction += SendHolopadAnswerCallMessage; + _window.SendHolopadEndCallMessageAction += SendHolopadEndCallMessage; + _window.SendHolopadStartBroadcastMessageAction += SendHolopadStartBroadcastMessage; + _window.SendHolopadActivateProjectorMessageAction += SendHolopadActivateProjectorMessage; + _window.SendHolopadRequestStationAiMessageAction += SendHolopadRequestStationAiMessage; + + // If this call is addressed to an AI, open the window in the bottom right hand corner of the screen + if (uiKey == HolopadUiKey.AiRequestWindow) + _window.OpenCenteredAt(new Vector2(1f, 1f)); + + // Otherwise offset to the left so the holopad can still be seen + else + _window.OpenCenteredAt(new Vector2(0.3333f, 0.50f)); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + var castState = (HolopadBoundInterfaceState)state; + EntMan.TryGetComponent(Owner, out var xform); + + _window?.UpdateState(castState.Holopads); + } + + public void SendHolopadStartNewCallMessage(NetEntity receiver) + { + SendMessage(new HolopadStartNewCallMessage(receiver)); + } + + public void SendHolopadAnswerCallMessage() + { + SendMessage(new HolopadAnswerCallMessage()); + } + + public void SendHolopadEndCallMessage() + { + SendMessage(new HolopadEndCallMessage()); + } + + public void SendHolopadStartBroadcastMessage() + { + SendMessage(new HolopadStartBroadcastMessage()); + } + + public void SendHolopadActivateProjectorMessage() + { + SendMessage(new HolopadActivateProjectorMessage()); + } + + public void SendHolopadRequestStationAiMessage() + { + SendMessage(new HolopadStationAiRequestMessage()); + } +} diff --git a/Content.Client/Holopad/HolopadSystem.cs b/Content.Client/Holopad/HolopadSystem.cs new file mode 100644 index 0000000000..3bd556f1fc --- /dev/null +++ b/Content.Client/Holopad/HolopadSystem.cs @@ -0,0 +1,172 @@ +using Content.Shared.Chat.TypingIndicator; +using Content.Shared.Holopad; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using System.Linq; + +namespace Content.Client.Holopad; + +public sealed class HolopadSystem : SharedHolopadSystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnShaderRender); + SubscribeAllEvent(OnTypingChanged); + + SubscribeNetworkEvent(OnPlayerSpriteStateRequest); + SubscribeNetworkEvent(OnPlayerSpriteStateMessage); + } + + private void OnComponentInit(EntityUid uid, HolopadHologramComponent component, ComponentInit ev) + { + if (!TryComp(uid, out var sprite)) + return; + + UpdateHologramSprite(uid); + } + + private void OnShaderRender(EntityUid uid, HolopadHologramComponent component, BeforePostShaderRenderEvent ev) + { + if (ev.Sprite.PostShader == null) + return; + + ev.Sprite.PostShader.SetParameter("t", (float)_timing.CurTime.TotalSeconds * component.ScrollRate); + } + + private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args) + { + var uid = args.SenderSession.AttachedEntity; + + if (!Exists(uid)) + return; + + if (!HasComp(uid)) + return; + + var netEv = new HolopadUserTypingChangedEvent(GetNetEntity(uid.Value), ev.IsTyping); + RaiseNetworkEvent(netEv); + } + + private void OnPlayerSpriteStateRequest(PlayerSpriteStateRequest ev) + { + var targetPlayer = GetEntity(ev.TargetPlayer); + var player = _playerManager.LocalSession?.AttachedEntity; + + // Ignore the request if received by a player who isn't the target + if (targetPlayer != player) + return; + + if (!TryComp(player, out var playerSprite)) + return; + + var spriteLayerData = new List(); + + if (playerSprite.Visible) + { + // Record the RSI paths, state names and shader paramaters of all visible layers + for (int i = 0; i < playerSprite.AllLayers.Count(); i++) + { + if (!playerSprite.TryGetLayer(i, out var layer)) + continue; + + if (!layer.Visible || + string.IsNullOrEmpty(layer.ActualRsi?.Path.ToString()) || + string.IsNullOrEmpty(layer.State.Name)) + continue; + + var layerDatum = new PrototypeLayerData(); + layerDatum.RsiPath = layer.ActualRsi.Path.ToString(); + layerDatum.State = layer.State.Name; + + if (layer.CopyToShaderParameters != null) + { + var key = (string)layer.CopyToShaderParameters.LayerKey; + + if (playerSprite.LayerMapTryGet(key, out var otherLayerIdx) && + playerSprite.TryGetLayer(otherLayerIdx, out var otherLayer) && + otherLayer.Visible) + { + layerDatum.MapKeys = new() { key }; + + layerDatum.CopyToShaderParameters = new PrototypeCopyToShaderParameters() + { + LayerKey = key, + ParameterTexture = layer.CopyToShaderParameters.ParameterTexture, + ParameterUV = layer.CopyToShaderParameters.ParameterUV + }; + } + } + + spriteLayerData.Add(layerDatum); + } + } + + // Return the recorded data to the server + var evResponse = new PlayerSpriteStateMessage(ev.TargetPlayer, spriteLayerData.ToArray()); + RaiseNetworkEvent(evResponse); + } + + private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev) + { + UpdateHologramSprite(GetEntity(ev.SpriteEntity), ev.SpriteLayerData); + } + + private void UpdateHologramSprite(EntityUid uid, PrototypeLayerData[]? layerData = null) + { + if (!TryComp(uid, out var hologramSprite)) + return; + + if (!TryComp(uid, out var holopadhologram)) + return; + + for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--) + hologramSprite.RemoveLayer(i); + + if (layerData == null || layerData.Length == 0) + { + layerData = new PrototypeLayerData[1]; + layerData[0] = new PrototypeLayerData() + { + RsiPath = holopadhologram.RsiPath, + State = holopadhologram.RsiState + }; + } + + for (int i = 0; i < layerData.Length; i++) + { + var layer = layerData[i]; + layer.Shader = "unshaded"; + + hologramSprite.AddLayer(layerData[i], i); + } + + UpdateHologramShader(uid, hologramSprite, holopadhologram); + } + + private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram) + { + // Find the texture height of the largest layer + float texHeight = sprite.AllLayers.Max(x => x.PixelSize.Y); + + var instance = _prototypeManager.Index(holopadHologram.ShaderName).InstanceUnique(); + instance.SetParameter("color1", new Vector3(holopadHologram.Color1.R, holopadHologram.Color1.G, holopadHologram.Color1.B)); + instance.SetParameter("color2", new Vector3(holopadHologram.Color2.R, holopadHologram.Color2.G, holopadHologram.Color2.B)); + instance.SetParameter("alpha", holopadHologram.Alpha); + instance.SetParameter("intensity", holopadHologram.Intensity); + instance.SetParameter("texHeight", texHeight); + instance.SetParameter("t", (float)_timing.CurTime.TotalSeconds * holopadHologram.ScrollRate); + + sprite.PostShader = instance; + sprite.RaiseShaderEvent = true; + } +} diff --git a/Content.Client/Holopad/HolopadWindow.xaml b/Content.Client/Holopad/HolopadWindow.xaml new file mode 100644 index 0000000000..9c3dfab1ea --- /dev/null +++ b/Content.Client/Holopad/HolopadWindow.xaml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +