using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Client._NF.Medical.EntitySystems; using Content.Client.Chemistry.EntitySystems; using Content.Client.Guidebook.Controls; using Content.Client.Guidebook.Richtext; using Content.Client.Message; using Content.Shared.Chemistry.Reagent; using Content.Shared.Damage.Prototypes; using Content.Shared.FixedPoint; using JetBrains.Annotations; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Client._NF.Guidebook.Controls; /// /// Control for embedding a medical recipe into a guidebook. /// [UsedImplicitly, GenerateTypedNameReferences] public sealed partial class GuideMedicalEmbed : BoxContainer, IDocumentTag, ISearchableControl { [Dependency] private readonly IEntitySystemManager _systemManager = default!; [Dependency] private readonly IPrototypeManager _prototype = default!; private readonly MedicalGuideDataSystem _medicalGuideData; private readonly ISawmill _logger = default!; public GuideMedicalEmbed() { RobustXamlLoader.Load(this); IoCManager.InjectDependencies(this); _medicalGuideData = _systemManager.GetEntitySystem(); _logger = Logger.GetSawmill("medical guide"); MouseFilter = MouseFilterMode.Stop; } public GuideMedicalEmbed(MedicalGuideEntry entry) : this() { GenerateControl(entry); } public bool CheckMatchesSearch(string query) { return ResultName.GetMessage()?.Contains(query, StringComparison.InvariantCultureIgnoreCase) == true || Description.GetMessage()?.Contains(query, StringComparison.InvariantCultureIgnoreCase) == true; } public void SetHiddenState(bool state, string query) { Visible = CheckMatchesSearch(query) ? state : !state; } public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) { control = null; if (!args.TryGetValue("Result", out var id)) { _logger.Error("Result embed tag is missing food prototype argument."); return false; } if (!_medicalGuideData.TryGetData(id, out var data)) { _logger.Warning($"Specified result prototype \"{id}\" does not have any known sources."); return false; } GenerateControl(data); control = this; return true; } private void GenerateControl(MedicalGuideEntry data) { _prototype.TryIndex(data.Result, out var proto); if (proto == null) { ResultName.SetMarkup(Loc.GetString("guidebook-food-unknown-proto", ("id", data.Result))); return; } var composition = data.Composition .Select(it => _prototype.TryIndex(it.Reagent.Prototype, out var reagent) ? (reagent, it.Quantity) : (null, 0)) .Where(it => it.reagent is not null) .Cast<(ReagentPrototype, FixedPoint2)>() .ToList(); #region Colors CalculateColors(composition, out var textColor, out var backgroundColor); NameBackground.PanelOverride = new StyleBoxFlat { BackgroundColor = backgroundColor }; ResultName.SetMarkup(Loc.GetString("guidebook-food-name", ("color", textColor), ("name", proto.Name))); #endregion #region Recipes if (data.Recipes.Length > 0) RecipesContainer.Visible = true; foreach (var recipe in data.Recipes.OrderBy(it => it.OutputCount)) { var control = new GuideMedicalSource(proto, recipe, _prototype); RecipesDescriptionContainer.AddChild(control); } #endregion #region Composition if (composition.Count > 0) CompositionContainer.Visible = true; foreach (var (reagent, quantity) in composition) { var control = new GuideMedicalComposition(reagent, quantity); CompositionDescriptionContainer.AddChild(control); } #endregion #region Damage var damageDict = data.Healing?.DamageDict ?? new(); if (damageDict.Count > 0) DamageContainer.Visible = true; foreach (var (damageType, damage) in damageDict) { if (_prototype.TryIndex(damageType, out var damageProto)) { var control = new GuideMedicalDamage(damageProto, -damage); // Negative damage means positive healing DamageDescriptionContainer.AddChild(control); } else if (_prototype.TryIndex(damageType, out var groupProto)) { var control = new GuideMedicalDamage(groupProto, -damage); // Negative damage means positive healing DamageDescriptionContainer.AddChild(control); } } #endregion FormattedMessage description = new(); description.AddText(proto?.Description ?? string.Empty); // Cannot describe food flavor or smth beause food is entirely server-side Description.SetMessage(description); } private void CalculateColors(List<(ReagentPrototype, FixedPoint2)> composition, out Color text, out Color background) { // Background color is calculated as the weighted average of the colors of the composition. // Text color is determined based on background luminosity. float r = 0, g = 0, b = 0; FixedPoint2 weight = 0; foreach (var (proto, quantity) in composition) { var tcolor = proto.SubstanceColor; var prevalence = quantity <= 0 ? 0f : weight == 0f ? 1f : (quantity / (weight + quantity)).Float(); r = r * (1 - prevalence) + tcolor.R * prevalence; g = g * (1 - prevalence) + tcolor.G * prevalence; b = b * (1 - prevalence) + tcolor.B * prevalence; if (quantity > 0) weight += quantity; } // Copied from GuideReagentEmbed which was probably copied from stackoverflow. This is the formula for color luminosity. var lum = 0.2126f * r + 0.7152f * g + 0.0722f; background = new Color(r, g, b); text = lum > 0.5f ? Color.Black : Color.White; } }