diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index 66e2898088..e86493ab70 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -79,6 +79,7 @@ namespace Content.Client prototypes.RegisterIgnore("gasReaction"); prototypes.RegisterIgnore("seed"); // Seeds prototypes are server-only. prototypes.RegisterIgnore("barSign"); + prototypes.RegisterIgnore("objective"); ClientContentIoC.Register(); diff --git a/Content.Client/GameObjects/Components/Actor/CharacterInfoComponent.cs b/Content.Client/GameObjects/Components/Actor/CharacterInfoComponent.cs index 1a33b83398..28f7ffb381 100644 --- a/Content.Client/GameObjects/Components/Actor/CharacterInfoComponent.cs +++ b/Content.Client/GameObjects/Components/Actor/CharacterInfoComponent.cs @@ -1,26 +1,30 @@ +#nullable enable +using System.Drawing; using Content.Client.GameObjects.Components.Mobs; using Content.Client.UserInterface; using Content.Client.UserInterface.Stylesheets; +using Content.Shared.GameObjects.Components.Actor; using Robust.Client.Interfaces.GameObjects.Components; using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Client.Utility; using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.Network; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Players; namespace Content.Client.GameObjects.Components.Actor { [RegisterComponent] - public sealed class CharacterInfoComponent : Component, ICharacterUI + public sealed class CharacterInfoComponent : SharedCharacterInfoComponent, ICharacterUI { [Dependency] private readonly IResourceCache _resourceCache = default!; - private CharacterInfoControl _control; + private CharacterInfoControl _control = default!; - public override string Name => "CharacterInfo"; - - public Control Scene { get; private set; } + public Control Scene { get; private set; } = default!; public UIPriority Priority => UIPriority.Info; public override void OnAdd() @@ -30,18 +34,29 @@ namespace Content.Client.GameObjects.Components.Actor Scene = _control = new CharacterInfoControl(_resourceCache); } - public override void Initialize() + public void Opened() { - base.Initialize(); + SendNetworkMessage(new RequestCharacterInfoMessage()); + } - if (Owner.TryGetComponent(out ISpriteComponent spriteComponent)) + public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null) + { + base.HandleNetworkMessage(message, netChannel, session); + + if(session?.AttachedEntity != Owner) return; + + switch (message) { - _control.SpriteView.Sprite = spriteComponent; - } + case CharacterInfoMessage characterInfoMessage: + _control.UpdateUI(characterInfoMessage); + if (Owner.TryGetComponent(out ISpriteComponent? spriteComponent)) + { + _control.SpriteView.Sprite = spriteComponent; + } - _control.NameLabel.Text = Owner.Name; - // ReSharper disable once StringLiteralTypo - _control.SubText.Text = Loc.GetString("Professional Greyshirt"); + _control.NameLabel.Text = Owner.Name; + break; + } } private sealed class CharacterInfoControl : VBoxContainer @@ -50,8 +65,12 @@ namespace Content.Client.GameObjects.Components.Actor public Label NameLabel { get; } public Label SubText { get; } + public VBoxContainer ObjectivesContainer { get; } + public CharacterInfoControl(IResourceCache resourceCache) { + IoCManager.InjectDependencies(this); + AddChild(new HBoxContainer { Children = @@ -66,7 +85,8 @@ namespace Content.Client.GameObjects.Components.Actor (SubText = new Label { SizeFlagsVertical = SizeFlags.None, - StyleClasses = {StyleNano.StyleClassLabelSubText} + StyleClasses = {StyleNano.StyleClassLabelSubText}, + }) } } @@ -78,16 +98,65 @@ namespace Content.Client.GameObjects.Components.Actor PlaceholderText = Loc.GetString("Health & status effects") }); - AddChild(new Placeholder(resourceCache) + AddChild(new Label { - PlaceholderText = Loc.GetString("Objectives") + Text = Loc.GetString("Objectives"), + SizeFlagsHorizontal = SizeFlags.ShrinkCenter }); + ObjectivesContainer = new VBoxContainer(); + AddChild(ObjectivesContainer); AddChild(new Placeholder(resourceCache) { PlaceholderText = Loc.GetString("Antagonist Roles") }); } + + public void UpdateUI(CharacterInfoMessage characterInfoMessage) + { + SubText.Text = characterInfoMessage.JobTitle; + + ObjectivesContainer.RemoveAllChildren(); + foreach (var (groupId, objectiveConditions) in characterInfoMessage.Objectives) + { + var vbox = new VBoxContainer + { + Modulate = Color.Gray + }; + + vbox.AddChild(new Label + { + Text = groupId, + Modulate = Color.LightSkyBlue + }); + + foreach (var objectiveCondition in objectiveConditions) + { + var hbox = new HBoxContainer(); + hbox.AddChild(new ProgressTextureRect + { + Texture = objectiveCondition.SpriteSpecifier.Frame0(), + Progress = objectiveCondition.Progress, + SizeFlagsVertical = SizeFlags.ShrinkCenter + }); + hbox.AddChild(new Control + { + CustomMinimumSize = (10,0) + }); + hbox.AddChild(new VBoxContainer + { + Children = + { + new Label{Text = objectiveCondition.Title}, + new Label{Text = objectiveCondition.Description} + } + } + ); + vbox.AddChild(hbox); + } + ObjectivesContainer.AddChild(vbox); + } + } } } } diff --git a/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs b/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs index f776411554..d96fef4f6c 100644 --- a/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs +++ b/Content.Client/GameObjects/Components/Actor/CharacterInterface.cs @@ -116,6 +116,7 @@ namespace Content.Client.GameObjects.Components.Actor public class CharacterWindow : SS14Window { private readonly VBoxContainer _contentsVBox; + private readonly List _windowComponents; public CharacterWindow(List windowComponents) { @@ -129,6 +130,17 @@ namespace Content.Client.GameObjects.Components.Actor { _contentsVBox.AddChild(element.Scene); } + + _windowComponents = windowComponents; + } + + protected override void Opened() + { + base.Opened(); + foreach (var windowComponent in _windowComponents) + { + windowComponent.Opened(); + } } } } diff --git a/Content.Client/GameObjects/Components/Actor/ProgressTextureRect.cs b/Content.Client/GameObjects/Components/Actor/ProgressTextureRect.cs new file mode 100644 index 0000000000..40f95f717b --- /dev/null +++ b/Content.Client/GameObjects/Components/Actor/ProgressTextureRect.cs @@ -0,0 +1,22 @@ +using System; +using Content.Client.GameObjects.EntitySystems.DoAfter; +using Robust.Client.Graphics.Drawing; +using Robust.Client.UserInterface.Controls; +using Robust.Shared.Maths; + +namespace Content.Client.GameObjects.Components.Actor +{ + public class ProgressTextureRect : TextureRect + { + public float Progress; + + protected override void Draw(DrawingHandleScreen handle) + { + var dims = Texture != null ? GetDrawDimensions(Texture) : UIBox2.FromDimensions(Vector2.Zero, PixelSize); + dims.Top = Math.Max(dims.Bottom - dims.Bottom * Progress,0); + handle.DrawRect(dims, DoAfterHelpers.GetProgressColor(Progress)); + + base.Draw(handle); + } + } +} diff --git a/Content.Client/GameObjects/Components/Mobs/ICharacterUI.cs b/Content.Client/GameObjects/Components/Mobs/ICharacterUI.cs index 748bdf40bc..ef9c3a6821 100644 --- a/Content.Client/GameObjects/Components/Mobs/ICharacterUI.cs +++ b/Content.Client/GameObjects/Components/Mobs/ICharacterUI.cs @@ -17,5 +17,10 @@ namespace Content.Client.GameObjects.Components.Mobs /// The order it will appear in the character UI, higher is lower /// UIPriority Priority { get; } + + /// + /// Called when the CharacterUi was opened + /// + void Opened(){} } } diff --git a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterBar.cs b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterBar.cs index 172c05d047..c612670e5e 100644 --- a/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterBar.cs +++ b/Content.Client/GameObjects/EntitySystems/DoAfter/DoAfterBar.cs @@ -1,4 +1,4 @@ -#nullable enable +#nullable enable using System; using Robust.Client.Graphics.Drawing; using Robust.Client.Graphics.Shaders; @@ -105,16 +105,10 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter } color = new Color(1.0f, 0.0f, 0.0f, _flash ? 1.0f : 0.0f); - } - else if (Ratio >= 1.0f) - { - color = new Color(0f, 1f, 0f); - } + } else { - // lerp - var hue = (5f / 18f) * Ratio; - color = Color.FromHsv((hue, 1f, 0.75f, 1f)); + color = DoAfterHelpers.GetProgressColor(Ratio); } handle.UseShader(_shader); @@ -128,4 +122,18 @@ namespace Content.Client.GameObjects.EntitySystems.DoAfter handle.DrawRect(box, color); } } + + public static class DoAfterHelpers + { + public static Color GetProgressColor(float progress) + { + if (progress >= 1.0f) + { + return new Color(0f, 1f, 0f); + } + // lerp + var hue = (5f / 18f) * progress; + return Color.FromHsv((hue, 1f, 0.75f, 1f)); + } + } } diff --git a/Content.Server/GameObjects/Components/Actor/CharacterInfoComponent.cs b/Content.Server/GameObjects/Components/Actor/CharacterInfoComponent.cs new file mode 100644 index 0000000000..7f4c040725 --- /dev/null +++ b/Content.Server/GameObjects/Components/Actor/CharacterInfoComponent.cs @@ -0,0 +1,58 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.Mobs.Roles; +using Content.Shared.GameObjects.Components.Actor; +using Content.Shared.Objectives; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Players; + +namespace Content.Server.GameObjects.Components.Actor +{ + [RegisterComponent] + public class CharacterInfoComponent : SharedCharacterInfoComponent + { + public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null) + { + switch (message) + { + case RequestCharacterInfoMessage _: + var conditions = new Dictionary>(); + var jobTitle = "No Profession"; + if (Owner.TryGetComponent(out MindComponent? mindComponent)) + { + var mind = mindComponent.Mind; + + if (mind != null) + { + // getting conditions + foreach (var objective in mind.AllObjectives) + { + if (!conditions.ContainsKey(objective.Issuer)) + conditions[objective.Issuer] = new List(); + foreach (var condition in objective.Conditions) + { + conditions[objective.Issuer].Add(new ConditionInfo(condition.GetTitle(), + condition.GetDescription(), condition.GetIcon(), condition.GetProgress(mindComponent.Mind))); + } + } + + // getting jobtitle + foreach (var role in mind.AllRoles) + { + if (role.GetType() == typeof(Job)) + { + jobTitle = role.Name; + break; + } + } + } + } + SendNetworkMessage(new CharacterInfoMessage(jobTitle, conditions)); + break; + } + } + } +} diff --git a/Content.Server/GameObjects/Components/ContainerExt/ContainerExt.cs b/Content.Server/GameObjects/Components/ContainerExt/ContainerExt.cs new file mode 100644 index 0000000000..8263cca814 --- /dev/null +++ b/Content.Server/GameObjects/Components/ContainerExt/ContainerExt.cs @@ -0,0 +1,23 @@ +using Robust.Server.GameObjects.Components.Container; + +namespace Content.Server.GameObjects.Components.ContainerExt +{ + public static class ContainerExt + { + public static int CountPrototypeOccurencesRecursive(this ContainerManagerComponent mgr, string prototypeId) + { + int total = 0; + foreach (var container in mgr.GetAllContainers()) + { + foreach (var entity in container.ContainedEntities) + { + if (entity.Prototype?.ID == prototypeId) total++; + if(!entity.TryGetComponent(out var component)) continue; + total += component.CountPrototypeOccurencesRecursive(prototypeId); + } + } + + return total; + } + } +} diff --git a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs index 8ae15d2640..78af6ec95a 100644 --- a/Content.Server/GameObjects/Components/Mobs/MindComponent.cs +++ b/Content.Server/GameObjects/Components/Mobs/MindComponent.cs @@ -16,7 +16,6 @@ using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Serialization; -using Robust.Shared.Timers; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; diff --git a/Content.Server/IgnoredComponents.cs b/Content.Server/IgnoredComponents.cs index 59b7053ca7..0be2eccd08 100644 --- a/Content.Server/IgnoredComponents.cs +++ b/Content.Server/IgnoredComponents.cs @@ -10,7 +10,6 @@ "SubFloorHide", "LowWall", "ReinforcedWall", - "CharacterInfo", "InteractionOutline", "MeleeWeaponArcAnimation", "AnimationsTest", diff --git a/Content.Server/Mobs/Mind.cs b/Content.Server/Mobs/Mind.cs index 9adcb4a776..77d5c23394 100644 --- a/Content.Server/Mobs/Mind.cs +++ b/Content.Server/Mobs/Mind.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Content.Server.GameObjects.Components.Mobs; using Content.Server.Mobs.Roles; +using Content.Server.Objectives; using Content.Server.Players; using Robust.Server.Interfaces.GameObjects; using Robust.Server.Interfaces.Player; @@ -27,6 +28,8 @@ namespace Content.Server.Mobs { private readonly ISet _roles = new HashSet(); + private readonly List _objectives = new List(); + /// /// Creates the new mind attached to a specific player session. /// @@ -74,6 +77,12 @@ namespace Content.Server.Mobs [ViewVariables] public IEnumerable AllRoles => _roles; + /// + /// An enumerable over all the objectives this mind has. + /// + [ViewVariables] + public IEnumerable AllObjectives => _objectives; + /// /// The session of the player owning this mind. /// Can be null, in which case the player is currently not logged in. @@ -144,6 +153,32 @@ namespace Content.Server.Mobs return _roles.Any(role => role.GetType() == t); } + /// + /// Adds an objective to this mind. + /// + public bool TryAddObjective(ObjectivePrototype objective) + { + if (!objective.CanBeAssigned(this)) + return false; + _objectives.Add(objective); + return true; + } + + /// + /// Removes an objective to this mind. + /// + /// Returns true if the removal succeeded. + public bool TryRemoveObjective(int index) + { + if (_objectives.Count >= index) return false; + + var objective = _objectives[index]; + _objectives.Remove(objective); + return true; + } + + + /// /// Transfer this mind's control over to a new entity. /// diff --git a/Content.Server/Objectives/Commands.cs b/Content.Server/Objectives/Commands.cs new file mode 100644 index 0000000000..47ecdd4f8a --- /dev/null +++ b/Content.Server/Objectives/Commands.cs @@ -0,0 +1,139 @@ +#nullable enable +using System.Linq; +using Content.Server.Administration; +using Content.Server.Players; +using Content.Shared.Administration; +using Robust.Server.Interfaces.Console; +using Robust.Server.Interfaces.Player; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; + +namespace Content.Server.Objectives +{ + [AdminCommand(AdminFlags.Admin)] + public class AddObjectiveCommand : IClientCommand + { + public string Command => "addobjective"; + public string Description => "Adds an objective to the player's mind."; + public string Help => "addobjective "; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (args.Length != 2) + { + shell.SendText(player, "Expected exactly 2 arguments."); + return; + } + + var mgr = IoCManager.Resolve(); + if (!mgr.TryGetPlayerDataByUsername(args[0], out var data)) + { + shell.SendText(player, "Can't find the playerdata."); + return; + } + + + var mind = data.ContentData()?.Mind; + if (mind == null) + { + shell.SendText(player, "Can't find the mind."); + return; + } + + if (!IoCManager.Resolve() + .TryIndex(args[1], out var objectivePrototype)) + { + shell.SendText(player, $"Can't find matching ObjectivePrototype {objectivePrototype}"); + return; + } + + if (!mind.TryAddObjective(objectivePrototype)) + { + shell.SendText(player, "Objective requirements dont allow that objective to be added."); + } + + } + } + + [AdminCommand(AdminFlags.Admin)] + public class ListObjectivesCommand : IClientCommand + { + public string Command => "lsobjectives"; + public string Description => "Lists all objectives in a players mind."; + public string Help => "lsobjectives []"; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + IPlayerData? data; + if (args.Length == 0 && player != null) + { + data = player.Data; + } + else if (player == null || !IoCManager.Resolve().TryGetPlayerDataByUsername(args[0], out data)) + { + shell.SendText(player, "Can't find the playerdata."); + return; + } + + var mind = data.ContentData()?.Mind; + if (mind == null) + { + shell.SendText(player, "Can't find the mind."); + return; + } + + shell.SendText(player, $"Objectives for player {data.UserId}:"); + var objectives = mind.AllObjectives.ToList(); + if (objectives.Count == 0) + { + shell.SendText(player, "None."); + } + for (var i = 0; i < objectives.Count; i++) + { + shell.SendText(player, $"- [{i}] {objectives[i]}"); + } + + } + } + + [AdminCommand(AdminFlags.Admin)] + public class RemoveObjectiveCommand : IClientCommand + { + public string Command => "rmobjective"; + public string Description => "Removes an objective from the player's mind."; + public string Help => "rmobjective "; + public void Execute(IConsoleShell shell, IPlayerSession? player, string[] args) + { + if (args.Length != 2) + { + shell.SendText(player, "Expected exactly 2 arguments."); + return; + } + + var mgr = IoCManager.Resolve(); + if (mgr.TryGetPlayerDataByUsername(args[0], out var data)) + { + var mind = data.ContentData()?.Mind; + if (mind == null) + { + shell.SendText(player, "Can't find the mind."); + return; + } + + if (int.TryParse(args[1], out var i)) + { + shell.SendText(player, + mind.TryRemoveObjective(i) + ? "Objective successfully removed!" + : "Objective removing failed. Maybe the index is out of bounds? Check lsobjectives!"); + } + else + { + shell.SendText(player, $"Invalid index {args[1]}!"); + } + } + else + { + shell.SendText(player, "Can't find the playerdata."); + } + } + } +} diff --git a/Content.Server/Objectives/Conditions/StealCondition.cs b/Content.Server/Objectives/Conditions/StealCondition.cs new file mode 100644 index 0000000000..a7ea32187a --- /dev/null +++ b/Content.Server/Objectives/Conditions/StealCondition.cs @@ -0,0 +1,57 @@ +#nullable enable +using Content.Server.GameObjects.Components.ContainerExt; +using Content.Server.Mobs; +using Content.Server.Objectives.Interfaces; +using Robust.Server.GameObjects.Components.Container; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.Log; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions +{ + public class StealCondition : IObjectiveCondition + { + public string PrototypeId { get; private set; } = default!; + public int Amount { get; private set; } + + public void ExposeData(ObjectSerializer serializer) + { + serializer.DataField(this, x => x.PrototypeId, "prototype", ""); + serializer.DataField(this, x => x.Amount, "amount", 1); + + if (Amount < 1) + { + Logger.Error("StealCondition has an amount less than 1 ({0})", Amount); + } + } + + private string PrototypeName => + IoCManager.Resolve().TryIndex(PrototypeId, out var prototype) + ? prototype.Name + : "[CANNOT FIND NAME]"; + + public string GetTitle() => Loc.GetString("Steal {0} {1}", Amount > 1 ? $"{Amount}x" : "", Loc.GetString(PrototypeName)); + + public string GetDescription() => Loc.GetString("We need you to steal {0}. Don't get caught.", Loc.GetString(PrototypeName)); + + public SpriteSpecifier GetIcon() + { + return new SpriteSpecifier.EntityPrototype(PrototypeId); + } + + public float GetProgress(Mind? mind) + { + if (mind?.OwnedEntity == null) return 0f; + if (!mind.OwnedEntity.TryGetComponent(out var containerManagerComponent)) return 0f; + + float count = containerManagerComponent.CountPrototypeOccurencesRecursive(PrototypeId); + return count/Amount; + } + + public float GetDifficulty() => 1f; + } +} diff --git a/Content.Server/Objectives/Interfaces/IObjectiveCondition.cs b/Content.Server/Objectives/Interfaces/IObjectiveCondition.cs new file mode 100644 index 0000000000..97385aa805 --- /dev/null +++ b/Content.Server/Objectives/Interfaces/IObjectiveCondition.cs @@ -0,0 +1,35 @@ +using Content.Server.Mobs; +using Robust.Shared.Interfaces.Serialization; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Interfaces +{ + public interface IObjectiveCondition : IExposeData + { + /// + /// Returns the title of the condition. + /// + string GetTitle(); + + /// + /// Returns the description of the condition. + /// + string GetDescription(); + + /// + /// Returns a SpriteSpecifier to be used as an icon for the condition. + /// + SpriteSpecifier GetIcon(); + + /// + /// Returns the current progress of the condition in %. + /// + /// Current progress in %. + float GetProgress(Mind mind); + + /// + /// Returns a difficulty of the condition. + /// + float GetDifficulty(); + } +} diff --git a/Content.Server/Objectives/Interfaces/IObjectiveRequirement.cs b/Content.Server/Objectives/Interfaces/IObjectiveRequirement.cs new file mode 100644 index 0000000000..7615c8a0a3 --- /dev/null +++ b/Content.Server/Objectives/Interfaces/IObjectiveRequirement.cs @@ -0,0 +1,15 @@ +using Content.Server.Mobs; +using Robust.Shared.Interfaces.Serialization; + +namespace Content.Server.Objectives.Interfaces +{ + public interface IObjectiveRequirement : IExposeData + { + + /// + /// Checks whether or not the entity & its surroundings are valid to be given the objective. + /// + /// Returns true if objective can be given. + bool CanBeAssigned(Mind mind); + } +} diff --git a/Content.Server/Objectives/Interfaces/IObjectivesManager.cs b/Content.Server/Objectives/Interfaces/IObjectivesManager.cs new file mode 100644 index 0000000000..25b72812e7 --- /dev/null +++ b/Content.Server/Objectives/Interfaces/IObjectivesManager.cs @@ -0,0 +1,17 @@ +using Content.Server.Mobs; + +namespace Content.Server.Objectives.Interfaces +{ + public interface IObjectivesManager + { + /// + /// Returns all objectives the provided mind is valid for. + /// + ObjectivePrototype[] GetAllPossibleObjectives(Mind mind); + + /// + /// Returns a randomly picked (no pop) collection of objectives the provided mind is valid for. + /// + ObjectivePrototype[] GetRandomObjectives(Mind mind, float maxDifficulty = 3f); + } +} diff --git a/Content.Server/Objectives/ObjectivePrototype.cs b/Content.Server/Objectives/ObjectivePrototype.cs new file mode 100644 index 0000000000..78965c79f6 --- /dev/null +++ b/Content.Server/Objectives/ObjectivePrototype.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Server.Mobs; +using Content.Server.Objectives.Interfaces; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; +using YamlDotNet.RepresentationModel; + +namespace Content.Server.Objectives +{ + [Prototype("objective")] + public class ObjectivePrototype : IPrototype, IIndexedPrototype + { + [ViewVariables] + public string ID { get; private set; } + + [ViewVariables(VVAccess.ReadWrite)] + public string Issuer { get; private set; } + + [ViewVariables] + public float Probability { get; private set; } + + [ViewVariables] + public IReadOnlyList Conditions => _conditions; + [ViewVariables] + public IReadOnlyList Requirements => _requirements; + + [ViewVariables] + public float Difficulty => _difficultyOverride ?? _conditions.Sum(c => c.GetDifficulty()); + + private List _conditions = new List(); + private List _requirements = new List(); + + [ViewVariables(VVAccess.ReadWrite)] + private float? _difficultyOverride = null; + + public bool CanBeAssigned(Mind mind) + { + foreach (var requirement in _requirements) + { + if (!requirement.CanBeAssigned(mind)) return false; + } + + return true; + } + + public void LoadFrom(YamlMappingNode mapping) + { + var ser = YamlObjectSerializer.NewReader(mapping); + + ser.DataField(this, x => x.ID, "id", string.Empty); + ser.DataField(this, x => x.Issuer, "issuer", "Unknown"); + ser.DataField(this, x => x.Probability, "prob", 0.3f); + ser.DataField(this, x => x._conditions, "conditions", new List()); + ser.DataField(this, x => x._requirements, "requirements", new List()); + ser.DataField(this, x => x._difficultyOverride, "difficultyOverride", null); + } + } +} diff --git a/Content.Server/Objectives/ObjectivesManager.cs b/Content.Server/Objectives/ObjectivesManager.cs new file mode 100644 index 0000000000..e8f75dbc71 --- /dev/null +++ b/Content.Server/Objectives/ObjectivesManager.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.Linq; +using Content.Server.Mobs; +using Content.Server.Objectives.Interfaces; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.IoC; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives +{ + public class ObjectivesManager : IObjectivesManager + { + [Dependency] private IPrototypeManager _prototypeManager = default!; + [Dependency] private IRobustRandom _random = default!; + + public ObjectivePrototype[] GetAllPossibleObjectives(Mind mind) + { + return _prototypeManager.EnumeratePrototypes().Where(objectivePrototype => objectivePrototype.CanBeAssigned(mind)).ToArray(); + } + + public ObjectivePrototype[] GetRandomObjectives(Mind mind, float maxDifficulty = 3) + { + var objectives = GetAllPossibleObjectives(mind); + + //to prevent endless loops + if(objectives.Length == 0 || objectives.Sum(o => o.Difficulty) == 0f) return objectives; + + var result = new List(); + var currentDifficulty = 0f; + _random.Shuffle(objectives); + while (currentDifficulty < maxDifficulty) + { + foreach (var objective in objectives) + { + if (!_random.Prob(objective.Probability)) continue; + + result.Add(objective); + currentDifficulty += objective.Difficulty; + if (currentDifficulty >= maxDifficulty) break; + } + } + + if (currentDifficulty > maxDifficulty) //will almost always happen + { + result.Pop(); + } + + return result.ToArray(); + } + } +} diff --git a/Content.Server/Objectives/Requirements/SuspicionTraitorRequirement.cs b/Content.Server/Objectives/Requirements/SuspicionTraitorRequirement.cs new file mode 100644 index 0000000000..183e24a2c6 --- /dev/null +++ b/Content.Server/Objectives/Requirements/SuspicionTraitorRequirement.cs @@ -0,0 +1,17 @@ +using Content.Server.Mobs; +using Content.Server.Mobs.Roles.Suspicion; +using Content.Server.Objectives.Interfaces; +using Robust.Shared.Serialization; + +namespace Content.Server.Objectives.Requirements +{ + public class SuspicionTraitorRequirement : IObjectiveRequirement + { + public void ExposeData(ObjectSerializer serializer){} + + public bool CanBeAssigned(Mind mind) + { + return mind.HasRole(); + } + } +} diff --git a/Content.Server/ServerContentIoC.cs b/Content.Server/ServerContentIoC.cs index ad69775abf..9f150e83df 100644 --- a/Content.Server/ServerContentIoC.cs +++ b/Content.Server/ServerContentIoC.cs @@ -14,6 +14,8 @@ using Content.Server.Interfaces; using Content.Server.Interfaces.Chat; using Content.Server.Interfaces.GameTicking; using Content.Server.Interfaces.PDA; +using Content.Server.Objectives; +using Content.Server.Objectives.Interfaces; using Content.Server.PDA; using Content.Server.Preferences; using Content.Server.Sandbox; @@ -49,6 +51,7 @@ namespace Content.Server IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Content.Shared/GameObjects/Components/Actor/SharedCharacterInfoComponent.cs b/Content.Shared/GameObjects/Components/Actor/SharedCharacterInfoComponent.cs new file mode 100644 index 0000000000..9e20f4f86e --- /dev/null +++ b/Content.Shared/GameObjects/Components/Actor/SharedCharacterInfoComponent.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Objectives; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Actor +{ + public class SharedCharacterInfoComponent : Component + { + public override string Name => "CharacterInfo"; + public override uint? NetID => ContentNetIDs.CHARACTERINFO; + + [Serializable, NetSerializable] + protected class RequestCharacterInfoMessage : ComponentMessage + { + public RequestCharacterInfoMessage() + { + Directed = true; + } + } + + [Serializable, NetSerializable] + protected class CharacterInfoMessage : ComponentMessage + { + public readonly Dictionary> Objectives; + public readonly string JobTitle; + + public CharacterInfoMessage(string jobTitle, Dictionary> objectives) + { + Directed = true; + JobTitle = jobTitle; + Objectives = objectives; + } + } + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index a2d20277d0..8e1c89dd5e 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -84,6 +84,7 @@ public const uint PULLABLE = 1078; public const uint GAS_TANK = 1079; public const uint SINGULARITY = 1080; + public const uint CHARACTERINFO = 1081; // Net IDs for integration tests. public const uint PREDICTION_TEST = 10001; diff --git a/Content.Shared/Objectives/ConditionInfo.cs b/Content.Shared/Objectives/ConditionInfo.cs new file mode 100644 index 0000000000..de0e7f934b --- /dev/null +++ b/Content.Shared/Objectives/ConditionInfo.cs @@ -0,0 +1,23 @@ +using System; +using Robust.Shared.Serialization; +using Robust.Shared.Utility; + +namespace Content.Shared.Objectives +{ + [Serializable, NetSerializable] + public class ConditionInfo + { + public string Title { get; } + public string Description { get; } + public SpriteSpecifier SpriteSpecifier { get; } + public float Progress { get; } + + public ConditionInfo(string title, string description, SpriteSpecifier spriteSpecifier, float progress) + { + Title = title; + Description = description; + SpriteSpecifier = spriteSpecifier; + Progress = progress; + } + } +} diff --git a/Resources/Prototypes/Objectives/traitorObjectives.yml b/Resources/Prototypes/Objectives/traitorObjectives.yml new file mode 100644 index 0000000000..2423769e5c --- /dev/null +++ b/Resources/Prototypes/Objectives/traitorObjectives.yml @@ -0,0 +1,9 @@ +- type: objective + id: SoapDeluxeStealObjective + prob: 0.3 + issuer: The Syndicate + requirements: + - !type:SuspicionTraitorRequirement {} + conditions: + - !type:StealCondition + prototype: SoapDeluxe