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
This commit is contained in:
parent
ea88904579
commit
d5d7be36a2
|
|
@ -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;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The oracle's request pool is huge.
|
||||
/// We need to test everything that the oracle could request can be turned in.
|
||||
/// </summary>
|
||||
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<IMapManager>();
|
||||
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
||||
|
||||
var oracleSystem = entitySystemManager.GetEntitySystem<OracleSystem>();
|
||||
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<ItemComponent>(spawned),
|
||||
$"Oracle can request non-item {proto}");
|
||||
|
||||
Assert.That(!entityManager.HasComponent<SolutionTransferComponent>(spawned),
|
||||
$"Oracle can request reagent container {proto} that will conflict with the fountain");
|
||||
|
||||
Assert.That(!entityManager.HasComponent<MobStateComponent>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<string> RewardReagents = new[]
|
||||
{
|
||||
"LotophagoiOil", "LotophagoiOil", "LotophagoiOil", "LotophagoiOil", "LotophagoiOil", "Wine", "Blood", "Ichor"
|
||||
};
|
||||
|
||||
[DataField("demandMessages")]
|
||||
public IReadOnlyList<string> 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<string> RejectMessages = new[]
|
||||
{
|
||||
"ἄγνοια",
|
||||
"υλικό",
|
||||
"ἀγνωσία",
|
||||
"γήινος",
|
||||
"σάκλας"
|
||||
};
|
||||
|
||||
[DataField("blacklistedPrototypes")]
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public IReadOnlyList<string> BlacklistedPrototypes = new[]
|
||||
{
|
||||
"Drone",
|
||||
"QSI",
|
||||
"HandTeleporter",
|
||||
"BluespaceBeaker",
|
||||
"ClothingBackpackHolding",
|
||||
"ClothingBackpackSatchelHolding",
|
||||
"ClothingBackpackDuffelHolding",
|
||||
"TrashBagOfHolding",
|
||||
"BluespaceCrystal",
|
||||
"InsulativeHeadcage",
|
||||
"CrystalNormality",
|
||||
};
|
||||
}
|
||||
|
|
@ -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<OracleComponent>())
|
||||
{
|
||||
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<OracleComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<OracleComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<OracleComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, OracleComponent component, ComponentInit args)
|
||||
{
|
||||
NextItem(component);
|
||||
}
|
||||
|
||||
private void OnInteractHand(EntityUid uid, OracleComponent component, InteractHandEvent args)
|
||||
{
|
||||
if (!HasComp<PotentialPsionicComponent>(args.User) || HasComp<PsionicInsulationComponent>(args.User))
|
||||
return;
|
||||
|
||||
if (!TryComp<ActorComponent>(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<MobStateComponent>(args.Used))
|
||||
return;
|
||||
|
||||
if (!TryComp<MetaDataComponent>(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<RefillableSolutionComponent>(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<ReagentPrototype>()
|
||||
.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<EntityPrototype>(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<string> GetAllProtos(OracleComponent component)
|
||||
{
|
||||
var allTechs = _prototypeManager.EnumeratePrototypes<TechnologyPrototype>();
|
||||
var allRecipes = new List<string>();
|
||||
|
||||
foreach (var tech in allTechs)
|
||||
{
|
||||
foreach (var recipe in tech.RecipeUnlocks)
|
||||
{
|
||||
var recipeProto = _prototypeManager.Index(recipe);
|
||||
allRecipes.Add(recipeProto.Result);
|
||||
}
|
||||
}
|
||||
|
||||
var allPlants = _prototypeManager.EnumeratePrototypes<SeedPrototype>().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<EntityPrototype>(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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
@ -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: []
|
||||
|
|
|
|||
Loading…
Reference in New Issue