From 97444374477d06ffffc81134a1bc08d43c99143e Mon Sep 17 00:00:00 2001 From: Pspritechologist <81725545+Pspritechologist@users.noreply.github.com> Date: Wed, 18 Oct 2023 11:28:32 -0400 Subject: [PATCH] I mean, it freakin works :P What else can ya ask for. There needs to be a proper way to specify skill requirements in YAML, likely a serializable struct. There's a basis for that commented out in the main System at the moment but I wanted to test if it actually worked. Other than that, I suppose it's just a matter of refining, helper functions, and of course, implementation. --- .../Skills/RoleSkillsSpecial.cs | 27 ++ .../SimpleStation14/Skills/SkillsCommands.cs | 225 ++-------- .../Skills/Components/SkillsComponent.cs | 8 +- .../Prototypes/SkillCategoryPrototype.cs | 26 +- .../Skills/Prototypes/SkillPrototype.cs | 22 +- .../Skills/Systems/SharedSkillsSystem.cs | 390 +++++++++++++----- .../Tools/Components/ToolComponent.cs | 18 + .../Tools/Systems/SharedToolSystem.cs | 12 + .../Entities/Objects/Tools/tools.yml | 10 + .../Roles/Jobs/Engineering/chief_engineer.yml | 5 + .../Jobs/Engineering/station_engineer.yml | 5 + 11 files changed, 440 insertions(+), 308 deletions(-) create mode 100644 Content.Server/SimpleStation14/Skills/RoleSkillsSpecial.cs diff --git a/Content.Server/SimpleStation14/Skills/RoleSkillsSpecial.cs b/Content.Server/SimpleStation14/Skills/RoleSkillsSpecial.cs new file mode 100644 index 0000000000..a5f7a25d93 --- /dev/null +++ b/Content.Server/SimpleStation14/Skills/RoleSkillsSpecial.cs @@ -0,0 +1,27 @@ +using Content.Shared.Roles; +using Content.Shared.SimpleStation14.Skills.Prototypes; +using Content.Shared.SimpleStation14.Skills.Systems; +using JetBrains.Annotations; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary; + +namespace Content.Server.SimpleStation14.Skills; + +/// +/// Adds to an Entity's skills on equip. +/// +[UsedImplicitly] +public sealed class RoleSkillsSpecial : JobSpecial +{ + + [DataField("skills", customTypeSerializer: typeof(PrototypeIdDictionarySerializer))] + public Dictionary Skills { get; } = new(); + + public override void AfterEquip(EntityUid mob) + { + var entMan = IoCManager.Resolve(); + var skillSystem = entMan.System(); + + foreach (var skill in Skills) + skillSystem.TryModifySkillLevel(mob, skill.Key, skill.Value); + } +} diff --git a/Content.Server/SimpleStation14/Skills/SkillsCommands.cs b/Content.Server/SimpleStation14/Skills/SkillsCommands.cs index 0223b1c1ee..8dab64b8a4 100644 --- a/Content.Server/SimpleStation14/Skills/SkillsCommands.cs +++ b/Content.Server/SimpleStation14/Skills/SkillsCommands.cs @@ -1,6 +1,5 @@ using Content.Server.Administration; using Content.Shared.Administration; -using Content.Shared.Borgs; using Robust.Shared.Console; using Content.Server.Players; using Robust.Server.Player; @@ -9,106 +8,17 @@ 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 sealed class SkillUiCommand : 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; -// } +// public string Command => "skillui"; +// public string Description => Loc.GetString("command-skillui-description"); +// public string Help => Loc.GetString("command-skillui-help"); -// 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 uiSystem = IoCManager.Resolve(); // var player = shell.Player as IPlayerSession; // EntityUid? entity = null; @@ -131,102 +41,47 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args) // return; // } -// if (!entityManager.TryGetComponent(entity.Value, out var skills)) -// { -// shell.WriteLine("Entity has no skills component to clear"); +// if (!uiSystem.TryGetUi(entity.Value, SkillsUiKey.Key, out _)) // return; -// } -// entityManager.EntitySysManager.GetEntitySystem().ClearSkills(entity.Value, skills); +// uiSystem.TryOpen(entity.Value, SkillsUiKey.Key, player!); // } // } -// [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; -// } +[AdminCommand(AdminFlags.Logs)] +public sealed class SkillModifyCommand : IConsoleCommand +{ + public string Command => "skillmodify"; + public string Description => Loc.GetString("command-skillmodify-description"); + public string Help => Loc.GetString("command-skillmodify-help"); -// 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; -// } + public async void Execute(IConsoleShell shell, string argStr, string[] args) + { + var entityManager = IoCManager.Resolve(); + var player = shell.Player as IPlayerSession; + EntityUid? entity = null; + var skillSystem = entityManager.EntitySysManager.GetEntitySystem(); -// if (entity == null) -// { -// shell.WriteLine("Can't find entity."); -// return; -// } + if (args.Length < 3 && player != null) + { + entity = player.ContentData()?.Mind?.CurrentEntity; + } + else if (IoCManager.Resolve().TryGetPlayerDataByUsername(args[2], out var data)) + { + entity = data.ContentData()?.Mind?.CurrentEntity; + } + else if (EntityUid.TryParse(args[2], out var foundEntity)) + { + entity = foundEntity; + } -// if (!entityManager.TryGetComponent(entity, out var skills)) -// { -// shell.WriteLine("Entity has no skills to remove!"); -// return; -// } + if (entity == null) + { + shell.WriteLine("Can't find entity."); + 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); -// } -// } + if (!skillSystem.TryModifySkillLevel(entity.Value, args[0], int.Parse(args[1]))) + shell.WriteLine("Failed to modify skill."); + } +} diff --git a/Content.Shared/SimpleStation14/Skills/Components/SkillsComponent.cs b/Content.Shared/SimpleStation14/Skills/Components/SkillsComponent.cs index 1e5f0613c8..3b515095be 100644 --- a/Content.Shared/SimpleStation14/Skills/Components/SkillsComponent.cs +++ b/Content.Shared/SimpleStation14/Skills/Components/SkillsComponent.cs @@ -7,11 +7,14 @@ namespace Content.Shared.SimpleStation14.Skills.Components { + /// + /// This holds all the data on an entity's skills. + /// It should generally never be accessed directly, and instead accessed through . + /// No, not event just to get a single value. I worked hard on those functions ;~;. + /// [RegisterComponent] [NetworkedComponent] [AutoGenerateComponentState] - [Serializable] - [NetSerializable] public sealed partial class SkillsComponent : Component { /// @@ -19,6 +22,7 @@ public sealed partial class SkillsComponent : Component /// If you're using this- don't. Use to access it. /// [AutoNetworkedField] + [ViewVariables(VVAccess.ReadOnly)] public Dictionary Skills = new(); /// diff --git a/Content.Shared/SimpleStation14/Skills/Prototypes/SkillCategoryPrototype.cs b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillCategoryPrototype.cs index beaf75eb2e..e8af3ab65b 100644 --- a/Content.Shared/SimpleStation14/Skills/Prototypes/SkillCategoryPrototype.cs +++ b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillCategoryPrototype.cs @@ -16,20 +16,20 @@ public sealed class SkillCategoryPrototype : IPrototype // [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 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; + // /// + // /// 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. diff --git a/Content.Shared/SimpleStation14/Skills/Prototypes/SkillPrototype.cs b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillPrototype.cs index 534264ce32..a707a8aa8e 100644 --- a/Content.Shared/SimpleStation14/Skills/Prototypes/SkillPrototype.cs +++ b/Content.Shared/SimpleStation14/Skills/Prototypes/SkillPrototype.cs @@ -1,3 +1,4 @@ +using Content.Shared.SimpleStation14.Skills.Systems; using Content.Shared.Whitelist; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations; @@ -21,6 +22,9 @@ public sealed class SkillPrototype : IPrototype [DataField("subCategory", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] public string SubCategory = default!; + [DataField("maxValue")] + public int MaxValue = 5; + /// /// If true, this skill's icon won't be colored by the sub category's color. /// @@ -39,16 +43,16 @@ public sealed class SkillPrototype : IPrototype /// Skills lower will make less of an impact. /// [DataField("weight")] - public int Weight = 1; + public float 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; + // /// + // /// 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. diff --git a/Content.Shared/SimpleStation14/Skills/Systems/SharedSkillsSystem.cs b/Content.Shared/SimpleStation14/Skills/Systems/SharedSkillsSystem.cs index febb35284a..538260a6ac 100644 --- a/Content.Shared/SimpleStation14/Skills/Systems/SharedSkillsSystem.cs +++ b/Content.Shared/SimpleStation14/Skills/Systems/SharedSkillsSystem.cs @@ -4,6 +4,8 @@ using Content.Shared.SimpleStation14.Skills.Prototypes; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; namespace Content.Shared.SimpleStation14.Skills.Systems; @@ -23,111 +25,162 @@ public override void Initialize() _prototype.PrototypesReloaded += OnPrototypesReloaded; } - public bool TryModifySkillLevel(string skill, int level) + # region Public Functions + /// Forces the level of a skill for a given entity. + /// + /// The value to set the skill level to. + /// WARNING: This bypasses several checks, such as species restrictions, max skill level, etc.. Use sparingly. + public void ForceSkillLevel(EntityUid uid, string skillId, int level, SkillsComponent? skillsComp = null) { - if (!_prototype.HasIndex(skill)) - return false; - + SetSkillInternal(uid, skillId, level, skillsComp, true); + } + /// Tries to modify the level of a skill for a given entity. + /// + /// The amount to modify the skill level by. + /// True if the skill level was successfully modified, false otherwise. + public bool TryModifySkillLevel(EntityUid uid, string skillId, int level, SkillsComponent? skillsComp = null) + { + return SetSkillInternal(uid, skillId, GetSkillLevel(uid, skillId, skillsComp) + level, skillsComp); } - public bool TrySetSkillLevel(string skill, int level) + /// Tries to set the level of a skill for a given entity. + /// The value to set the skill level to. + /// + public bool TrySetSkillLevel(EntityUid uid, string skillId, int level, SkillsComponent? skillsComp = null) { + return SetSkillInternal(uid, skillId, level, skillsComp); + } + /// Tries to get the level of a specific skill for an entity. + /// + /// The level of the skill, or 0 if none is found. + /// True if the entity is capable of having skills, otherwise false. + public bool TryGetSkillLevel(EntityUid uid, string skillId, out int level, SkillsComponent? skillsComp = null, bool raw = false) + { + return GetSkillInternal(uid, skillId, out level, skillsComp, raw); } - /// - /// 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) + /// Returns the level of an entity's skill, defaulting to 0. + /// + /// The level of the entity's skill. + public int GetSkillLevel(EntityUid uid, string skillId, SkillsComponent? skillsComp = null, bool raw = false) { - if (!Resolve(uid, ref skillsComp)) + if (!TryGetSkillLevel(uid, skillId, out var level, skillsComp, raw)) return 0; + return level; + } - if (skillsComp.Skills.TryGetValue(skillId, out var level)) - return level; + /// Returns the overall level of a set of skills, altered by their weights. + /// + /// A list of skill IDs to get the average skill level of. + /// The average of all the skills as a float, or zero if none could be found. + public float GetSkillAverages(EntityUid uid, List skillIds, SkillsComponent? skillsComp = null, bool raw = false) + { + return GetSkillAverages(uid, skillIds.ConvertAll(_prototype.Index), skillsComp, raw); + } - return 0; + /// + /// A list of skill prototypes to get the average skill level of. + public float GetSkillAverages(EntityUid uid, List skills, SkillsComponent? skillsComp = null, bool raw = false) + { + return GetSkillAverages(uid, skillWeights: skills.ToDictionary(skill => skill, skill => skill.Weight), skillsComp, raw); } - /// - /// 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) + /// + /// A dictionary of skill IDs and their weights to get the average skill level of. + /// Uses custom weights for each skill, rather than their default. + public float GetSkillAverages(EntityUid uid, Dictionary skillIdWeights, SkillsComponent? skillsComp = null, bool raw = false) { - if (!Resolve(uid, ref skillsComp)) - return 0; + return GetSkillAverages(uid, skillWeights: skillIdWeights.ToDictionary(pair => _prototype.Index(pair.Key), pair => pair.Value), skillsComp, raw); + } + + /// + /// A dictionary of skill prototypes and their weights to get the average skill level of. + public float GetSkillAverages(EntityUid uid, Dictionary skillWeights, SkillsComponent? skillsComp = null, bool raw = false) + { + var totalWeight = 0f; + var totalLevel = 0f; + foreach (var (skill, weight) in skillWeights) + { + if (!GetSkillInternal(uid, skill.ID, out var level, skillsComp, raw)) + continue; + + totalLevel += level * weight; + totalWeight += weight; + } + + return totalLevel / totalWeight; + } + + /// 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 float GetSubCategoryLevel(EntityUid uid, string subCategoryId, SkillsComponent? skillsComp = null) + { if (!TryGetSkillSubCategory(subCategoryId, out var subCategory)) return 0; + return GetSubCategoryLevel(uid, subCategory, skillsComp); + } + + /// + public float GetSubCategoryLevel(EntityUid uid, SkillSubCategoryPrototype subCategory, SkillsComponent? skillsComp = null) + { 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; + return GetSkillAverages(uid, _categorizedSkills[category][subCategory], skillsComp); } - /// - /// 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) + /// Used to determine whether or not an Entity is 'trained' in a particular skill. + /// + /// True if the entity has at least one level in the skill. + public bool IsSkillTrained(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) + /// Returns a Dictionary of all the skills an Entity has levels in. + /// Note that this will only return skills that the Entity has greater than 0 levels in i.e. are trained in. + /// + /// A Dictionary of skills and levels. + /// True if the entity is capable of having skills, false otherwise. + public bool TryGetAllSkillLevels(EntityUid uid, out Dictionary skillLevels, SkillsComponent? skillsComp = null) { + skillLevels = new Dictionary(); + if (!Resolve(uid, ref skillsComp)) - return new Dictionary(); + return false; + + foreach (var skillId in skillsComp.Skills.Keys) + { + if (!GetSkillInternal(uid, skillId, out var level, skillsComp)) + continue; - return new(skillsComp.Skills); + skillLevels.Add(skillId, level); + } + + return true; } - /// - /// Gets all skills in the game organized by category, and then sub category. - /// + /// 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. + /// Tries to get all skills in a given category. + /// + /// 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)) + if (!SkillIndex(categoryId, out SkillCategoryPrototype? category)) return false; if (!_categorizedSkills.TryGetValue(category, out skillsBySubCategory)) @@ -136,17 +189,15 @@ public bool TryGetSkillsInCategory(string categoryId, [NotNullWhen(true)] out Di 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. + /// Tries to get all skills in a given sub category. + /// + /// 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)) + if (!SkillIndex(subCategoryId, out SkillSubCategoryPrototype? subCategory)) return false; if (!TryGetSkillsInCategory(subCategory.Category, out var skillsBySubCategory)) @@ -158,62 +209,109 @@ public bool TryGetSkillsInSubCategory(string subCategoryId, [NotNullWhen(true)] 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. + /// Tries to get a skill by its ID. + /// + /// True if the skill was found, false otherwise. public bool TryGetSkill(string skillId, [NotNullWhen(true)] out SkillPrototype? skill) { - return _prototype.TryIndex(skillId, out skill); + return SkillIndex(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. + /// Tries to get a skill subcategory by its ID. + /// + /// 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); + return SkillIndex(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. + /// Tries to get a skill category by its ID. + /// + /// 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); + return SkillIndex(categoryId, out category); } + #endregion + #region Private Functions private void OnSkillsInit(EntityUid uid, SkillsComponent component, ComponentInit args) { - component.Skills.Add + foreach (var startSkill in component.StartingSkills) + TryModifySkillLevel(uid, startSkill.Key, startSkill.Value, component); } - private void SetSkill(EntityUid uid, string skillId, int level, SkillsComponent? skillsComp = null) + private bool GetSkillInternal(EntityUid uid, string skillId, out int level, SkillsComponent? skillsComp = null, bool raw = false) { + level = 0; + if (!Resolve(uid, ref skillsComp)) - return; + return false; + + if (!SkillIndex(skillId, out _)) + return false; + skillsComp.Skills.TryGetValue(skillId, out level); + + if (!raw) + { + var ev = new GetSkillEvent(skillId, level); + RaiseLocalEvent(uid, ref ev); + + level = ev.Level; + } + + return true; + } + + private bool SetSkillInternal(EntityUid uid, string skillId, int level, SkillsComponent? skillsComp = null, bool force = false) + { + if (!SkillIndex(skillId, out var skill)) + return false; // Return if the skill isn't real. + + return SetSkillInternal(uid, skill, level, skillsComp, force); + } + + private bool SetSkillInternal(EntityUid uid, SkillPrototype skill, int level, SkillsComponent? skillsComp = null, bool force = false) + { + if (!Resolve(uid, ref skillsComp)) + return false; + + // If the skill is on the blacklist, or not on the whitelist, return. + if (!force && + (skill.Whitelist != null && !skill.Whitelist.IsValid(uid) || skill.Blacklist != null && skill.Blacklist.IsValid(uid))) + return false; + + // Cap the skill to its own max value. + if (!force) + level = level > skill.MaxValue ? skill.MaxValue : level; + + if (!force) + { + var cancelEvent = new SkillModifyAttemptEvent(skill.ID, level, GetSkillLevel(uid, skill.ID, skillsComp, true)); + RaiseLocalEvent(uid, ref cancelEvent); + if (cancelEvent.Cancelled) + return false; + } + + var ev = new SkillModifiedEvent(skill.ID, level, GetSkillLevel(uid, skill.ID, skillsComp, true)); + RaiseLocalEvent(uid, ref ev); + + // If the level is 0 or less, simply remove the skill from their component. if (level <= 0) { - if (skillsComp.Skills.ContainsKey(skillId)) - skillsComp.Skills.Remove(skillId); - return; + skillsComp.Skills.Remove(skill.ID); + return true; } - if (skillsComp.Skills.ContainsKey(skillId)) - skillsComp.Skills[skillId] = level; + // Finally, set or add the skill to the component. + if (skillsComp.Skills.ContainsKey(skill.ID)) + skillsComp.Skills[skill.ID] = level; else - skillsComp.Skills.Add(skillId, level); + skillsComp.Skills.Add(skill.ID, level); Dirty(skillsComp); + return true; } private void GenerateCategorizedSkills() @@ -241,14 +339,108 @@ private void GenerateCategorizedSkills() _categorizedSkills = skillsCategorized; } + private bool SkillIndex(string prototypeId, [NotNullWhen(true)] out T? prototype) where T : class, IPrototype + { + if (!_prototype.TryIndex(prototypeId, out prototype)) + { + Log.Warning($"Unknown {typeof(T).Name} prototype: {prototypeId}"); + return false; + } + return true; + } + private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) { GenerateCategorizedSkills(); } + + // This is pretty goofy, I know, but how many times am I repeating these parameters? + /// The entity UID. + /// The SkillsComponent to check on, if avaliable. + /// The skill prototype. + /// The ID of the skill. + /// The skill category prototype. + /// The ID of the skill category. + /// The skill sub category prototype. + /// The ID of the skill sub category. + private static void DefaultXmlDocs() + { + } + #endregion +} + +#region Events +[ByRefEvent] +/// Cancellable Event called before a skill level is modified. Can be used to modify the new value. +/// The skill prototype ID. +/// The new level of the skill. +/// The old level of the skill. +public record struct SkillModifyAttemptEvent(string SkillId, int NewLevel, int OldLevel) +{ + public int NewLevel = NewLevel; + public bool Cancelled = false; + + public readonly string SkillId = SkillId; + public readonly int OldLevel = OldLevel; + public readonly bool Increased => NewLevel > OldLevel; +} + +[ByRefEvent] +/// Event called when a skill level is modified. +/// +public readonly record struct SkillModifiedEvent(string SkillId, int NewLevel, int OldLevel) +{ + // /// The ID of the skill being modified. + // public readonly string SkillId = SkillId; + + // /// The new level of the skill. + // public readonly int NewLevel = NewLevel; + + // /// The old level of the skill. + // public readonly int OldLevel = OldLevel; + + /// Whether the skill level was increased (true) or decreased (false). + public readonly bool Increased => NewLevel > OldLevel; } +/// Event called when attempting to get a skill level. +/// Can be used to modify the level returned. +/// The skill prototype ID. +[ByRefEvent] +public record struct GetSkillEvent(string SkillId, int Level) +{ + /// The ID of the skill being checked. + public readonly string SkillId = SkillId; + + /// The level of the skill. + public int Level = Level; +} +#endregion +#region Various Data + +// [DataDefinition, Serializable, NetSerializable] +// public struct SkillUseData +// { +// /// The skill prototype IDs to use. +// /// Overrides if not null. +// [DataField("skills", customTypeSerializer: typeof(PrototypeIdListSerializer))] +// public List? Skills; + +// /// The skill sub category prototype IDs to use. +// /// Ignored if is not null. +// [DataField("skillSubCategory", customTypeSerializer: typeof(PrototypeIdSerializer))] +// public string? SkillSubCategory; + +// /// Are you required to be trained in the skill to use it? +// /// If used with a list of skills, the individual skill will be ignored if not trained. +// [DataField("requireTrained")] +// public bool RequireTrained = false; +// } + + [NetSerializable, Serializable] public enum SkillsUiKey : byte { Key, } +#endregion diff --git a/Content.Shared/Tools/Components/ToolComponent.cs b/Content.Shared/Tools/Components/ToolComponent.cs index 45fde79d4e..d11531a4fe 100644 --- a/Content.Shared/Tools/Components/ToolComponent.cs +++ b/Content.Shared/Tools/Components/ToolComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.SimpleStation14.Skills.Systems; // Parkstation-Skills using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Utility; @@ -19,6 +20,23 @@ public sealed class ToolComponent : Component [DataField("useSound")] public SoundSpecifier? UseSound { get; set; } + + // Parkstation-Skills-Start + /// + /// The for the usage of this tool. + /// + [DataField("skillUsed")] + public string? SkillUsed { get; set; } + + /// + /// The 'expected' skill level for this tool. Below this level lowers the use speed, above it raises it. + /// + /// + /// Unused if is null. + /// + [DataField("skillExpected")] + public float SkillExpected { get; set; } = 3; + // Parkstation-Skills-End } /// diff --git a/Content.Shared/Tools/Systems/SharedToolSystem.cs b/Content.Shared/Tools/Systems/SharedToolSystem.cs index 989610797e..60e9a757a7 100644 --- a/Content.Shared/Tools/Systems/SharedToolSystem.cs +++ b/Content.Shared/Tools/Systems/SharedToolSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.DoAfter; +using Content.Shared.SimpleStation14.Skills.Systems; // Parkstation-Skills using Content.Shared.Tools.Components; using Robust.Shared.Map; using Robust.Shared.Prototypes; @@ -13,6 +14,7 @@ public abstract partial class SharedToolSystem : EntitySystem [Dependency] private readonly IPrototypeManager _protoMan = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedSkillsSystem _skillsSystem = default!; // Parkstation-Skills public override void Initialize() { @@ -108,6 +110,16 @@ public bool UseTool( if (!CanStartToolUse(tool, user, target, toolQualitiesNeeded, toolComponent)) return false; + // Parkstation-Skills-Start + Log.Error(delay.ToString()); + if (toolComponent.SkillUsed != null && _skillsSystem.TryGetSkillLevel(user, toolComponent.SkillUsed, out var skill)) + delay *= toolComponent.SkillExpected / (skill > 0 ? skill : 0.5); + Log.Error(_skillsSystem.TryGetSkillLevel(user, toolComponent.SkillUsed!, out _).ToString()); + Log.Error(toolComponent.SkillUsed!); + Log.Error(toolComponent.SkillExpected.ToString()); + Log.Error(delay.ToString()); + // Parkstation-Skills-End + var toolEvent = new ToolDoAfterEvent(doAfterEv, target); var doAfterArgs = new DoAfterArgs(user, delay / toolComponent.SpeedModifier, toolEvent, tool, target: target, used: tool) { diff --git a/Resources/Prototypes/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/Entities/Objects/Tools/tools.yml index 60cd6ee284..70e0fb6b52 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/tools.yml @@ -33,6 +33,8 @@ - Cutting useSound: path: /Audio/Items/wirecutter.ogg + skillUsed: SkillElectrical + skillExpected: 3 - type: RandomSprite available: - enum.DamageStateVisualLayers.Base: @@ -82,6 +84,8 @@ - Screwing useSound: collection: Screwdriver + skillUsed: SkillElectrical + skillExpected: 2 - type: RandomSprite available: - enum.DamageStateVisualLayers.Base: @@ -128,6 +132,8 @@ - Anchoring useSound: path: /Audio/Items/ratchet.ogg + skillUsed: SkillMechanical + skillExpected: 2 - type: PhysicalComposition materialComposition: Steel: 100 @@ -166,6 +172,8 @@ - Prying useSound: path: /Audio/Items/crowbar.ogg + skillUsed: SkillMechanical + skillExpected: 3 - type: TilePrying - type: PhysicalComposition materialComposition: @@ -219,6 +227,8 @@ - type: Tool qualities: - Pulsing + skillUsed: SkillElectrical + skillExpected: 7 - type: NetworkConfigurator - type: ActivatableUI key: enum.NetworkConfiguratorUiKey.List diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml index 4888fec2f5..2423a407b5 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml @@ -36,6 +36,11 @@ components: - type: PsionicBonusChance flatBonus: 0.025 + - !type:RoleSkillsSpecial + skills: + SkillMechanical: 7 + SkillElectrical: 8 + SkillQuantumPhysics: 3 - type: startingGear id: ChiefEngineerGear diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml index acd0b2d6cb..fe6ed2bed1 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml @@ -16,6 +16,11 @@ - External extendedAccess: - Atmospherics + special: + - !type:RoleSkillsSpecial + skills: + SkillMechanical: 4 + SkillElectrical: 3 - type: startingGear id: StationEngineerGear