From d5d7be36a2df5184122b179d941547cc6725abce Mon Sep 17 00:00:00 2001 From: Debug <49997488+DebugOk@users.noreply.github.com> Date: Sun, 22 Oct 2023 03:27:51 +0200 Subject: [PATCH] The Oracle (#208) * Working oracle moment * Update OracleSystem Use a dynamic blacklist, so we dont have to manually specify all invalids, and do some general code cleanup * Convert ReadWrites into ReadOnly --- .../Tests/Nyanotrasen/Oracle/OracleTest.cs | 72 +++++ .../Research/Oracle/OracleComponent.cs | 79 ++++++ .../Research/Oracle/OracleSystem.cs | 250 ++++++++++++++++++ .../en-US/nyanotrasen/research/oracle.ftl | 15 ++ .../Entities/Structures/Research/oracle.yml | 58 ++-- 5 files changed, 445 insertions(+), 29 deletions(-) create mode 100644 Content.IntegrationTests/Tests/Nyanotrasen/Oracle/OracleTest.cs create mode 100644 Content.Server/Nyanotrasen/Research/Oracle/OracleComponent.cs create mode 100644 Content.Server/Nyanotrasen/Research/Oracle/OracleSystem.cs create mode 100644 Resources/Locale/en-US/nyanotrasen/research/oracle.ftl diff --git a/Content.IntegrationTests/Tests/Nyanotrasen/Oracle/OracleTest.cs b/Content.IntegrationTests/Tests/Nyanotrasen/Oracle/OracleTest.cs new file mode 100644 index 0000000000..c925db3ba2 --- /dev/null +++ b/Content.IntegrationTests/Tests/Nyanotrasen/Oracle/OracleTest.cs @@ -0,0 +1,72 @@ +#nullable enable +using NUnit.Framework; +using System.Threading.Tasks; +using Content.Shared.Item; +using Content.Shared.Mobs.Components; +using Content.Server.Research.Oracle; +using Content.Shared.Chemistry.Components; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; + + +/// +/// The oracle's request pool is huge. +/// We need to test everything that the oracle could request can be turned in. +/// +namespace Content.IntegrationTests.Tests.Oracle +{ + [TestFixture] + [TestOf(typeof(OracleSystem))] + public sealed class OracleTest + { + [Test] + public async Task AllOracleItemsCanBeTurnedIn() + { + await using var pairTracker = await PoolManager.GetServerClient(); + var server = pairTracker.Server; + // Per RobustIntegrationTest.cs, wait until state is settled to access it. + await server.WaitIdleAsync(); + + var mapManager = server.ResolveDependency(); + var prototypeManager = server.ResolveDependency(); + var entityManager = server.ResolveDependency(); + var entitySystemManager = server.ResolveDependency(); + + var oracleSystem = entitySystemManager.GetEntitySystem(); + var oracleComponent = new OracleComponent(); + + var testMap = await pairTracker.CreateTestMap(); + + await server.WaitAssertion(() => + { + var allProtos = oracleSystem.GetAllProtos(oracleComponent); + var coordinates = testMap.GridCoords; + + Assert.That((allProtos.Count > 0), "Oracle has no valid prototypes!"); + + foreach (var proto in allProtos) + { + var spawned = entityManager.SpawnEntity(proto, coordinates); + + Assert.That(entityManager.HasComponent(spawned), + $"Oracle can request non-item {proto}"); + + Assert.That(!entityManager.HasComponent(spawned), + $"Oracle can request reagent container {proto} that will conflict with the fountain"); + + Assert.That(!entityManager.HasComponent(spawned), + $"Oracle can request mob {proto} that could potentially have a player-set name."); + } + + // Because Server/Client pairs can be re-used between Tests, we + // need to clean up anything that might affect other tests, + // otherwise this pair cannot be considered clean, and the + // CleanReturnAsync call would need to be removed. + mapManager.DeleteMap(testMap.MapId); + }); + + await pairTracker.CleanReturnAsync(); + } + } +} diff --git a/Content.Server/Nyanotrasen/Research/Oracle/OracleComponent.cs b/Content.Server/Nyanotrasen/Research/Oracle/OracleComponent.cs new file mode 100644 index 0000000000..b876a06375 --- /dev/null +++ b/Content.Server/Nyanotrasen/Research/Oracle/OracleComponent.cs @@ -0,0 +1,79 @@ +using Robust.Shared.Prototypes; + +namespace Content.Server.Research.Oracle; + +[RegisterComponent] +public sealed partial class OracleComponent : Component +{ + public const string SolutionName = "fountain"; + + [ViewVariables] + [DataField("accumulator")] + public float Accumulator; + + [ViewVariables] + [DataField("resetTime")] + public TimeSpan ResetTime = TimeSpan.FromMinutes(10); + + [DataField("barkAccumulator")] + public float BarkAccumulator; + + [DataField("barkTime")] + public TimeSpan BarkTime = TimeSpan.FromMinutes(1); + + [ViewVariables(VVAccess.ReadWrite)] + public EntityPrototype DesiredPrototype = default!; + + [ViewVariables(VVAccess.ReadWrite)] + public EntityPrototype? LastDesiredPrototype = default!; + + [DataField("rewardReagents")] + public static IReadOnlyList RewardReagents = new[] + { + "LotophagoiOil", "LotophagoiOil", "LotophagoiOil", "LotophagoiOil", "LotophagoiOil", "Wine", "Blood", "Ichor" + }; + + [DataField("demandMessages")] + public IReadOnlyList DemandMessages = new[] + { + "oracle-demand-1", + "oracle-demand-2", + "oracle-demand-3", + "oracle-demand-4", + "oracle-demand-5", + "oracle-demand-6", + "oracle-demand-7", + "oracle-demand-8", + "oracle-demand-9", + "oracle-demand-10", + "oracle-demand-11", + "oracle-demand-12" + }; + + [DataField("rejectMessages")] + public IReadOnlyList RejectMessages = new[] + { + "ἄγνοια", + "υλικό", + "ἀγνωσία", + "γήινος", + "σάκλας" + }; + + [DataField("blacklistedPrototypes")] + [ViewVariables(VVAccess.ReadOnly)] + public IReadOnlyList BlacklistedPrototypes = new[] + { + "Drone", + "QSI", + "HandTeleporter", + "BluespaceBeaker", + "ClothingBackpackHolding", + "ClothingBackpackSatchelHolding", + "ClothingBackpackDuffelHolding", + "TrashBagOfHolding", + "BluespaceCrystal", + "InsulativeHeadcage", + "CrystalNormality", + }; +} diff --git a/Content.Server/Nyanotrasen/Research/Oracle/OracleSystem.cs b/Content.Server/Nyanotrasen/Research/Oracle/OracleSystem.cs new file mode 100644 index 0000000000..2548f3da2b --- /dev/null +++ b/Content.Server/Nyanotrasen/Research/Oracle/OracleSystem.cs @@ -0,0 +1,250 @@ +using System.Linq; +using Content.Server.Botany; +using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; +using Content.Server.Fluids.EntitySystems; +using Content.Server.Psionics; +using Content.Shared.Abilities.Psionics; +using Content.Shared.Chat; +using Content.Shared.Chemistry.Components; +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Chemistry.Reagent; +using Content.Shared.Interaction; +using Content.Shared.Mobs.Components; +using Content.Shared.Psionics.Glimmer; +using Content.Shared.Research.Prototypes; +using Robust.Server.GameObjects; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Research.Oracle; + +public sealed class OracleSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!; + [Dependency] private readonly GlimmerSystem _glimmerSystem = default!; + [Dependency] private readonly PuddleSystem _puddleSystem = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + foreach (var oracle in EntityQuery()) + { + oracle.Accumulator += frameTime; + oracle.BarkAccumulator += frameTime; + if (oracle.BarkAccumulator >= oracle.BarkTime.TotalSeconds) + { + oracle.BarkAccumulator = 0; + var message = Loc.GetString(_random.Pick(oracle.DemandMessages), ("item", oracle.DesiredPrototype.Name)) + .ToUpper(); + _chat.TrySendInGameICMessage(oracle.Owner, message, InGameICChatType.Speak, false); + } + + if (oracle.Accumulator >= oracle.ResetTime.TotalSeconds) + { + oracle.LastDesiredPrototype = oracle.DesiredPrototype; + NextItem(oracle); + } + } + } + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnInteractHand); + SubscribeLocalEvent(OnInteractUsing); + } + + private void OnInit(EntityUid uid, OracleComponent component, ComponentInit args) + { + NextItem(component); + } + + private void OnInteractHand(EntityUid uid, OracleComponent component, InteractHandEvent args) + { + if (!HasComp(args.User) || HasComp(args.User)) + return; + + if (!TryComp(args.User, out var actor)) + return; + + var message = Loc.GetString("oracle-current-item", ("item", component.DesiredPrototype.Name)); + + var messageWrap = Loc.GetString("chat-manager-send-telepathic-chat-wrap-message", + ("telepathicChannelName", Loc.GetString("chat-manager-telepathic-channel-name")), ("message", message)); + + _chatManager.ChatMessageToOne(ChatChannel.Telepathic, + message, messageWrap, uid, false, actor.PlayerSession.ConnectedClient, Color.PaleVioletRed); + + if (component.LastDesiredPrototype != null) + { + var message2 = Loc.GetString("oracle-previous-item", ("item", component.LastDesiredPrototype.Name)); + var messageWrap2 = Loc.GetString("chat-manager-send-telepathic-chat-wrap-message", + ("telepathicChannelName", Loc.GetString("chat-manager-telepathic-channel-name")), + ("message", message2)); + + _chatManager.ChatMessageToOne(ChatChannel.Telepathic, + message2, messageWrap2, uid, false, actor.PlayerSession.ConnectedClient, Color.PaleVioletRed); + } + } + + private void OnInteractUsing(EntityUid uid, OracleComponent component, InteractUsingEvent args) + { + if (HasComp(args.Used)) + return; + + if (!TryComp(args.Used, out var meta)) + return; + + if (meta.EntityPrototype == null) + return; + + var validItem = CheckValidity(meta.EntityPrototype, component.DesiredPrototype); + + var nextItem = true; + + if (component.LastDesiredPrototype != null && + CheckValidity(meta.EntityPrototype, component.LastDesiredPrototype)) + { + nextItem = false; + validItem = true; + component.LastDesiredPrototype = null; + } + + if (!validItem) + { + if (!HasComp(args.Used)) + _chat.TrySendInGameICMessage(uid, _random.Pick(component.RejectMessages), InGameICChatType.Speak, true); + return; + } + + EntityManager.QueueDeleteEntity(args.Used); + + EntityManager.SpawnEntity("ResearchDisk5000", Transform(args.User).Coordinates); + + DispenseLiquidReward(uid); + + var i = _random.Next(1, 4); + + while (i != 0) + { + EntityManager.SpawnEntity("MaterialBluespace1", Transform(args.User).Coordinates); + i--; + } + + if (nextItem) + NextItem(component); + } + + private bool CheckValidity(EntityPrototype given, EntityPrototype target) + { + // 1: directly compare Names + // name instead of ID because the oracle asks for them by name + // this could potentially lead to like, labeller exploits maybe but so far only mob names can be fully player-set. + if (given.Name == target.Name) + return true; + + return false; + } + + private void DispenseLiquidReward(EntityUid uid) + { + if (!_solutionSystem.TryGetSolution(uid, OracleComponent.SolutionName, out var fountainSol)) + return; + + var allReagents = _prototypeManager.EnumeratePrototypes() + .Where(x => !x.Abstract) + .Select(x => x.ID).ToList(); + + var amount = 20 + _random.Next(1, 30) + _glimmerSystem.Glimmer / 10f; + amount = (float) Math.Round(amount); + + var sol = new Solution(); + var reagent = ""; + + if (_random.Prob(0.2f)) + reagent = _random.Pick(allReagents); + else + reagent = _random.Pick(OracleComponent.RewardReagents); + + sol.AddReagent(reagent, amount); + + _solutionSystem.TryMixAndOverflow(uid, fountainSol, sol, fountainSol.MaxVolume, out var overflowing); + + if (overflowing != null && overflowing.Volume > 0) + _puddleSystem.TrySpillAt(uid, overflowing, out var _); + } + + private void NextItem(OracleComponent component) + { + component.Accumulator = 0; + component.BarkAccumulator = 0; + var protoString = GetDesiredItem(component); + if (_prototypeManager.TryIndex(protoString, out var proto)) + component.DesiredPrototype = proto; + else + Logger.Error("Oracle can't index prototype " + protoString); + } + + private string GetDesiredItem(OracleComponent component) + { + return _random.Pick(GetAllProtos(component)); + } + + + public List GetAllProtos(OracleComponent component) + { + var allTechs = _prototypeManager.EnumeratePrototypes(); + var allRecipes = new List(); + + foreach (var tech in allTechs) + { + foreach (var recipe in tech.RecipeUnlocks) + { + var recipeProto = _prototypeManager.Index(recipe); + allRecipes.Add(recipeProto.Result); + } + } + + var allPlants = _prototypeManager.EnumeratePrototypes().Select(x => x.ProductPrototypes[0]) + .ToList(); + var allProtos = allRecipes.Concat(allPlants).ToList(); + var blacklist = component.BlacklistedPrototypes.ToList(); + + foreach (var proto in allProtos) + { + if (!_prototypeManager.TryIndex(proto, out var entityProto)) + { + blacklist.Add(proto); + continue; + } + + if (!entityProto.Components.ContainsKey("Item")) + { + blacklist.Add(proto); + continue; + } + + if (entityProto.Components.ContainsKey("SolutionTransfer")) + { + blacklist.Add(proto); + continue; + } + + if (entityProto.Components.ContainsKey("MobState")) + blacklist.Add(proto); + } + + foreach (var proto in blacklist) + { + allProtos.Remove(proto); + } + + return allProtos; + } +} diff --git a/Resources/Locale/en-US/nyanotrasen/research/oracle.ftl b/Resources/Locale/en-US/nyanotrasen/research/oracle.ftl new file mode 100644 index 0000000000..a8db7f5028 --- /dev/null +++ b/Resources/Locale/en-US/nyanotrasen/research/oracle.ftl @@ -0,0 +1,15 @@ +oracle-demand-1 = I demand one {$item}! Do not question why! +oracle-demand-2 = In exchange for knowledge, you must bring me one {$item}! +oracle-demand-3 = I will grant you a gift of the divine in exchange for one {$item}! +oracle-demand-4 = Bring me one {$item} if you ever wish to receive even one drop of my grace! +oracle-demand-5 = If you are not ignorant, you will bring me one {$item}! +oracle-demand-6 = The archons have a request of you: one {$item}! +oracle-demand-7 = To ascend to a higher state of being, to achieve gnosis, your pathetic corporeal bodies must bring me one {$item}! +oracle-demand-8 = If you wish to prolong the length of time your spark of divine light spends in that vessel, you should bring me one {$item}! +oracle-demand-9 = Bring me one {$item} or revel in your ignorance, worms! +oracle-demand-10 = If you ever wish to pierce the veil of this false existence, you must bring me one {$item}! +oracle-demand-11 = I'll make sure the next vessel you inhabit in this material hell is one of those monkeys you torture if you do not bring me one {$item}! +oracle-demand-12 = One {$item}, a step on the path to divinity. + +oracle-current-item = Current requested item: {$item} +oracle-previous-item = Last requested item, still accepting: {$item} diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/oracle.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/oracle.yml index 6e518d1296..f7481abf1e 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/oracle.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/oracle.yml @@ -13,32 +13,32 @@ - state: oracle-0 - map: ["enum.SolutionContainerLayers.Fill"] state: oracle-0 -# - type: Oracle -# - type: Speech -# speechSounds: Tenor -# - type: Psionic - # - type: SolutionContainerManager - # solutions: - # fountain: - # maxVol: 200 - # - type: Drink - # isOpen: true - # solution: fountain - # - type: DrawableSolution - # solution: fountain - # - type: DrainableSolution - # solution: fountain - # - type: ExaminableSolution - # solution: fountain - # - type: Appearance - # - type: SolutionContainerVisuals - # maxFillLevels: 10 - # fillBaseName: oracle- - # - type: Grammar - # attributes: - # gender: female - # proper: true - # - type: Prayable - # - type: SpriteFade - # - type: Tag - # tags: [] + - type: Oracle + - type: Speech + speechSounds: Tenor + - type: Psionic + - type: SolutionContainerManager + solutions: + fountain: + maxVol: 200 + - type: Drink + isOpen: true + solution: fountain + - type: DrawableSolution + solution: fountain + - type: DrainableSolution + solution: fountain + - type: ExaminableSolution + solution: fountain + - type: Appearance + - type: SolutionContainerVisuals + maxFillLevels: 10 + fillBaseName: oracle- + - type: Grammar + attributes: + gender: female + proper: true + - type: Prayable + - type: SpriteFade + - type: Tag + tags: []