diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4e1a86d79eb..27fc5e15a29 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,3 +5,7 @@ # Ping for all PRs that include translations/editing fluent strings *.ftl @ficcialfaint + +# Map files +/Resources/Prototypes/Maps/** @Ko4ergaPunk +/Resources/Maps/** @Ko4ergaPunk diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c1671e16882..f78e26fd65b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,6 +6,11 @@ ## Почему / Баланс +## Ссылка на ветку + + ## Технические детали @@ -13,12 +18,6 @@ -## Требования - -- [ ] Я прочитал(а) и следую [Рекомендациям по оформлению Pull Request и Changelog](https://docs.spacestation14.com/en/general-development/codebase-info/pull-request-guidelines.html). -- [ ] Я добавил(а) медиафайлы к этому PR или он не требует демонстрации в игре. - - ## Критические изменения diff --git a/Content.Client/Body/Systems/BodySystem.cs b/Content.Client/Body/Systems/BodySystem.cs deleted file mode 100644 index bab785525b0..00000000000 --- a/Content.Client/Body/Systems/BodySystem.cs +++ /dev/null @@ -1,7 +0,0 @@ -using Content.Shared.Body.Systems; - -namespace Content.Client.Body.Systems; - -public sealed class BodySystem : SharedBodySystem -{ -} diff --git a/Content.Client/Commands/ShowHealthBarsCommand.cs b/Content.Client/Commands/ShowHealthBarsCommand.cs index 0811f966637..6ea9d06c8c3 100644 --- a/Content.Client/Commands/ShowHealthBarsCommand.cs +++ b/Content.Client/Commands/ShowHealthBarsCommand.cs @@ -1,6 +1,8 @@ +using Content.Shared.Damage.Prototypes; using Content.Shared.Overlays; using Robust.Client.Player; using Robust.Shared.Console; +using Robust.Shared.Prototypes; using System.Linq; namespace Content.Client.Commands; @@ -34,7 +36,7 @@ public override void Execute(IConsoleShell shell, string argStr, string[] args) { var showHealthBarsComponent = new ShowHealthBarsComponent { - DamageContainers = args.ToList(), + DamageContainers = args.Select(arg => new ProtoId(arg)).ToList(), HealthStatusIcon = null, NetSyncEnabled = false }; diff --git a/Content.Client/Corvax/HyperLink/HyperLinkSystem.cs b/Content.Client/Corvax/HyperLink/HyperLinkSystem.cs deleted file mode 100644 index 48e1fa4032b..00000000000 --- a/Content.Client/Corvax/HyperLink/HyperLinkSystem.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Inspired by Nyanotrasen - -using Content.Shared.HyperLink; -using Robust.Client.UserInterface; - -namespace Content.Client.HyperLink; - -public sealed class HyperLinkSystem : EntitySystem -{ - public override void Initialize() - { - base.Initialize(); - SubscribeNetworkEvent(OnOpenURL); - } - - private void OnOpenURL(OpenURLEvent args) - { - var uriOpener = IoCManager.Resolve(); - uriOpener.OpenUri(args.URL); - } -} \ No newline at end of file diff --git a/Content.Client/Corvax/TTS/TTSSystem.cs b/Content.Client/Corvax/TTS/TTSSystem.cs index b31ba16d621..fa41aa8bc4c 100644 --- a/Content.Client/Corvax/TTS/TTSSystem.cs +++ b/Content.Client/Corvax/TTS/TTSSystem.cs @@ -22,7 +22,7 @@ public sealed class TTSSystem : EntitySystem [Dependency] private readonly AudioSystem _audio = default!; private ISawmill _sawmill = default!; - private readonly MemoryContentRoot _contentRoot = new(); + private MemoryContentRoot _contentRoot = default!; private static readonly ResPath Prefix = ResPath.Root / "TTS"; /// @@ -40,6 +40,7 @@ public sealed class TTSSystem : EntitySystem public override void Initialize() { + _contentRoot = new(); _sawmill = Logger.GetSawmill("tts"); _res.AddRoot(Prefix, _contentRoot); _cfg.OnValueChanged(CCCVars.TTSVolume, OnTtsVolumeChanged, true); diff --git a/Content.Client/Effects/ColorFlashEffectSystem.cs b/Content.Client/Effects/ColorFlashEffectSystem.cs index 956c9465244..b584aa9ad1b 100644 --- a/Content.Client/Effects/ColorFlashEffectSystem.cs +++ b/Content.Client/Effects/ColorFlashEffectSystem.cs @@ -124,6 +124,10 @@ private void OnColorFlashEffect(ColorFlashEffectEvent ev) continue; } + var targetEv = new GetFlashEffectTargetEvent(ent); + RaiseLocalEvent(ent, ref targetEv); + ent = targetEv.Target; + EnsureComp(ent, out comp); comp.NetSyncEnabled = false; comp.Color = sprite.Color; @@ -132,3 +136,9 @@ private void OnColorFlashEffect(ColorFlashEffectEvent ev) } } } + +/// +/// Raised on an entity to change the target for a color flash effect. +/// +[ByRefEvent] +public record struct GetFlashEffectTargetEvent(EntityUid Target); diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs index 68800a2afe5..18e4540e153 100644 --- a/Content.Client/Hands/Systems/HandsSystem.cs +++ b/Content.Client/Hands/Systems/HandsSystem.cs @@ -4,6 +4,7 @@ using Content.Client.Examine; using Content.Client.Strip; using Content.Client.Verbs.UI; +using Content.Shared.Body.Part; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -40,7 +41,6 @@ public sealed class HandsSystem : SharedHandsSystem public event Action? OnPlayerItemRemoved; public event Action? OnPlayerHandBlocked; public event Action? OnPlayerHandUnblocked; - public override void Initialize() { base.Initialize(); @@ -51,6 +51,8 @@ public override void Initialize() SubscribeLocalEvent(OnHandsShutdown); SubscribeLocalEvent(HandleComponentState); SubscribeLocalEvent(OnVisualsChanged); + SubscribeLocalEvent(HandleBodyPartRemoved); // _CorvaxNext: surgery + SubscribeLocalEvent(HandleBodyPartDisabled); // _CorvaxNext: surgery OnHandSetActive += OnHandActivated; } @@ -238,8 +240,40 @@ public void UIHandAltActivateItem(string handName) RaisePredictiveEvent(new RequestHandAltInteractEvent(handName)); } + #region pulling + + #endregion + #region visuals + // start-_CorvaxNext: surgery + private void HideLayers(EntityUid uid, HandsComponent component, Entity part, SpriteComponent? sprite = null) + { + if (part.Comp.PartType != BodyPartType.Hand || !Resolve(uid, ref sprite, logMissing: false)) + return; + + var location = part.Comp.Symmetry switch + { + BodyPartSymmetry.None => HandLocation.Middle, + BodyPartSymmetry.Left => HandLocation.Left, + BodyPartSymmetry.Right => HandLocation.Right, + _ => throw new ArgumentOutOfRangeException(nameof(part.Comp.Symmetry)) + }; + + if (component.RevealedLayers.TryGetValue(location, out var revealedLayers)) + { + foreach (var key in revealedLayers) + sprite.RemoveLayer(key); + + revealedLayers.Clear(); + } + } + + private void HandleBodyPartRemoved(EntityUid uid, HandsComponent component, ref BodyPartRemovedEvent args) => HideLayers(uid, component, args.Part); + + private void HandleBodyPartDisabled(EntityUid uid, HandsComponent component, ref BodyPartDisabledEvent args) => HideLayers(uid, component, args.Part); + // end-_CorvaxNext: surgery + protected override void HandleEntityInserted(EntityUid uid, HandsComponent hands, EntInsertedIntoContainerMessage args) { base.HandleEntityInserted(uid, hands, args); @@ -264,6 +298,7 @@ protected override void HandleEntityRemoved(EntityUid uid, HandsComponent hands, if (!hands.Hands.TryGetValue(args.Container.ID, out var hand)) return; + UpdateHandVisuals(uid, args.Entity, hand); _stripSys.UpdateUi(uid); diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs index baea03c8923..e2c75c4b391 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs @@ -1,6 +1,7 @@ using Content.Shared.MedicalScanner; +using Content.Shared._CorvaxNext.Targeting; using JetBrains.Annotations; -using Robust.Client.UserInterface; +using Robust.Client.GameObjects; namespace Content.Client.HealthAnalyzer.UI { @@ -17,10 +18,13 @@ public HealthAnalyzerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owne protected override void Open() { base.Open(); - - _window = this.CreateWindow(); - - _window.Title = EntMan.GetComponent(Owner).EntityName; + _window = new HealthAnalyzerWindow + { + Title = EntMan.GetComponent(Owner).EntityName, + }; + _window.OnClose += Close; + _window.OnBodyPartSelected += SendBodyPartMessage; + _window.OpenCentered(); } protected override void ReceiveMessage(BoundUserInterfaceMessage message) @@ -33,5 +37,22 @@ protected override void ReceiveMessage(BoundUserInterfaceMessage message) _window.Populate(cast); } + + private void SendBodyPartMessage(TargetBodyPart? part, EntityUid target) => SendMessage(new HealthAnalyzerPartMessage(EntMan.GetNetEntity(target), part ?? null)); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) + return; + + if (_window != null) + { + _window.OnClose -= Close; + _window.OnBodyPartSelected -= SendBodyPartMessage; + } + + _window?.Dispose(); + } } } diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml index aae8785b1fe..0a0b5ac89e7 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml @@ -1,8 +1,8 @@ - + MinWidth="350"> private readonly Dictionary _unreadMessages = new(); + // Corvax-Highlights-Start + /// + /// A list of words to be highlighted in the chatbox. + /// + private List _highlights = []; + + /// + /// The color (hex) in witch the words will be highlighted as. + /// + private string? _highlightsColor; + + private bool _autoFillHighlightsEnabled; + + /// + /// A bool to keep track if the 'CharacterUpdated' event is a new player attaching or the opening of the character info panel. + /// + private bool _charInfoIsAttach = false; + // Corvax-Highlights-End + // TODO add a cap for this for non-replays public readonly List<(GameTick Tick, ChatMessage Msg)> History = new(); @@ -172,6 +201,9 @@ private readonly Dictionary _queuedSpeechBubbl public event Action? SelectableChannelsChanged; public event Action? UnreadMessageCountsUpdated; public event Action? MessageAdded; + // Corvax-Highlights-Start + public event Action? HighlightsUpdated; + // Corvax-Highlights-End public override void Initialize() { @@ -240,6 +272,21 @@ public override void Initialize() _config.OnValueChanged(CCVars.ChatWindowOpacity, OnChatWindowOpacityChanged); + // Corvax-Highlights-Start + _config.OnValueChanged(CCCVars.ChatAutoFillHighlights, (value) => { _autoFillHighlightsEnabled = value; }); + _autoFillHighlightsEnabled = _config.GetCVar(CCCVars.ChatAutoFillHighlights); + + _config.OnValueChanged(CCCVars.ChatHighlightsColor, (value) => { _highlightsColor = value; }); + _highlightsColor = _config.GetCVar(CCCVars.ChatHighlightsColor); + + // Load highlights if any were saved. + string highlights = _config.GetCVar(CCCVars.ChatHighlights); + + if (!string.IsNullOrEmpty(highlights)) + { + UpdateHighlights(highlights); + } + // Corvax-Highlights-End } public void OnScreenLoad() @@ -257,6 +304,69 @@ public void OnScreenUnload() SetMainChat(false); } + // Corvax-Highlights-Start + public void OnSystemLoaded(CharacterInfoSystem system) + { + system.OnCharacterUpdate += CharacterUpdated; + } + + public void OnSystemUnloaded(CharacterInfoSystem system) + { + system.OnCharacterUpdate -= CharacterUpdated; + } + + private void CharacterUpdated(CharacterData data) + { + // If the _charInfoIsAttach is false then the character panel created the event, dismiss. + if (!_charInfoIsAttach) + return; + + var (_, job, _, _, entityName) = data; + + // If the character has a normal name (eg. "Name Surname" and not "Name Initial Surname" or a particular species name) + // subdivide it so that the name and surname individually get highlighted. + if (entityName.Count(c => c == ' ') == 1) + entityName = entityName.Replace(' ', '\n'); + + string newHighlights = entityName; + + // Convert the job title to kebab-case and use it as a key for the loc file. + string jobKey = job.Replace(' ', '-').ToLower(); + + if (Loc.TryGetString($"highlights-{jobKey}", out var jobMatches)) + newHighlights += '\n' + jobMatches.Replace(", ", "\n"); + + UpdateHighlights(newHighlights); + HighlightsUpdated?.Invoke(newHighlights); + _charInfoIsAttach = false; + } + + public void UpdateHighlights(string highlights) + { + // Save the newly provided list of highlighs if different. + if (!_config.GetCVar(CCCVars.ChatHighlights).Equals(highlights, StringComparison.CurrentCultureIgnoreCase)) + { + _config.SetCVar(CCCVars.ChatHighlights, highlights); + _config.SaveToFile(); + } + + // If the word is surrounded by "" we replace them with a whole-word regex tag. + highlights = highlights.Replace("\"", "\\b"); + + // Fill the array with the highlights separated by newlines, disregarding empty entries. + string[] arrHighlights = highlights.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + _highlights.Clear(); + foreach (var keyword in arrHighlights) + { + _highlights.Add(keyword); + } + + // Arrange the list in descending order so that when highlighting, + // the full word (eg. "Security") appears before the abbreviation (eg. "Sec"). + _highlights.Sort((x, y) => y.Length.CompareTo(x.Length)); + } + // Corvax-Highlights-End + private void OnChatWindowOpacityChanged(float opacity) { SetChatWindowOpacity(opacity); @@ -426,6 +536,14 @@ public void SetSpeechBubbleRoot(LayoutContainer root) private void OnAttachedChanged(EntityUid uid) { UpdateChannelPermissions(); + + // Corvax-Highlights-Start + if (_autoFillHighlightsEnabled) + { + _charInfoIsAttach = true; + _characterInfo.RequestCharacterInfo(); + } + // Corvax-Highlights-End } private void AddSpeechBubble(ChatMessage msg, SpeechBubble.SpeechType speechType) @@ -824,6 +942,14 @@ public void ProcessChatMessage(ChatMessage msg, bool speechBubble = true) msg.WrappedMessage = SharedChatSystem.InjectTagInsideTag(msg, "Name", "color", GetNameColor(SharedChatSystem.GetStringInsideTag(msg, "Name"))); } + // Corvax-Highlights-Start + // Color any words choosen by the client. + foreach (var highlight in _highlights) + { + msg.WrappedMessage = SharedChatSystem.InjectTagAroundString(msg, highlight, "color", _highlightsColor); + } + // Corvax-Highlights-End + // Color any codewords for minds that have roles that use them if (_player.LocalUser != null && _mindSystem != null && _roleCodewordSystem != null) { diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml index 459c44eee26..69cfc4218b7 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml @@ -1,10 +1,23 @@ - - - + + + + + + + diff --git a/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs new file mode 100644 index 00000000000..62504368f2b --- /dev/null +++ b/Content.Client/Xenonids/UI/XenoChoiceControl.xaml.cs @@ -0,0 +1,28 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.Graphics; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Utility; + +namespace Content.Client.Xenonids.UI; + +// start-_CorvaxNext: surgery +[GenerateTypedNameReferences] +[Virtual] +public partial class XenoChoiceControl : Control +{ + public XenoChoiceControl() => RobustXamlLoader.Load(this); + + public void Set(string name, Texture? texture) + { + NameLabel.SetMessage(name); + Texture.Texture = texture; + } + + public void Set(FormattedMessage msg, Texture? texture) + { + NameLabel.SetMessage(msg); + Texture.Texture = texture; + } +} +// end-_CorvaxNext: surgery diff --git a/Content.Client/_CorvaxNext/Body/BodySystem.cs b/Content.Client/_CorvaxNext/Body/BodySystem.cs new file mode 100644 index 00000000000..10dc057a8fd --- /dev/null +++ b/Content.Client/_CorvaxNext/Body/BodySystem.cs @@ -0,0 +1,72 @@ +using Content.Shared.Body.Systems; +using Content.Shared.Body.Part; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.GameObjects; +using Robust.Shared.Utility; +using Content.Shared.Body.Components; + +namespace Content.Client.Body.Systems; + +public sealed class BodySystem : SharedBodySystem +{ + [Dependency] private readonly MarkingManager _markingManager = default!; + + private void ApplyMarkingToPart(MarkingPrototype markingPrototype, + IReadOnlyList? colors, + bool visible, + SpriteComponent sprite) + { + for (var j = 0; j < markingPrototype.Sprites.Count; j++) + { + var markingSprite = markingPrototype.Sprites[j]; + + if (markingSprite is not SpriteSpecifier.Rsi rsi) + continue; + + var layerId = $"{markingPrototype.ID}-{rsi.RsiState}"; + + if (!sprite.LayerMapTryGet(layerId, out _)) + { + var layer = sprite.AddLayer(markingSprite, j + 1); + sprite.LayerMapSet(layerId, layer); + sprite.LayerSetSprite(layerId, rsi); + } + + sprite.LayerSetVisible(layerId, visible); + + if (!visible) + continue; + + // Okay so if the marking prototype is modified but we load old marking data this may no longer be valid + // and we need to check the index is correct. So if that happens just default to white? + if (colors != null && j < colors.Count) + sprite.LayerSetColor(layerId, colors[j]); + else + sprite.LayerSetColor(layerId, Color.White); + } + } + + protected override void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component) + { + if (!TryComp(target, out SpriteComponent? sprite)) + return; + + if (component.Color != null) + sprite.Color = component.Color.Value; + + foreach (var (visualLayer, markingList) in component.Markings) + foreach (var marking in markingList) + { + if (!_markingManager.TryGetMarking(marking, out var markingPrototype)) + continue; + + ApplyMarkingToPart(markingPrototype, marking.MarkingColors, marking.Visible, sprite); + } + } + + protected override void RemoveBodyMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance) + { + return; + } +} diff --git a/Content.Client/_CorvaxNext/Body/Components/BrainComponent.cs b/Content.Client/_CorvaxNext/Body/Components/BrainComponent.cs new file mode 100644 index 00000000000..41846ae38ff --- /dev/null +++ b/Content.Client/_CorvaxNext/Body/Components/BrainComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Client._CorvaxNext.Body.Components; + +[RegisterComponent] +public sealed partial class BrainComponent : Component; diff --git a/Content.Client/_CorvaxNext/Body/Components/LungComponent.cs b/Content.Client/_CorvaxNext/Body/Components/LungComponent.cs new file mode 100644 index 00000000000..ad5dbc8c48a --- /dev/null +++ b/Content.Client/_CorvaxNext/Body/Components/LungComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Client._CorvaxNext.Body.Components; + +[RegisterComponent] +public sealed partial class LungComponent : Component; diff --git a/Content.Client/_CorvaxNext/Body/Components/StomachComponent.cs b/Content.Client/_CorvaxNext/Body/Components/StomachComponent.cs new file mode 100644 index 00000000000..a960d4be298 --- /dev/null +++ b/Content.Client/_CorvaxNext/Body/Components/StomachComponent.cs @@ -0,0 +1,4 @@ +namespace Content.Client._CorvaxNext.Body.Components; + +[RegisterComponent] +public sealed partial class StomachComponent : Component; diff --git a/Content.Client/_CorvaxNext/Footprints/FootprintsVisualizerSystem.cs b/Content.Client/_CorvaxNext/Footprints/FootprintsVisualizerSystem.cs new file mode 100644 index 00000000000..2eed78b04ae --- /dev/null +++ b/Content.Client/_CorvaxNext/Footprints/FootprintsVisualizerSystem.cs @@ -0,0 +1,75 @@ +using Content.Shared._CorvaxNext.Footprints; +using Content.Shared._CorvaxNext.Footprints.Components; +using Robust.Client.GameObjects; +using Robust.Shared.Random; + +namespace Content.Client._CorvaxNext.Footprints; + +public sealed class FootprintsVisualizerSystem : VisualizerSystem +{ + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInitialized); + SubscribeLocalEvent(OnShutdown); + } + + private void OnInitialized(EntityUid uid, FootprintComponent comp, ComponentInit args) + { + if (!TryComp(uid, out var sprite)) + return; + + sprite.LayerMapReserveBlank(FootprintVisualLayers.Print); + UpdateAppearance(uid, comp, sprite); + } + + private void OnShutdown(EntityUid uid, FootprintComponent comp, ComponentShutdown args) + { + if (!TryComp(uid, out var sprite)) + return; + + if (!sprite.LayerMapTryGet(FootprintVisualLayers.Print, out var layer)) + return; + + sprite.RemoveLayer(layer); + } + + private void UpdateAppearance(EntityUid uid, FootprintComponent component, SpriteComponent sprite) + { + if (!sprite.LayerMapTryGet(FootprintVisualLayers.Print, out var layer)) + return; + + if (!TryComp(component.FootprintsVisualizer, out var printsComponent)) + return; + + if (!TryComp(uid, out var appearance)) + return; + + if (!_appearance.TryGetData(uid, FootprintVisualState.State, out var printVisuals, appearance)) + return; + + sprite.LayerSetState(layer, new(printVisuals switch + { + FootprintVisuals.BareFootprint => printsComponent.RightStep ? printsComponent.RightBarePrint : printsComponent.LeftBarePrint, + FootprintVisuals.ShoesPrint => printsComponent.ShoesPrint, + FootprintVisuals.SuitPrint => printsComponent.SuitPrint, + FootprintVisuals.Dragging => _random.Pick(printsComponent.DraggingPrint), + _ => throw new ArgumentOutOfRangeException($"Unknown {printVisuals} parameter.") + }), printsComponent.RsiPath); + + if (_appearance.TryGetData(uid, FootprintVisualState.Color, out var printColor, appearance)) + sprite.LayerSetColor(layer, printColor); + } + + protected override void OnAppearanceChange(EntityUid uid, FootprintComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite is null) + return; + + UpdateAppearance(uid, component, args.Sprite); + } +} diff --git a/Content.Client/_CorvaxNext/OfferItem/OfferItemIndicatorsOverlay.cs b/Content.Client/_CorvaxNext/OfferItem/OfferItemIndicatorsOverlay.cs new file mode 100644 index 00000000000..a233ec833ec --- /dev/null +++ b/Content.Client/_CorvaxNext/OfferItem/OfferItemIndicatorsOverlay.cs @@ -0,0 +1,65 @@ +using System.Numerics; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.UserInterface; +using Robust.Shared.Enums; +using Robust.Shared.Utility; + +namespace Content.Client._CorvaxNext.OfferItem; + +public sealed class OfferItemIndicatorsOverlay : Overlay +{ + private readonly IInputManager _inputManager; + private readonly IEntityManager _entMan; + private readonly IEyeManager _eye; + private readonly OfferItemSystem _offer; + + private readonly Texture _sight; + + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + + private readonly Color _mainColor = Color.White.WithAlpha(0.3f); + private readonly Color _strokeColor = Color.Black.WithAlpha(0.5f); + private readonly float _scale = 0.6f; // 1 is a little big + + public OfferItemIndicatorsOverlay(IInputManager input, IEntityManager entMan, IEyeManager eye, OfferItemSystem offerSys) + { + _inputManager = input; + _entMan = entMan; + _eye = eye; + _offer = offerSys; + + var spriteSys = _entMan.EntitySysManager.GetEntitySystem(); + _sight = spriteSys.Frame0(new SpriteSpecifier.Rsi(new ResPath("/Textures/_CorvaxNext/Misc/give_item.rsi"), "give_item")); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + return _offer.IsInOfferMode() && base.BeforeDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var mouseScreenPosition = _inputManager.MouseScreenPosition; + var mousePosMap = _eye.PixelToMap(mouseScreenPosition); + + if (mousePosMap.MapId != args.MapId) + return; + + var mousePos = mouseScreenPosition.Position; + var uiScale = (args.ViewportControl as Control)?.UIScale ?? 1f; + var limitedScale = Math.Min(1.25f, uiScale); + + DrawSight(_sight, args.ScreenHandle, mousePos, limitedScale * _scale); + } + + private void DrawSight(Texture sight, DrawingHandleScreen screen, Vector2 centerPos, float scale) + { + var sightSize = sight.Size * scale; + var expandedSize = sightSize + new Vector2(7); + + screen.DrawTextureRect(sight, UIBox2.FromDimensions(centerPos - sightSize * 0.5f, sightSize), _strokeColor); + screen.DrawTextureRect(sight, UIBox2.FromDimensions(centerPos - expandedSize * 0.5f, expandedSize), _mainColor); + } +} diff --git a/Content.Client/_CorvaxNext/OfferItem/OfferItemSystem.cs b/Content.Client/_CorvaxNext/OfferItem/OfferItemSystem.cs new file mode 100644 index 00000000000..949ce1addbc --- /dev/null +++ b/Content.Client/_CorvaxNext/OfferItem/OfferItemSystem.cs @@ -0,0 +1,44 @@ +using Content.Shared._CorvaxNext.OfferItem; +using Content.Shared._CorvaxNext.NextVars; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; +using Robust.Shared.Configuration; + +namespace Content.Client._CorvaxNext.OfferItem; + +public sealed class OfferItemSystem : SharedOfferItemSystem +{ + [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IEyeManager _eye = default!; + + public override void Initialize() + { + base.Initialize(); + Subs.CVar(_cfg, NextVars.OfferModeIndicatorsPointShow, OnShowOfferIndicatorsChanged, true); + } + + public override void Shutdown() + { + _overlayManager.RemoveOverlay(); + base.Shutdown(); + } + + public bool IsInOfferMode() + { + var entity = _playerManager.LocalEntity; + + return entity is not null && IsInOfferMode(entity.Value); + } + + private void OnShowOfferIndicatorsChanged(bool isShow) + { + if (isShow) + _overlayManager.AddOverlay(new OfferItemIndicatorsOverlay(_inputManager, EntityManager, _eye, this)); + else + _overlayManager.RemoveOverlay(); + } +} diff --git a/Content.Client/_CorvaxNext/Options/UI/OptionColorSlider.xaml b/Content.Client/_CorvaxNext/Options/UI/OptionColorSlider.xaml new file mode 100644 index 00000000000..4f5f0823506 --- /dev/null +++ b/Content.Client/_CorvaxNext/Options/UI/OptionColorSlider.xaml @@ -0,0 +1,7 @@ + + + + diff --git a/Content.Client/_CorvaxNext/Options/UI/OptionColorSlider.xaml.cs b/Content.Client/_CorvaxNext/Options/UI/OptionColorSlider.xaml.cs new file mode 100644 index 00000000000..86356565377 --- /dev/null +++ b/Content.Client/_CorvaxNext/Options/UI/OptionColorSlider.xaml.cs @@ -0,0 +1,30 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; + +namespace Content.Client._CorvaxNext.Options.UI; + +/// +/// Standard UI control used for color sliders in the options menu. Intended for use with . +/// +/// +[GenerateTypedNameReferences] +public sealed partial class OptionColorSlider : Control +{ + /// + /// The text describing what this slider affects. + /// + public string? Title + { + get => TitleLabel.Text; + set => TitleLabel.Text = value; + } + + /// + /// The example text showing the current color of the slider. + /// + public string? Example + { + get => ExampleLabel.Text; + set => ExampleLabel.Text = value; + } +} diff --git a/Content.Client/_CorvaxNext/Surgery/SurgeryBui.cs b/Content.Client/_CorvaxNext/Surgery/SurgeryBui.cs new file mode 100644 index 00000000000..c851ce5162e --- /dev/null +++ b/Content.Client/_CorvaxNext/Surgery/SurgeryBui.cs @@ -0,0 +1,360 @@ +using Content.Client.Xenonids.UI; +using Content.Client.Administration.UI.CustomControls; +using Content.Shared._CorvaxNext.Surgery; +using Content.Shared.Body.Components; +using Content.Shared.Body.Part; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client._CorvaxNext.Surgery; + +[UsedImplicitly] +public sealed class SurgeryBui : BoundUserInterface +{ + [Dependency] private readonly IEntityManager _entities = default!; + [Dependency] private readonly IPlayerManager _player = default!; + + private readonly SurgerySystem _system; + [ViewVariables] + private SurgeryWindow? _window; + private EntityUid? _part; + private bool _isBody; + private (EntityUid Ent, EntProtoId Proto)? _surgery; + private readonly List _previousSurgeries = new(); + public SurgeryBui(EntityUid owner, Enum uiKey) : base(owner, uiKey) => _system = _entities.System(); + + protected override void ReceiveMessage(BoundUserInterfaceMessage message) + { + if (_window is null + || message is not SurgeryBuiRefreshMessage) + return; + + RefreshUI(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + if (state is not SurgeryBuiState s) + return; + + Update(s); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + _window?.Dispose(); + } + + private void Update(SurgeryBuiState state) + { + if (!_entities.TryGetComponent(_player.LocalEntity, out SurgeryTargetComponent? surgeryTargetComp) + || !surgeryTargetComp.CanOperate) + return; + if (_window == null) + { + _window = new SurgeryWindow(); + _window.OnClose += Close; + _window.Title = Loc.GetString("surgery-ui-window-title"); + + _window.PartsButton.OnPressed += _ => + { + _part = null; + _isBody = false; + _surgery = null; + _previousSurgeries.Clear(); + View(ViewType.Parts); + }; + + _window.SurgeriesButton.OnPressed += _ => + { + _surgery = null; + _previousSurgeries.Clear(); + + if (!_entities.TryGetNetEntity(_part, out var netPart) + || State is not SurgeryBuiState s + || !s.Choices.TryGetValue(netPart.Value, out var surgeries)) + return; + + OnPartPressed(netPart.Value, surgeries); + }; + + _window.StepsButton.OnPressed += _ => + { + if (!_entities.TryGetNetEntity(_part, out var netPart) + || _previousSurgeries.Count == 0) + return; + + var last = _previousSurgeries[^1]; + _previousSurgeries.RemoveAt(_previousSurgeries.Count - 1); + + if (_system.GetSingleton(last) is not { } previousId + || !_entities.TryGetComponent(previousId, out SurgeryComponent? previous)) + return; + + OnSurgeryPressed((previousId, previous), netPart.Value, last); + }; + } + + _window.Surgeries.DisposeAllChildren(); + _window.Steps.DisposeAllChildren(); + _window.Parts.DisposeAllChildren(); + View(ViewType.Parts); + + var oldSurgery = _surgery; + var oldPart = _part; + _part = null; + _surgery = null; + + var options = new List<(NetEntity netEntity, EntityUid entity, string Name, BodyPartType? PartType)>(); + foreach (var choice in state.Choices.Keys) + if (_entities.TryGetEntity(choice, out var ent)) + { + if (_entities.TryGetComponent(ent, out BodyPartComponent? part)) + options.Add((choice, ent.Value, _entities.GetComponent(ent.Value).EntityName, part.PartType)); + else if (_entities.TryGetComponent(ent, out BodyComponent? body)) + options.Add((choice, ent.Value, _entities.GetComponent(ent.Value).EntityName, null)); + } + + options.Sort((a, b) => + { + int GetScore(BodyPartType? partType) + { + return partType switch + { + BodyPartType.Head => 1, + BodyPartType.Torso => 2, + BodyPartType.Arm => 3, + BodyPartType.Hand => 4, + BodyPartType.Leg => 5, + BodyPartType.Foot => 6, + // BodyPartType.Tail => 7, No tails yet! + BodyPartType.Other => 8, + _ => 9 + }; + } + + return GetScore(a.PartType) - GetScore(b.PartType); + }); + + foreach (var (netEntity, entity, partName, _) in options) + { + //var netPart = _entities.GetNetEntity(part.Owner); + var surgeries = state.Choices[netEntity]; + var partButton = new XenoChoiceControl(); + + partButton.Set(partName, null); + partButton.Button.OnPressed += _ => OnPartPressed(netEntity, surgeries); + + _window.Parts.AddChild(partButton); + + foreach (var surgeryId in surgeries) + { + if (_system.GetSingleton(surgeryId) is not { } surgery || + !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp)) + continue; + + if (oldPart == entity && oldSurgery?.Proto == surgeryId) + OnSurgeryPressed((surgery, surgeryComp), netEntity, surgeryId); + } + + if (oldPart == entity && oldSurgery == null) + OnPartPressed(netEntity, surgeries); + } + + + if (!_window.IsOpen) + { + //Logger.Debug("Attempting to open"); + _window.OpenCentered(); + } + } + + private void AddStep(EntProtoId stepId, NetEntity netPart, EntProtoId surgeryId) + { + if (_window == null + || _system.GetSingleton(stepId) is not { } step) + return; + + var stepName = new FormattedMessage(); + stepName.AddText(_entities.GetComponent(step).EntityName); + var stepButton = new SurgeryStepButton { Step = step }; + stepButton.Button.OnPressed += _ => SendMessage(new SurgeryStepChosenBuiMsg(netPart, surgeryId, stepId, _isBody)); + + _window.Steps.AddChild(stepButton); + } + + private void OnSurgeryPressed(Entity surgery, NetEntity netPart, EntProtoId surgeryId) + { + if (_window == null) + return; + + _part = _entities.GetEntity(netPart); + _isBody = _entities.HasComponent(_part); + _surgery = (surgery, surgeryId); + + _window.Steps.DisposeAllChildren(); + + // This apparently does not consider if theres multiple surgery requirements in one surgery. Maybe thats fine. + if (surgery.Comp.Requirement is { } requirementId && _system.GetSingleton(requirementId) is { } requirement) + { + var label = new XenoChoiceControl(); + label.Button.OnPressed += _ => + { + _previousSurgeries.Add(surgeryId); + + if (_entities.TryGetComponent(requirement, out SurgeryComponent? requirementComp)) + OnSurgeryPressed((requirement, requirementComp), netPart, requirementId); + }; + + var msg = new FormattedMessage(); + var surgeryName = _entities.GetComponent(requirement).EntityName; + msg.AddMarkup($"[bold]{Loc.GetString("surgery-ui-window-require")}: {surgeryName}[/bold]"); + label.Set(msg, null); + + _window.Steps.AddChild(label); + _window.Steps.AddChild(new HSeparator { Margin = new Thickness(0, 0, 0, 1) }); + } + foreach (var stepId in surgery.Comp.Steps) + AddStep(stepId, netPart, surgeryId); + + View(ViewType.Steps); + RefreshUI(); + } + + private void OnPartPressed(NetEntity netPart, List surgeryIds) + { + if (_window == null) + return; + + _part = _entities.GetEntity(netPart); + _isBody = _entities.HasComponent(_part); + _window.Surgeries.DisposeAllChildren(); + + var surgeries = new List<(Entity Ent, EntProtoId Id, string Name)>(); + foreach (var surgeryId in surgeryIds) + { + if (_system.GetSingleton(surgeryId) is not { } surgery || + !_entities.TryGetComponent(surgery, out SurgeryComponent? surgeryComp)) + { + continue; + } + + var name = _entities.GetComponent(surgery).EntityName; + surgeries.Add(((surgery, surgeryComp), surgeryId, name)); + } + + surgeries.Sort((a, b) => + { + var priority = a.Ent.Comp.Priority.CompareTo(b.Ent.Comp.Priority); + if (priority != 0) + return priority; + + return string.Compare(a.Name, b.Name, StringComparison.Ordinal); + }); + + foreach (var surgery in surgeries) + { + var surgeryButton = new XenoChoiceControl(); + surgeryButton.Set(surgery.Name, null); + + surgeryButton.Button.OnPressed += _ => OnSurgeryPressed(surgery.Ent, netPart, surgery.Id); + _window.Surgeries.AddChild(surgeryButton); + } + + RefreshUI(); + View(ViewType.Surgeries); + } + + private void RefreshUI() + { + if (_window == null + || !_window.IsOpen + || _part == null + || !_entities.HasComponent(_surgery?.Ent) + || !_entities.TryGetComponent(_player.LocalEntity ?? EntityUid.Invalid, out SurgeryTargetComponent? surgeryComp) + || !surgeryComp.CanOperate) + return; + + var next = _system.GetNextStep(Owner, _part.Value, _surgery.Value.Ent); + var i = 0; + foreach (var child in _window.Steps.Children) + { + if (child is not SurgeryStepButton stepButton) + continue; + + var status = StepStatus.Incomplete; + if (next == null) + status = StepStatus.Complete; + else if (next.Value.Surgery.Owner != _surgery.Value.Ent) + status = StepStatus.Incomplete; + else if (next.Value.Step == i) + status = StepStatus.Next; + else if (i < next.Value.Step) + status = StepStatus.Complete; + + stepButton.Button.Disabled = status != StepStatus.Next; + + var stepName = new FormattedMessage(); + stepName.AddText(_entities.GetComponent(stepButton.Step).EntityName); + + if (status == StepStatus.Complete) + stepButton.Button.Modulate = Color.Green; + else + { + stepButton.Button.Modulate = Color.White; + if (_player.LocalEntity is { } player + && status == StepStatus.Next + && !_system.CanPerformStep(player, Owner, _part.Value, stepButton.Step, false, out var popup, out var reason, out _)) + stepButton.ToolTip = popup; + } + + var texture = _entities.GetComponentOrNull(stepButton.Step)?.Icon?.Default; + stepButton.Set(stepName, texture); + i++; + } + } + + private void View(ViewType type) + { + if (_window == null) + return; + + _window.PartsButton.Parent!.Margin = new Thickness(0, 0, 0, 10); + + _window.Parts.Visible = type == ViewType.Parts; + _window.PartsButton.Disabled = type == ViewType.Parts; + + _window.Surgeries.Visible = type == ViewType.Surgeries; + _window.SurgeriesButton.Disabled = type != ViewType.Steps; + + _window.Steps.Visible = type == ViewType.Steps; + _window.StepsButton.Disabled = type != ViewType.Steps || _previousSurgeries.Count == 0; + + if (_entities.TryGetComponent(_part, out MetaDataComponent? partMeta) && + _entities.TryGetComponent(_surgery?.Ent, out MetaDataComponent? surgeryMeta)) + _window.Title = $"Surgery - {partMeta.EntityName}, {surgeryMeta.EntityName}"; + else if (partMeta != null) + _window.Title = $"Surgery - {partMeta.EntityName}"; + else + _window.Title = "Surgery"; + } + + private enum ViewType + { + Parts, + Surgeries, + Steps + } + + private enum StepStatus + { + Next, + Complete, + Incomplete + } +} diff --git a/Content.Client/_CorvaxNext/Surgery/SurgeryStepButton.xaml b/Content.Client/_CorvaxNext/Surgery/SurgeryStepButton.xaml new file mode 100644 index 00000000000..d7878fb6572 --- /dev/null +++ b/Content.Client/_CorvaxNext/Surgery/SurgeryStepButton.xaml @@ -0,0 +1,4 @@ + + diff --git a/Content.Client/_CorvaxNext/Surgery/SurgeryStepButton.xaml.cs b/Content.Client/_CorvaxNext/Surgery/SurgeryStepButton.xaml.cs new file mode 100644 index 00000000000..c4b90e6e154 --- /dev/null +++ b/Content.Client/_CorvaxNext/Surgery/SurgeryStepButton.xaml.cs @@ -0,0 +1,16 @@ +using Content.Client.Xenonids.UI; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._CorvaxNext.Surgery; + +[GenerateTypedNameReferences] +public sealed partial class SurgeryStepButton : XenoChoiceControl +{ + public EntityUid Step { get; set; } + + public SurgeryStepButton() + { + RobustXamlLoader.Load(this); + } +} diff --git a/Content.Client/_CorvaxNext/Surgery/SurgerySystem.cs b/Content.Client/_CorvaxNext/Surgery/SurgerySystem.cs new file mode 100644 index 00000000000..bc4c51dc519 --- /dev/null +++ b/Content.Client/_CorvaxNext/Surgery/SurgerySystem.cs @@ -0,0 +1,11 @@ +using Content.Shared._CorvaxNext.Surgery; + +namespace Content.Client._CorvaxNext.Surgery; + +public sealed class SurgerySystem : SharedSurgerySystem +{ + public override void Initialize() + { + base.Initialize(); + } +} diff --git a/Content.Client/_CorvaxNext/Surgery/SurgeryWindow.xaml b/Content.Client/_CorvaxNext/Surgery/SurgeryWindow.xaml new file mode 100644 index 00000000000..e3842228773 --- /dev/null +++ b/Content.Client/_CorvaxNext/Surgery/SurgeryWindow.xaml @@ -0,0 +1,23 @@ + + + +