Chem guidebook (#17123)

* im good at atomizing. welcome to half-finished chem guides.

* wagh

* e

* save work

* aa

* woweee UI

* finishing the last of it

* don't actually update the engine :(

---------

Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
This commit is contained in:
Nemanja
2023-06-04 16:45:02 -04:00
committed by GitHub
parent 1e6dbd0b67
commit b9fb66f005
72 changed files with 1411 additions and 12 deletions

View File

@@ -7,5 +7,8 @@ namespace Content.Shared.Body.Prototypes
{
[IdDataField]
public string ID { get; } = default!;
[DataField("name", required: true)]
public string Name { get; } = default!;
}
}

View File

@@ -1,8 +1,11 @@
using System.Text.Json.Serialization;
using System.Linq;
using System.Text.Json.Serialization;
using Content.Shared.Chemistry.Components;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Localizations;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Shared.Chemistry.Reagent
@@ -23,6 +26,10 @@ namespace Content.Shared.Chemistry.Reagent
[DataField("conditions")]
public ReagentEffectCondition[]? Conditions;
public virtual string ReagentEffectFormat => "guidebook-reagent-effect-description";
protected abstract string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys); // => Loc.GetString("reagent-effect-guidebook-missing", ("chance", Probability));
/// <summary>
/// What's the chance, from 0 to 1, that this effect will occur?
/// </summary>
@@ -42,6 +49,23 @@ namespace Content.Shared.Chemistry.Reagent
public virtual bool ShouldLog { get; } = false;
public abstract void Effect(ReagentEffectArgs args);
/// <summary>
/// Produces a localized, bbcode'd guidebook description for this effect.
/// </summary>
/// <returns></returns>
public string? GuidebookEffectDescription(IPrototypeManager prototype, IEntitySystemManager entSys)
{
var effect = ReagentEffectGuidebookText(prototype, entSys);
if (effect is null)
return null;
return Loc.GetString(ReagentEffectFormat, ("effect", effect), ("chance", Probability),
("conditionCount", Conditions?.Length ?? 0),
("conditions",
ContentLocalizationManager.FormatList(Conditions?.Select(x => x.GuidebookExplanation(prototype)).ToList() ??
new List<string>())));
}
}
public static class ReagentEffectExt

View File

@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Shared.Chemistry.Reagent
{
@@ -10,5 +11,12 @@ namespace Content.Shared.Chemistry.Reagent
[JsonPropertyName("id")] private protected string _id => this.GetType().Name;
public abstract bool Condition(ReagentEffectArgs args);
/// <summary>
/// Effect explanations are of the form "[chance to] [action] when [condition] and [condition]"
/// </summary>
/// <param name="prototype"></param>
/// <returns></returns>
public abstract string GuidebookExplanation(IPrototypeManager prototype);
}
}

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using System.Linq;
using System.Text.Json.Serialization;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Components;
@@ -9,6 +10,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
@@ -157,6 +159,23 @@ namespace Content.Shared.Chemistry.Reagent
}
}
[Serializable, NetSerializable]
public struct ReagentGuideEntry
{
public string ReagentPrototype;
public Dictionary<string, ReagentEffectsGuideEntry>? GuideEntries;
public ReagentGuideEntry(ReagentPrototype proto, IPrototypeManager prototype, IEntitySystemManager entSys)
{
ReagentPrototype = proto.ID;
GuideEntries = proto.Metabolisms?
.Select(x => (x.Key, x.Value.MakeGuideEntry(prototype, entSys)))
.ToDictionary(x => x.Key, x => x.Item2);
}
}
[DataDefinition]
public sealed class ReagentEffectsEntry
{
@@ -173,6 +192,30 @@ namespace Content.Shared.Chemistry.Reagent
[JsonPropertyName("effects")]
[DataField("effects", required: true)]
public ReagentEffect[] Effects = default!;
public ReagentEffectsGuideEntry MakeGuideEntry(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return new ReagentEffectsGuideEntry(MetabolismRate,
Effects
.Select(x => x.GuidebookEffectDescription(prototype, entSys)) // hate.
.Where(x => x is not null)
.Select(x => x!)
.ToArray());
}
}
[Serializable, NetSerializable]
public struct ReagentEffectsGuideEntry
{
public FixedPoint2 MetabolismRate;
public string[] EffectDescriptions;
public ReagentEffectsGuideEntry(FixedPoint2 metabolismRate, string[] effectDescriptions)
{
MetabolismRate = metabolismRate;
EffectDescriptions = effectDescriptions;
}
}
[DataDefinition]

View File

@@ -0,0 +1,42 @@
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry;
/// <summary>
/// This handles the chemistry guidebook and caching it.
/// </summary>
public abstract class SharedChemistryGuideDataSystem : EntitySystem
{
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
protected readonly Dictionary<string, ReagentGuideEntry> Registry = new();
public IReadOnlyDictionary<string, ReagentGuideEntry> ReagentGuideRegistry => Registry;
}
[Serializable, NetSerializable]
public sealed class ReagentGuideRegistryChangedEvent : EntityEventArgs
{
public ReagentGuideChangeset Changeset;
public ReagentGuideRegistryChangedEvent(ReagentGuideChangeset changeset)
{
Changeset = changeset;
}
}
[Serializable, NetSerializable]
public sealed class ReagentGuideChangeset
{
public Dictionary<string,ReagentGuideEntry> GuideEntries;
public HashSet<string> Removed;
public ReagentGuideChangeset(Dictionary<string, ReagentGuideEntry> guideEntries, HashSet<string> removed)
{
GuideEntries = guideEntries;
Removed = removed;
}
}

View File

@@ -1,4 +1,7 @@
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using Robust.Shared.Utility;
namespace Content.Shared.Localizations
{
@@ -31,13 +34,96 @@ namespace Content.Shared.Localizations
_loc.AddFunction(culture, "UNITS", FormatUnits);
_loc.AddFunction(culture, "TOSTRING", args => FormatToString(culture, args));
_loc.AddFunction(culture, "LOC", FormatLoc);
_loc.AddFunction(culture, "NATURALFIXED", FormatNaturalFixed);
_loc.AddFunction(culture, "NATURALPERCENT", FormatNaturalPercent);
/*
* The following language functions are specific to the english localization. When working on your own
* localization you should NOT modify these, instead add new functions specific to your language/culture.
* This ensures the english translations continue to work as expected when fallbacks are needed.
*/
var cultureEn = new CultureInfo("en-US");
_loc.AddFunction(cultureEn, "MAKEPLURAL", FormatMakePlural);
_loc.AddFunction(cultureEn, "MANY", FormatMany);
}
private ILocValue FormatMany(LocArgs args)
{
var count = ((LocValueNumber) args.Args[1]).Value;
if (Math.Abs(count - 1) < 0.0001f)
{
return (LocValueString) args.Args[0];
}
else
{
return (LocValueString) FormatMakePlural(args);
}
}
private ILocValue FormatNaturalPercent(LocArgs args)
{
var number = ((LocValueNumber) args.Args[0]).Value * 100;
var maxDecimals = (int)Math.Floor(((LocValueNumber) args.Args[1]).Value);
var formatter = (NumberFormatInfo)NumberFormatInfo.GetInstance(CultureInfo.GetCultureInfo(Culture)).Clone();
formatter.NumberDecimalDigits = maxDecimals;
return new LocValueString(string.Format(formatter, "{0:N}", number).TrimEnd('0').TrimEnd('.') + "%");
}
private ILocValue FormatNaturalFixed(LocArgs args)
{
var number = ((LocValueNumber) args.Args[0]).Value;
var maxDecimals = (int)Math.Floor(((LocValueNumber) args.Args[1]).Value);
var formatter = (NumberFormatInfo)NumberFormatInfo.GetInstance(CultureInfo.GetCultureInfo(Culture)).Clone();
formatter.NumberDecimalDigits = maxDecimals;
return new LocValueString(string.Format(formatter, "{0:N}", number).TrimEnd('0').TrimEnd('.'));
}
private static readonly Regex PluralEsRule = new("^.*(s|sh|ch|x|z)$");
private ILocValue FormatMakePlural(LocArgs args)
{
var text = ((LocValueString) args.Args[0]).Value;
var split = text.Split(" ", 1);
var firstWord = split[0];
if (PluralEsRule.IsMatch(firstWord))
{
if (split.Length == 1)
return new LocValueString($"{firstWord}es");
else
return new LocValueString($"{firstWord}es {split[1]}");
}
else
{
if (split.Length == 1)
return new LocValueString($"{firstWord}s");
else
return new LocValueString($"{firstWord}s {split[1]}");
}
}
// TODO: allow fluent to take in lists of strings so this can be a format function like it should be.
/// <summary>
/// Formats a list as per english grammar rules.
/// </summary>
public static string FormatList(List<string> list)
{
return list.Count switch
{
<= 0 => string.Empty,
1 => list[0],
2 => $"{list[0]} and {list[1]}",
> 2 => $"{string.Join(", ", list.GetRange(0, list.Count - 2))}, and {list[^1]}"
};
}
private static ILocValue FormatLoc(LocArgs args)
{
var id = ((LocValueString)args.Args[0]).Value;
var id = ((LocValueString) args.Args[0]).Value;
return new LocValueString(Loc.GetString(id));
return new LocValueString(Loc.GetString(id, args.Options.Select(x => (x.Key, x.Value.Value!)).ToArray()));
}
private static ILocValue FormatToString(CultureInfo culture, LocArgs args)
@@ -115,8 +201,8 @@ namespace Content.Shared.Localizations
//
// Note that the closing brace isn't replaced so that format specifiers can be applied.
var res = String.Format(
fmtstr.Replace("{UNIT", "{" + $"{fargs.Length - 1}"),
fargs
fmtstr.Replace("{UNIT", "{" + $"{fargs.Length - 1}"),
fargs
);
return new LocValueString(res);