From 66175f0725a0e489d8e47f5b7a5d2de346130501 Mon Sep 17 00:00:00 2001 From: Pspritechologist <81725545+Pspritechologist@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:03:47 -0400 Subject: [PATCH] Like, a prototype I guess? Some odd stuff here. --- .../Skills/SkillsBoundUserInterface.cs | 39 +++ .../SimpleStation14/Skills/SkillsMenu.xaml | 29 ++ .../SimpleStation14/Skills/SkillsMenu.xaml.cs | 60 +++++ .../Skills/SkillsUIContainer.xaml | 16 ++ .../Skills/SkillsUIContainer.xaml.cs | 32 +++ .../SimpleStation14/Skills/SkillsCommands.cs | 232 ++++++++++++++++ .../Humanoid/Prototypes/SpeciesPrototype.cs | 12 + .../Skills/Components/SkillsComponent.cs | 30 +++ .../Prototypes/SkillCategoryPrototype.cs | 46 ++++ .../Skills/Prototypes/SkillPrototype.cs | 77 ++++++ .../Prototypes/SkillSubCategoryPrototype.cs | 147 ++++++++++ .../Skills/Systems/SharedSkillsSystem.cs | 254 ++++++++++++++++++ .../Prototypes/Entities/Mobs/Species/base.yml | 15 ++ .../Skills/skill_categories.yml | 8 + .../Skills/skill_sub_categories.yml | 38 +++ .../Skills/skills_engineering.yml | 83 ++++++ 16 files changed, 1118 insertions(+) create mode 100644 Content.Client/SimpleStation14/Skills/SkillsBoundUserInterface.cs create mode 100644 Content.Client/SimpleStation14/Skills/SkillsMenu.xaml create mode 100644 Content.Client/SimpleStation14/Skills/SkillsMenu.xaml.cs create mode 100644 Content.Client/SimpleStation14/Skills/SkillsUIContainer.xaml create mode 100644 Content.Client/SimpleStation14/Skills/SkillsUIContainer.xaml.cs create mode 100644 Content.Server/SimpleStation14/Skills/SkillsCommands.cs create mode 100644 Content.Shared/SimpleStation14/Skills/Components/SkillsComponent.cs create mode 100644 Content.Shared/SimpleStation14/Skills/Prototypes/SkillCategoryPrototype.cs create mode 100644 Content.Shared/SimpleStation14/Skills/Prototypes/SkillPrototype.cs create mode 100644 Content.Shared/SimpleStation14/Skills/Prototypes/SkillSubCategoryPrototype.cs create mode 100644 Content.Shared/SimpleStation14/Skills/Systems/SharedSkillsSystem.cs create mode 100644 Resources/Prototypes/SimpleStation14/Skills/skill_categories.yml create mode 100644 Resources/Prototypes/SimpleStation14/Skills/skill_sub_categories.yml create mode 100644 Resources/Prototypes/SimpleStation14/Skills/skills_engineering.yml diff --git a/Content.Client/SimpleStation14/Skills/SkillsBoundUserInterface.cs b/Content.Client/SimpleStation14/Skills/SkillsBoundUserInterface.cs new file mode 100644 index 0000000000..f67e324226 --- /dev/null +++ b/Content.Client/SimpleStation14/Skills/SkillsBoundUserInterface.cs @@ -0,0 +1,39 @@ +using JetBrains.Annotations; +using Content.Shared.Borgs; +using Robust.Client.GameObjects; +using Content.Shared.SimpleStation14.Skills.Systems; + +namespace Content.Client.SimpleStation14.Skills; + +[UsedImplicitly] +public sealed class SkillsBoundUserInterface : BoundUserInterface +{ + private SkillsMenu? _skillsMenu; + + public SkillsBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + } + + protected override void Open() + { + base.Open(); + + _skillsMenu = new SkillsMenu(this, EntMan.System()); + + _skillsMenu.OnClose += Close; + + _skillsMenu.OpenCentered(); + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (!disposing) return; + _skillsMenu?.Dispose(); + } +} diff --git a/Content.Client/SimpleStation14/Skills/SkillsMenu.xaml b/Content.Client/SimpleStation14/Skills/SkillsMenu.xaml new file mode 100644 index 0000000000..e2eba50d84 --- /dev/null +++ b/Content.Client/SimpleStation14/Skills/SkillsMenu.xaml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/Content.Client/SimpleStation14/Skills/SkillsMenu.xaml.cs b/Content.Client/SimpleStation14/Skills/SkillsMenu.xaml.cs new file mode 100644 index 0000000000..0ed1083113 --- /dev/null +++ b/Content.Client/SimpleStation14/Skills/SkillsMenu.xaml.cs @@ -0,0 +1,60 @@ +using Content.Client.UserInterface.Controls; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.XAML; +using Content.Shared.SimpleStation14.Skills.Components; +using Content.Shared.SimpleStation14.Skills.Systems; + +namespace Content.Client.SimpleStation14.Skills; + +[GenerateTypedNameReferences] +public sealed partial class SkillsMenu : FancyWindow +{ + [Dependency] private readonly IEntityManager _entityManager = default!; + + private readonly SharedSkillsSystem _skills = default!; + + private readonly SkillsBoundUserInterface _owner; + + public SkillsMenu(SkillsBoundUserInterface owner, SharedSkillsSystem skillsSystem) + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + _skills = skillsSystem; + + _owner = owner; + + UpdateSkills(); + } + + public void UpdateSkills() + { + var skills = _skills.GetAllSkillsCategorized(); + + Skills.DisposeAllChildren(); + + foreach (var category in skills.Keys) + { + var categoryLabel = new SkillsUIContainer(); + categoryLabel.SetHeading(category.Name); + categoryLabel.SetDescription(category.Description); + Skills.AddChild(categoryLabel); + + foreach (var subCategory in skills[category].Keys) + { + var subCategoryLabel = new SkillsUIContainer(); + subCategoryLabel.SetHeading(subCategory.Name); + subCategoryLabel.SetDescription(subCategory.Description); + subCategoryLabel.SetColor(subCategory.SubCategoryColor); + Skills.AddChild(subCategoryLabel); + + foreach (var skill in skills[category][subCategory]) + { + var newLabel = new SkillsUIContainer(); + newLabel.SetHeading(skill.Name); + newLabel.SetDescription(skill.Description); + Skills.AddChild(newLabel); + } + } + } + } +} diff --git a/Content.Client/SimpleStation14/Skills/SkillsUIContainer.xaml b/Content.Client/SimpleStation14/Skills/SkillsUIContainer.xaml new file mode 100644 index 0000000000..6e53d3f12b --- /dev/null +++ b/Content.Client/SimpleStation14/Skills/SkillsUIContainer.xaml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/Content.Client/SimpleStation14/Skills/SkillsUIContainer.xaml.cs b/Content.Client/SimpleStation14/Skills/SkillsUIContainer.xaml.cs new file mode 100644 index 0000000000..b8bcf59a9f --- /dev/null +++ b/Content.Client/SimpleStation14/Skills/SkillsUIContainer.xaml.cs @@ -0,0 +1,32 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.CustomControls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Localization; +using Robust.Shared.Utility; + +namespace Content.Client.SimpleStation14.Skills; + +[GenerateTypedNameReferences] +public sealed partial class SkillsUIContainer : PanelContainer +{ + public SkillsUIContainer() + { + RobustXamlLoader.Load(this); + } + + public void SetHeading(string desc) + { + Title.Text = desc; + } + + public void SetDescription(string desc) + { + Description.SetMessage(desc); + } + + public void SetColor(Color color) + { + Modulate = color; + } +} diff --git a/Content.Server/SimpleStation14/Skills/SkillsCommands.cs b/Content.Server/SimpleStation14/Skills/SkillsCommands.cs new file mode 100644 index 0000000000..0223b1c1ee --- /dev/null +++ b/Content.Server/SimpleStation14/Skills/SkillsCommands.cs @@ -0,0 +1,232 @@ +using Content.Server.Administration; +using Content.Shared.Administration; +using Content.Shared.Borgs; +using Robust.Shared.Console; +using Content.Server.Players; +using Robust.Server.Player; +using Robust.Server.GameObjects; +using Content.Shared.SimpleStation14.Skills.Systems; + +namespace Content.Server.SimpleStation14.Skills; + +[AdminCommand(AdminFlags.Logs)] +public sealed class SkillUiCommand : IConsoleCommand +{ + public string Command => "skillui"; + public string Description => Loc.GetString("command-skillui-description"); + public string Help => Loc.GetString("command-skillui-help"); + + public async void Execute(IConsoleShell shell, string argStr, string[] args) + { + var entityManager = IoCManager.Resolve(); + var uiSystem = IoCManager.Resolve(); + var player = shell.Player as IPlayerSession; + EntityUid? entity = null; + + if (args.Length == 0 && player != null) + { + entity = player.ContentData()?.Mind?.CurrentEntity; + } + else if (IoCManager.Resolve().TryGetPlayerDataByUsername(args[0], out var data)) + { + entity = data.ContentData()?.Mind?.CurrentEntity; + } + else if (EntityUid.TryParse(args[0], out var foundEntity)) + { + entity = foundEntity; + } + + if (entity == null) + { + shell.WriteLine("Can't find entity."); + return; + } + + if (!uiSystem.TryGetUi(entity.Value, SkillsUiKey.Key, out _)) + return; + + uiSystem.TryOpen(entity.Value, SkillsUiKey.Key, player!); + } +} + +// [AdminCommand(AdminFlags.Logs)] +// public sealed class ListSkillsCommand : IConsoleCommand +// { +// public string Command => "skillsls"; +// public string Description => Loc.GetString("command-skillsls-description"); +// public string Help => Loc.GetString("command-skillsls-help"); + +// public async void Execute(IConsoleShell shell, string argStr, string[] args) +// { +// var entityManager = IoCManager.Resolve(); +// var player = shell.Player as IPlayerSession; +// EntityUid? entity = null; + +// if (args.Length == 0 && player != null) +// { +// entity = player.ContentData()?.Mind?.CurrentEntity; +// } +// else if (IoCManager.Resolve().TryGetPlayerDataByUsername(args[0], out var data)) +// { +// entity = data.ContentData()?.Mind?.CurrentEntity; +// } +// else if (EntityUid.TryParse(args[0], out var foundEntity)) +// { +// entity = foundEntity; +// } + +// if (entity == null) +// { +// shell.WriteLine("Can't find entity."); +// return; +// } + +// if (!entityManager.TryGetComponent(entity, out var skills)) +// { +// shell.WriteLine("Entity has no skills."); +// return; +// } + +// // Parkstation-Skills-Start +// shell.WriteLine($"Name: {Loc.GetString($"skillset-name-{skills.SkillsID}")}"); +// shell.WriteLine($"Description: {Loc.GetString($"skillset-description-{skills.SkillsID}")}"); +// // Parkstation-Skills-End + +// shell.WriteLine($"Skills for {entityManager.ToPrettyString(entity.Value)}:"); +// foreach (var skills in skills.Skills) +// { +// shell.WriteLine(skills); +// } +// } +// } + +// [AdminCommand(AdminFlags.Fun)] +// public sealed class ClearSkillsCommand : IConsoleCommand +// { +// public string Command => "skillsclear"; +// public string Description => Loc.GetString("command-skillsclear-description"); +// public string Help => Loc.GetString("command-skillsclear-help"); +// public async void Execute(IConsoleShell shell, string argStr, string[] args) +// { +// var entityManager = IoCManager.Resolve(); +// var player = shell.Player as IPlayerSession; +// EntityUid? entity = null; + +// if (args.Length == 0 && player != null) +// { +// entity = player.ContentData()?.Mind?.CurrentEntity; +// } +// else if (IoCManager.Resolve().TryGetPlayerDataByUsername(args[0], out var data)) +// { +// entity = data.ContentData()?.Mind?.CurrentEntity; +// } +// else if (EntityUid.TryParse(args[0], out var foundEntity)) +// { +// entity = foundEntity; +// } + +// if (entity == null) +// { +// shell.WriteLine("Can't find entity."); +// return; +// } + +// if (!entityManager.TryGetComponent(entity.Value, out var skills)) +// { +// shell.WriteLine("Entity has no skills component to clear"); +// return; +// } + +// entityManager.EntitySysManager.GetEntitySystem().ClearSkills(entity.Value, skills); +// } +// } + +// [AdminCommand(AdminFlags.Fun)] +// public sealed class AddSkillsCommand : IConsoleCommand +// { +// public string Command => "skillsadd"; +// public string Description => Loc.GetString("command-skillsadd-description"); +// public string Help => Loc.GetString("command-skillsadd-help"); +// public async void Execute(IConsoleShell shell, string argStr, string[] args) +// { +// var entityManager = IoCManager.Resolve(); +// var player = shell.Player as IPlayerSession; +// EntityUid? entity = null; + +// if (args.Length < 2 || args.Length > 3) +// { +// shell.WriteLine("Wrong number of arguments."); +// return; +// } + +// if (IoCManager.Resolve().TryGetPlayerDataByUsername(args[0], out var data)) +// { +// entity = data.ContentData()?.Mind?.CurrentEntity; +// } +// else if (EntityUid.TryParse(args[0], out var foundEntity)) +// { +// entity = foundEntity; +// } + +// if (entity == null) +// { +// shell.WriteLine("Can't find entity."); +// return; +// } + +// var skills = entityManager.EnsureComponent(entity.Value); + +// if (args.Length == 2) +// entityManager.EntitySysManager.GetEntitySystem().AddSkills(entity.Value, args[1], component: skills); +// else if (args.Length == 3 && int.TryParse(args[2], out var index)) +// entityManager.EntitySysManager.GetEntitySystem().AddSkills(entity.Value, args[1], index, skills); +// else +// shell.WriteLine("Third argument must be an integer."); +// } +// } + +// [AdminCommand(AdminFlags.Fun)] +// public sealed class RemoveSkillsCommand : IConsoleCommand +// { +// public string Command => "skillsrm"; +// public string Description => Loc.GetString("command-skillsrm-description"); +// public string Help => Loc.GetString("command-skillsrm-help"); +// public async void Execute(IConsoleShell shell, string argStr, string[] args) +// { +// var entityManager = IoCManager.Resolve(); +// var player = shell.Player as IPlayerSession; +// EntityUid? entity = null; + +// if (args.Length < 1 || args.Length > 2) +// { +// shell.WriteLine("Wrong number of arguments."); +// return; +// } + +// if (IoCManager.Resolve().TryGetPlayerDataByUsername(args[0], out var data)) +// { +// entity = data.ContentData()?.Mind?.CurrentEntity; +// } +// else if (EntityUid.TryParse(args[0], out var foundEntity)) +// { +// entity = foundEntity; +// } + +// if (entity == null) +// { +// shell.WriteLine("Can't find entity."); +// return; +// } + +// if (!entityManager.TryGetComponent(entity, out var skills)) +// { +// shell.WriteLine("Entity has no skills to remove!"); +// return; +// } + +// if (args[1] == null || !int.TryParse(args[1], out var index)) +// entityManager.EntitySysManager.GetEntitySystem().RemoveSkills(entity.Value); +// else +// entityManager.EntitySysManager.GetEntitySystem().RemoveSkills(entity.Value, index); +// } +// } diff --git a/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs b/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs index c16d987a3c..2eabc2b3dd 100644 --- a/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs +++ b/Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs @@ -1,5 +1,7 @@ +using Content.Shared.SimpleStation14.Skills.Prototypes; // Parkstation-Skills using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; // Parkstation-Skills namespace Content.Shared.Humanoid.Prototypes; @@ -120,6 +122,16 @@ public sealed class SpeciesPrototype : IPrototype /// [DataField("maxAge")] public int MaxAge = 120; + + // Parkstation-Skills-Start + + /// + /// Any skills this species starts with bonus points in. + /// + [DataField("skillBonuses", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + public Dictionary SkillBonuses { get; } = new(); + + // Parkstation-Skills-End } public enum SpeciesNaming : byte diff --git a/Content.Shared/SimpleStation14/Skills/Components/SkillsComponent.cs b/Content.Shared/SimpleStation14/Skills/Components/SkillsComponent.cs new file mode 100644 index 0000000000..1e5f0613c8 --- /dev/null +++ b/Content.Shared/SimpleStation14/Skills/Components/SkillsComponent.cs @@ -0,0 +1,30 @@ +using Content.Shared.SimpleStation14.Skills.Prototypes; +using Content.Shared.SimpleStation14.Skills.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; +using Content.Shared.Humanoid.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.SimpleStation14.Skills.Components +{ + [RegisterComponent] + [NetworkedComponent] + [AutoGenerateComponentState] + [Serializable] + [NetSerializable] + public sealed partial class SkillsComponent : Component + { + /// + /// This holds all the data on an entity's skills. + /// If you're using this- don't. Use to access it. + /// + [AutoNetworkedField] + public Dictionary Skills = new(); + + /// + /// Skills this entity starts with values in. + /// Make sure not to double this up with . + [DataField("startingSkills", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + public Dictionary StartingSkills = new(); + } +} diff --git a/Content.Shared/SimpleStation14/Skills/Prototypes/SkillCategoryPrototype.cs b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillCategoryPrototype.cs new file mode 100644 index 0000000000..beaf75eb2e --- /dev/null +++ b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillCategoryPrototype.cs @@ -0,0 +1,46 @@ +using Content.Shared.Whitelist; +using Robust.Shared.Prototypes; + +namespace Content.Shared.SimpleStation14.Skills.Prototypes; + +[Prototype("skillCategory")] +public sealed class SkillCategoryPrototype : IPrototype +{ + [IdDataField] + public string ID { get; } = default!; + + // /// + // /// The Sprite Specifier for the icon for this sub category. + // /// This directory will also be used to find the icons for the skills in this sub category. + // /// + // [DataField("icon")] + // public SpriteSpecifier Icon = default!; + + /// + /// A whitelist to determine what Entities are allowed to use this category of skills. + /// + [DataField("whitelist")] + public EntityWhitelist? Whitelist = null; + + /// + /// A whitelist to determine what Entities are NOT allowed to use this category of skills. + /// + /// + /// A blacklist is just a whitelist you deny. + /// + [DataField("blacklist")] + public EntityWhitelist? Blacklist = null; + + /// + /// Whether or not this category is viewable in the character menu. + /// + /// + /// Note this can be overriden on a per-sub category basis. + /// + [DataField("viewable")] + public bool Viewable = true; + + public string Name => Loc.GetString($"{ID}-name"); + + public string Description => Loc.GetString($"{ID}-description"); +} diff --git a/Content.Shared/SimpleStation14/Skills/Prototypes/SkillPrototype.cs b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillPrototype.cs new file mode 100644 index 0000000000..534264ce32 --- /dev/null +++ b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillPrototype.cs @@ -0,0 +1,77 @@ +using Content.Shared.Whitelist; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Utility; + +namespace Content.Shared.SimpleStation14.Skills.Prototypes; + +[Prototype("skill")] +public sealed class SkillPrototype : IPrototype +{ + [IdDataField] + public string ID { get; } = default!; + + /// + /// The sub category this skill belongs to, underneath that sub category's primary category. + /// + /// + /// This might be something like "Melee," "Ranged," "Medical," etc.. + /// + [DataField("subCategory", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string SubCategory = default!; + + /// + /// If true, this skill's icon won't be colored by the sub category's color. + /// + [DataField("overrideColor")] + public bool OverrideColor = false; + + /// + /// If not null, this will override the subcategory's selectable value for this skill. + /// + [DataField("selectableOverride")] + public bool? SelectableOverride = null; + + /// + /// The wheight this skill holds in the overall sub category's level. + /// Default is 1, skills higher will make more of an impact in the overall level. + /// Skills lower will make less of an impact. + /// + [DataField("weight")] + public int Weight = 1; + + /// + /// Optional override for the Sprite Specifier for the icon for this skill. + /// + /// + /// If null, the state will be generated from the skill's name, and the RSI will be inherited from the sub category. + /// + [DataField("icon", customTypeSerializer: typeof(SpriteSpecifierSerializer))] + public SpriteSpecifier? Icon = null; + + /// + /// A whitelist to determine what Entities are allowed to use this skill, in addition to the sub categories list. + /// + [DataField("whitelist")] + public EntityWhitelist? Whitelist = null; + + /// + /// A whitelist to determine what Entities are NOT allowed to use this skill, in addition to the sub categories list. + /// + /// + /// A blacklist is just a whitelist you deny. + /// + [DataField("blacklist")] + public EntityWhitelist? Blacklist = null; + + /// + /// Whether or not this skill is viewable in the character menu. + /// + [DataField("viewable")] + public bool? Viewable = null; + + public string Name => Loc.GetString($"{ID}-name"); + + public string Description => Loc.GetString($"{ID}-description"); +} diff --git a/Content.Shared/SimpleStation14/Skills/Prototypes/SkillSubCategoryPrototype.cs b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillSubCategoryPrototype.cs new file mode 100644 index 0000000000..346bb3e73d --- /dev/null +++ b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillSubCategoryPrototype.cs @@ -0,0 +1,147 @@ +using Content.Shared.SimpleStation14.Skills.Systems; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Utility; +using Content.Shared.Whitelist; + +using System.Linq; +using Robust.Shared.Serialization.Manager; +using Robust.Shared.Serialization.Markdown; +using Robust.Shared.Serialization.Markdown.Mapping; +using Robust.Shared.Serialization.Markdown.Sequence; +using Robust.Shared.Serialization.Markdown.Validation; +using Robust.Shared.Serialization.Markdown.Value; +using Robust.Shared.Serialization.TypeSerializers.Interfaces; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.SimpleStation14.Skills.Prototypes; + +[Prototype("skillSubCategory")] +public sealed class SkillSubCategoryPrototype : IPrototype +{ + [IdDataField] + public string ID { get; } = default!; + + /// + /// The primary category this sub category belongs to. + /// + [DataField("category", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string Category = default!; + + // /// + // /// The Sprite Specifier for the icon for this sub category. + // /// This directory will also be used to find the icons for the skills in this sub category. + // /// + // [DataField("icon")] + // public SpriteSpecifier Icon = default!; + + /// + /// Color for this sub category. + /// Used to color the icons of the skills. + /// + [DataField("color", customTypeSerializer: typeof(ColorSerializer))] + public Color SubCategoryColor = Color.FromHex("#A0A0A0"); + + /// + /// A whitelist to determine what Entities are allowed to use this sub category of skills. + /// + [DataField("whitelist")] + public EntityWhitelist? Whitelist = null; + + /// + /// A whitelist to determine what Entities are NOT allowed to use this sub category of skills. + /// + /// + /// A blacklist is just a whitelist you deny. + /// + [DataField("blacklist")] + public EntityWhitelist? Blacklist = null; + + /// + /// Whether or not this sub category is selectable by players in character creation. + /// + /// + /// Note this can be overriden on a per-skill basis. + /// + [DataField("selectable")] + public bool Selectable = true; + + /// + /// Whether or not this sub category is viewable in the character menu. + /// + /// + /// Note this can be overriden on a per-skill basis. + /// + [DataField("viewable")] + public bool Viewable = true; + + public string Name => Loc.GetString($"{ID}-name"); + + public string Description => Loc.GetString($"{ID}-description"); + + // /// + // /// A list of every skill in this sub category. + // /// + // /// + // /// Each entry is a Dictionary that must contain a unique name for the skill. + // /// Optionally, it can also contain a 'description' loc key string, a 'selectableOverride' bool, and an 'icon' state string. + // /// If not included, the description and icon will be generated from the skill's name, and the selectable bool will be inherited from the sub category. + // /// + // [DataField("skills", customTypeSerializer: typeof(SkillListSerializer))] + // public List> Skills = new(); +} + +// public sealed class SkillListSerializer : ITypeValidator>, SequenceDataNode> +// { +// private static readonly Dictionary ValidKeys = new() +// { +// {"selectableOverride", typeof(bool)}, +// {"description", typeof(string)}, +// {"icon", typeof(string)}, +// {"name", typeof(string)}, +// }; + +// private static bool CheckValidKey(DataNode node) +// { +// if (node is not ValueDataNode value) +// return false; + +// return ValidKeys.ContainsKey(value.Value); +// } + +// public ValidationNode Validate( +// ISerializationManager serializationManager, +// SequenceDataNode node, +// IDependencyCollection dependencies, +// ISerializationContext? context = null) +// { +// var sequence = new List(); + +// foreach (var mappingNode in new List(node.Sequence)) +// { +// // Check if the entry in the sequence is a mapping. +// if (mappingNode is not MappingDataNode mapping) +// { +// sequence.Add(new ErrorNode(mappingNode, "SkillListSerializer prototypes must be a list of mappings/dictionaries.")); +// continue; +// } + +// // Check if the mapping has a 'name' key, and that it's a string. +// if (!mapping.TryGet("name", out var nameNode) || nameNode is not ValueDataNode) +// { +// sequence.Add(new ErrorNode(mapping, "SkillListSerializer prototype entries must have a 'name' key representing a string.")); +// continue; +// } + +// // Check that all the keys present are contained in the valid keys list. +// if (mapping.Keys.Any(key => !CheckValidKey(key))) +// { +// sequence.Add(new ErrorNode(mapping, $"SkillListSerializer prototype entries must only contain valid keys. Valid keys are: {string.Join(", ", ValidKeys.Keys)}")); +// continue; +// } +// } + +// return new ValidatedSequenceNode(sequence); +// } + +// } diff --git a/Content.Shared/SimpleStation14/Skills/Systems/SharedSkillsSystem.cs b/Content.Shared/SimpleStation14/Skills/Systems/SharedSkillsSystem.cs new file mode 100644 index 0000000000..febb35284a --- /dev/null +++ b/Content.Shared/SimpleStation14/Skills/Systems/SharedSkillsSystem.cs @@ -0,0 +1,254 @@ +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Content.Shared.SimpleStation14.Skills.Components; +using Content.Shared.SimpleStation14.Skills.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.SimpleStation14.Skills.Systems; + +public sealed class SharedSkillsSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private Dictionary>> _categorizedSkills = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSkillsInit); + + GenerateCategorizedSkills(); + _prototype.PrototypesReloaded += OnPrototypesReloaded; + } + + public bool TryModifySkillLevel(string skill, int level) + { + if (!_prototype.HasIndex(skill)) + return false; + + + } + + public bool TrySetSkillLevel(string skill, int level) + { + + } + + /// + /// Returns the level of an entity's skill. + /// + /// Entity to check for skills on. + /// The Prototype ID of the skill to check for. + /// The SkillsComponent of the entity. + /// The level of the entitie's skill. + public int GetSkillLevel(EntityUid uid, string skillId, SkillsComponent? skillsComp = null) + { + if (!Resolve(uid, ref skillsComp)) + return 0; + + if (skillsComp.Skills.TryGetValue(skillId, out var level)) + return level; + + return 0; + } + + /// + /// Returns the overall level of a sub category. + /// This is an average of all the skills in the sub category, considering their individual weights. + /// + public int GetSubCategoryLevel(EntityUid uid, string subCategoryId, SkillsComponent? skillsComp = null) + { + if (!Resolve(uid, ref skillsComp)) + return 0; + + if (!TryGetSkillSubCategory(subCategoryId, out var subCategory)) + return 0; + + if (!TryGetSkillCategory(subCategory.Category, out var category)) + return 0; + + var skills = _categorizedSkills[category][subCategory]; + + var totalWeight = skills.Sum(skill => skill.Weight); + var totalLevel = skills.Sum(skill => GetSkillLevel(uid, skill.ID, skillsComp) * skill.Weight); + + return totalLevel / totalWeight; + } + + /// + /// Used to determine whether or not an Entity is 'trained' in a particular skill. + /// + /// Entity to check for skills on. + /// The Prototype ID of the skill to check for. + /// The SkillsComponent of the entity. + /// True if the entity has at least one level in the skill. + public bool IsTrained(EntityUid uid, string skillId, SkillsComponent? skillsComp = null) + { + if (!Resolve(uid, ref skillsComp)) + return false; + + return GetSkillLevel(uid, skillId, skillsComp) > 0; + } + + /// + /// Returns a Dictionary of all the skills an Entity has levels in. + /// + /// + /// Note that this will only be Components that happen to be logged on the Entity. They may or may not be trained, and it almost certainly won't be all of the skills in the game. + /// + /// Entity to check for skills on. + /// The SkillsComponent of the entity. + /// A Dictionary of skills and levels. + public Dictionary GetAllSkillLevels(EntityUid uid, SkillsComponent? skillsComp = null) + { + if (!Resolve(uid, ref skillsComp)) + return new Dictionary(); + + return new(skillsComp.Skills); + } + + /// + /// Gets all skills in the game organized by category, and then sub category. + /// + public Dictionary>> GetAllSkillsCategorized() + { + return _categorizedSkills; + } + + /// + /// Tries to get all skills in a given category. + /// + /// The ID of the category to get from. + /// A dictionary of sub categories, each containing a list of their skills. + /// True if the category exists, false otherwise. + public bool TryGetSkillsInCategory(string categoryId, [NotNullWhen(true)] out Dictionary>? skillsBySubCategory) + { + skillsBySubCategory = null; + + if (!_prototype.TryIndex(categoryId, out SkillCategoryPrototype? category)) + return false; + + if (!_categorizedSkills.TryGetValue(category, out skillsBySubCategory)) + return false; + + return true; + } + + /// + /// Tries to get all skills in a given sub category. + /// + /// The ID of the sub category to get from. + /// A list of skills in the sub category. + /// True if the sub category exists, false otherwise. + public bool TryGetSkillsInSubCategory(string subCategoryId, [NotNullWhen(true)] out List? skills) + { + skills = null; + + if (!_prototype.TryIndex(subCategoryId, out SkillSubCategoryPrototype? subCategory)) + return false; + + if (!TryGetSkillsInCategory(subCategory.Category, out var skillsBySubCategory)) + return false; + + if (!skillsBySubCategory.TryGetValue(subCategory, out skills)) + return false; + + return true; + } + + /// + /// Tries to get a skill prototype by its ID. + /// + /// The ID of the skill to get. + /// The skill prototype, if found. + /// True if the skill was found, false otherwise. + public bool TryGetSkill(string skillId, [NotNullWhen(true)] out SkillPrototype? skill) + { + return _prototype.TryIndex(skillId, out skill); + } + + /// + /// Tries to get a skill subcategory by its ID. + /// + /// The ID of the skill subcategory to get. + /// The skill subcategory, if found. + /// True if the skill subcategory was found, false otherwise. + public bool TryGetSkillSubCategory(string subCategoryId, [NotNullWhen(true)] out SkillSubCategoryPrototype? subCategory) + { + return _prototype.TryIndex(subCategoryId, out subCategory); + } + + /// + /// Tries to get a skill category by its ID. + /// + /// The ID of the skill category to get. + /// The skill category, if found. + /// True if the skill category was found, false otherwise. + public bool TryGetSkillCategory(string categoryId, [NotNullWhen(true)] out SkillCategoryPrototype? category) + { + return _prototype.TryIndex(categoryId, out category); + } + + private void OnSkillsInit(EntityUid uid, SkillsComponent component, ComponentInit args) + { + component.Skills.Add + } + + private void SetSkill(EntityUid uid, string skillId, int level, SkillsComponent? skillsComp = null) + { + if (!Resolve(uid, ref skillsComp)) + return; + + if (level <= 0) + { + if (skillsComp.Skills.ContainsKey(skillId)) + skillsComp.Skills.Remove(skillId); + return; + } + + if (skillsComp.Skills.ContainsKey(skillId)) + skillsComp.Skills[skillId] = level; + else + skillsComp.Skills.Add(skillId, level); + + Dirty(skillsComp); + } + + private void GenerateCategorizedSkills() + { + var skills = _prototype.EnumeratePrototypes(); + var subsById = _prototype.EnumeratePrototypes().ToDictionary(sub => sub.ID); + var categoriesById = _prototype.EnumeratePrototypes().ToDictionary(category => category.ID); + + var skillsCategorized = new Dictionary>>(); + + foreach (var skill in skills) + { + var sub = subsById[skill.SubCategory]; + var category = categoriesById[sub.Category]; + + if (!skillsCategorized.ContainsKey(category)) + skillsCategorized.Add(category, new Dictionary>()); + + if (!skillsCategorized[category].ContainsKey(sub)) + skillsCategorized[category].Add(sub, new List()); + + skillsCategorized[category][sub].Add(skill); + } + + _categorizedSkills = skillsCategorized; + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) + { + GenerateCategorizedSkills(); + } +} + +[NetSerializable, Serializable] +public enum SkillsUiKey : byte +{ + Key, +} diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 9379135cd1..709f8f72fb 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -289,6 +289,8 @@ type: HumanoidMarkingModifierBoundUserInterface - key: enum.StrippingUiKey.Key type: StrippableBoundUserInterface + - key: enum.SkillsUiKey.Key + type: SkillsBoundUserInterface - type: Puller - type: Butcherable butcheringType: Spike # TODO human. @@ -320,6 +322,19 @@ - type: CharacterInformation - type: SSDIndicator + - type: Skills + - type: IntrinsicUI + uis: + - key: enum.SkillsUiKey.Key + toggleAction: + name: action-name-show-skills + description: action-description-show-skills + icon: Structures/Wallmounts/posters.rsi/poster11_legit.png #someone wanna make new icons? + iconOn: Structures/Wallmounts/posters.rsi/poster11_legit.png + keywords: [ "skills", "stats", "attributes" ] + priority: -3 + event: !type:ToggleIntrinsicUIEvent + - type: entity save: false name: Urist McHands diff --git a/Resources/Prototypes/SimpleStation14/Skills/skill_categories.yml b/Resources/Prototypes/SimpleStation14/Skills/skill_categories.yml new file mode 100644 index 0000000000..fb3428fc2a --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Skills/skill_categories.yml @@ -0,0 +1,8 @@ +- type: skillCategory + id: SkillCategoryPhysical + +- type: skillCategory + id: SkillCategoryTechnical + +- type: skillCategory + id: SkillCategoryMystical diff --git a/Resources/Prototypes/SimpleStation14/Skills/skill_sub_categories.yml b/Resources/Prototypes/SimpleStation14/Skills/skill_sub_categories.yml new file mode 100644 index 0000000000..41b1671212 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Skills/skill_sub_categories.yml @@ -0,0 +1,38 @@ +## Physical. +- type: skillSubCategory + id: SkillSubCategoryStrength + category: SkillCategoryPhysical + color: "#b11e0d" + +- type: skillSubCategory + id: SkillSubCategoryDexterity + category: SkillCategoryPhysical + color: "#7d38dd" + +- type: skillSubCategory + id: SkillSubCategoryEndurance + category: SkillCategoryPhysical + color: "#448d0c" + +- type: skillSubCategory + id: SkillSubCategoryAgility + category: SkillCategoryPhysical + color: "#2ba5e7" + +- type: skillSubCategory + id: SkillSubCategorySenses + category: SkillCategoryPhysical + color: "#e7b02b" + +## Technical. +- type: skillSubCategory + id: SkillSubCategoryEngineering + category: SkillCategoryTechnical + +- type: skillSubCategory + id: SkillSubCategoryResearch + category: SkillCategoryTechnical + +- type: skillSubCategory + id: SkillSubCategoryMedicine + category: SkillCategoryTechnical diff --git a/Resources/Prototypes/SimpleStation14/Skills/skills_engineering.yml b/Resources/Prototypes/SimpleStation14/Skills/skills_engineering.yml new file mode 100644 index 0000000000..942ea48f72 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Skills/skills_engineering.yml @@ -0,0 +1,83 @@ +## Strength +- type: skill + id: SkillLifting + subCategory: SkillSubCategoryStrength + +- type: skill + id: SkillBoxing + subCategory: SkillSubCategoryStrength + +- type: skill + id: Endurance + subCategory: SkillSubCategoryStrength + +## Dexterity +- type: skill + id: SkillDueling + subCategory: SkillSubCategoryDexterity + +- type: skill + id: SkillFineMotorSkills + subCategory: SkillSubCategoryDexterity + +- type: skill + id: SkillObservation + subCategory: SkillSubCategorySenses + +## Agility +- type: skill + id: SkillSpriting + subCategory: SkillSubCategoryAgility + +- type: skill + id: SkillAcrobatics + subCategory: SkillSubCategoryAgility + +- type: skill + id: SkillDodging + subCategory: SkillSubCategoryAgility + +## Technical -------------- +## Engineering +- type: skill + id: SkillConstruction + subCategory: SkillSubCategoryEngineering + +- type: skill + id: SkillElectrical + subCategory: SkillSubCategoryEngineering + +- type: skill + id: SkillMechanical + subCategory: SkillSubCategoryEngineering + +## Medicine +- type: skill + id: SkillFirstAid + subCategory: SkillSubCategoryMedicine + +- type: skill + id: SkillSurgery + subCategory: SkillSubCategoryMedicine + +- type: skill + id: SkillChemistry + subCategory: SkillSubCategoryMedicine + +# - type: skill +# id: SkillVirology # Ha ha +# subCategory: SkillSubCategoryMedicine + +## Research +- type: skill + id: SkillQuantumPhysics + subCategory: SkillSubCategoryResearch + +- type: skill + id: AnomalousMaterials + subCategory: SkillSubCategoryResearch + +- type: skill + id: SkillXenobiology + subCategory: SkillSubCategoryResearch +