diff --git a/Content.Client/Guidebook/DocumentParsingManager.cs b/Content.Client/Guidebook/DocumentParsingManager.cs index 857ae552024..6201ddef365 100644 --- a/Content.Client/Guidebook/DocumentParsingManager.cs +++ b/Content.Client/Guidebook/DocumentParsingManager.cs @@ -36,7 +36,7 @@ public void Initialize() .Assert(_tagControlParsers.ContainsKey, tag => $"unknown tag: {tag}") .Bind(tag => _tagControlParsers[tag]); - _controlParser = OneOf(_tagParser, TryHeaderControl, ListControlParser, TextControlParser) + _controlParser = OneOf(_tagParser, TryHeaderControl, TryListControl, TextControlParser) // Frontier: ListControlParser()) diff --git a/Content.Client/Guidebook/DocumentParsingManager.static.cs b/Content.Client/Guidebook/DocumentParsingManager.static.cs index 5d25d8f6452..87f5f30bac5 100644 --- a/Content.Client/Guidebook/DocumentParsingManager.static.cs +++ b/Content.Client/Guidebook/DocumentParsingManager.static.cs @@ -14,6 +14,7 @@ namespace Content.Client.Guidebook; public sealed partial class DocumentParsingManager { private const string ListBullet = " › "; + private const string SublistBullet = " • "; // Frontier // Parser that consumes a - and then just parses normal rich text with some prefix text (a bullet point). private static readonly Parser TryEscapedChar = Try(Char('\\') @@ -123,6 +124,22 @@ public sealed partial class DocumentParsingManager .Cast()) .Labelled("list"); + // Frontier: sublists - should duplicate ListControlParser but for more hyphens, and print out more spaces before your list character + private static readonly Parser SublistControlParser = Try(String("--")) + .Then(SkipWhitespaces) + .Then(Map( + control => new BoxContainer + { + Children = { new Label { Text = SublistBullet, VerticalAlignment = VAlignment.Top }, control }, + Orientation = LayoutOrientation.Horizontal + }, + TextControlParser) + .Cast()) + .Labelled("sublist"); + + private static readonly Parser TryListControl = OneOf(SublistControlParser, ListControlParser); + // End Frontier: sublists + #region Text Parsing #region Basic Text Parsing diff --git a/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml.cs b/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml.cs index e76c51d55a9..43f9cc1f02e 100644 --- a/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml.cs +++ b/Content.Client/Salvage/UI/SalvageExpeditionWindow.xaml.cs @@ -3,6 +3,7 @@ using Content.Client.Stylesheets; using Content.Client.UserInterface.Controls; using Content.Shared.CCVar; +using Content.Shared._NF.CCVar; // Frontier using Content.Shared.Parallax.Biomes; using Content.Shared.Salvage; using Content.Shared.Salvage.Expeditions; @@ -303,7 +304,7 @@ protected override void FrameUpdate(FrameEventArgs args) else { var cooldown = _cooldown - ? TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.SalvageExpeditionFailedCooldown)) + ? TimeSpan.FromSeconds(_cfgManager.GetCVar(NFCCVars.SalvageExpeditionFailedCooldown)) : TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.SalvageExpeditionCooldown)); NextOfferBar.Value = 1f - (float) (remaining / cooldown); diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalComposition.xaml b/Content.Client/_NF/Guidebook/Controls/GuideMedicalComposition.xaml new file mode 100644 index 00000000000..87b755d6f1a --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalComposition.xaml @@ -0,0 +1,21 @@ + + + + + + diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalComposition.xaml.cs b/Content.Client/_NF/Guidebook/Controls/GuideMedicalComposition.xaml.cs new file mode 100644 index 00000000000..fd44847e355 --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalComposition.xaml.cs @@ -0,0 +1,32 @@ +using Content.Client.Guidebook.Controls; +using Content.Client.UserInterface.ControlExtensions; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._NF.Guidebook.Controls; // Frontier: add _EE + +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideMedicalComposition : BoxContainer, ISearchableControl +{ + public GuideMedicalComposition(ReagentPrototype proto, FixedPoint2 quantity) + { + RobustXamlLoader.Load(this); + + ReagentLabel.Text = proto.LocalizedName; + AmountLabel.Text = quantity.ToString(); + } + + public bool CheckMatchesSearch(string query) + { + return this.ChildrenContainText(query); + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } +} diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalDamage.xaml b/Content.Client/_NF/Guidebook/Controls/GuideMedicalDamage.xaml new file mode 100644 index 00000000000..d8ef86c56e1 --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalDamage.xaml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalDamage.xaml.cs b/Content.Client/_NF/Guidebook/Controls/GuideMedicalDamage.xaml.cs new file mode 100644 index 00000000000..8f8dcefa64a --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalDamage.xaml.cs @@ -0,0 +1,40 @@ +using Content.Client.Guidebook.Controls; +using Content.Client.UserInterface.ControlExtensions; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._NF.Guidebook.Controls; + +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideMedicalDamage : BoxContainer, ISearchableControl +{ + public GuideMedicalDamage(DamageTypePrototype proto, FixedPoint2 quantity) + { + RobustXamlLoader.Load(this); + + DamageLabel.Text = proto.LocalizedName; + AmountLabel.Text = quantity.ToString(); + } + + public GuideMedicalDamage(DamageGroupPrototype proto, FixedPoint2 quantity) + { + RobustXamlLoader.Load(this); + + DamageLabel.Text = Loc.GetString("guidebook-medical-damage-group", ("name", proto.LocalizedName)); + AmountLabel.Text = quantity.ToString(); + } + + public bool CheckMatchesSearch(string query) + { + return this.ChildrenContainText(query); + } + + public void SetHiddenState(bool state, string query) + { + Visible = CheckMatchesSearch(query) ? state : !state; + } +} diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalEmbed.xaml b/Content.Client/_NF/Guidebook/Controls/GuideMedicalEmbed.xaml new file mode 100644 index 00000000000..a6f64498269 --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalEmbed.xaml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalEmbed.xaml.cs b/Content.Client/_NF/Guidebook/Controls/GuideMedicalEmbed.xaml.cs new file mode 100644 index 00000000000..2016deb4200 --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalEmbed.xaml.cs @@ -0,0 +1,188 @@ +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; + } +} diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalGroupEmbed.xaml b/Content.Client/_NF/Guidebook/Controls/GuideMedicalGroupEmbed.xaml new file mode 100644 index 00000000000..6551f2cf18c --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalGroupEmbed.xaml @@ -0,0 +1,4 @@ + + + diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalGroupEmbed.xaml.cs b/Content.Client/_NF/Guidebook/Controls/GuideMedicalGroupEmbed.xaml.cs new file mode 100644 index 00000000000..134e51ff544 --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalGroupEmbed.xaml.cs @@ -0,0 +1,36 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Client._NF.Medical.EntitySystems; +using Content.Client.Guidebook.Richtext; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client._NF.Guidebook.Controls; + +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideMedicalGroupEmbed : BoxContainer, IDocumentTag +{ + [Dependency] private readonly IEntitySystemManager _sysMan = default!; + + public GuideMedicalGroupEmbed() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + MouseFilter = MouseFilterMode.Stop; + + foreach (var data in _sysMan.GetEntitySystem().Registry.OrderBy(it => it.Identifier)) + { + var embed = new GuideMedicalEmbed(data); + GroupContainer.AddChild(embed); + } + } + + public bool TryParseTag(Dictionary args, [NotNullWhen(true)] out Control? control) + { + control = this; + return true; + } +} diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalSource.xaml b/Content.Client/_NF/Guidebook/Controls/GuideMedicalSource.xaml new file mode 100644 index 00000000000..4527225ee4c --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalSource.xaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + diff --git a/Content.Client/_NF/Guidebook/Controls/GuideMedicalSource.xaml.cs b/Content.Client/_NF/Guidebook/Controls/GuideMedicalSource.xaml.cs new file mode 100644 index 00000000000..eb8954af8b5 --- /dev/null +++ b/Content.Client/_NF/Guidebook/Controls/GuideMedicalSource.xaml.cs @@ -0,0 +1,96 @@ +using System.Linq; +using Content.Client.Chemistry.EntitySystems; +using Content.Client.Guidebook.Controls; +using Content.Client.UserInterface.ControlExtensions; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.FixedPoint; +using Content.Shared.Kitchen; +using JetBrains.Annotations; +using Robust.Client.AutoGenerated; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Client._NF.Guidebook.Controls; + +[UsedImplicitly, GenerateTypedNameReferences] +public sealed partial class GuideMedicalSource : BoxContainer, ISearchableControl +{ + private readonly IPrototypeManager _protoMan; + private readonly SpriteSystem _sprites = default!; + + public GuideMedicalSource(IPrototypeManager protoMan) + { + RobustXamlLoader.Load(this); + _protoMan = protoMan; + _sprites = IoCManager.Resolve().GetEntitySystem(); + } + + public GuideMedicalSource(EntityPrototype result, MedicalRecipeData entry, IPrototypeManager protoMan) : this(protoMan) + { + GenerateControl(entry); + + GenerateOutputs(result, entry); + } + + private void GenerateControl(MedicalRecipeData entry) + { + if (!_protoMan.TryIndex(entry.Recipe, out var recipe)) + { + SourceLabel.Text = Loc.GetString("guidebook-food-unknown-proto", ("id", entry.Result)); // Frontier: SetMessage _protoMan.TryIndex(it.Key, out var proto) ? FormatIngredient(proto, it.Value) : "") + .Where(it => it.Length > 0); + var combinedLiquids = recipe.IngredientsReagents + .Select(it => _protoMan.TryIndex(it.Key, out var proto) ? FormatIngredient(proto, it.Value) : "") + .Where(it => it.Length > 0); + + var combinedIngredients = string.Join("\n", combinedLiquids.Union(combinedSolids)); + SourceLabel.Text = Loc.GetString("guidebook-food-processing-recipe", ("ingredients", combinedIngredients)); // Frontier: SetMessage(OnReceiveRegistryUpdate); + } + + private void OnReceiveRegistryUpdate(MedicalGuideRegistryChangedEvent message) + { + Registry = message.Changeset; + } + + public bool TryGetData(EntProtoId result, out MedicalGuideEntry entry) + { + var index = Registry.FindIndex(it => it.Result == result); + if (index == -1) + { + entry = default; + return false; + } + + entry = Registry[index]; + return true; + } +} diff --git a/Content.Client/_NF/Medical/UI/MedicalBountyRedemptionMenu.xaml b/Content.Client/_NF/Medical/UI/MedicalBountyRedemptionMenu.xaml new file mode 100644 index 00000000000..7c17ae9b8d0 --- /dev/null +++ b/Content.Client/_NF/Medical/UI/MedicalBountyRedemptionMenu.xaml @@ -0,0 +1,36 @@ + + + +