Files
wwdpublic/Content.Server/Research/Oracle/OracleSystem.cs
Mnemotechnican c976bfa40c Oracle Refactor (#549)
# Description
Refactors the oracle system and component, making them more customizable
and less trash.

- Most of the nyano shitcode was rewritten in accordance with the new
standards and in a less hardcoded manner (all features were preserved,
with some changes).
- Replaced the accumulator field pattern with the "nextX" pattern for
things oracle does on certain intervals.
- Removed some code duplication and bloat. 
- Gave oracle a 75% chance to request technology and 25% to request a
plant.
- When requesting a technology, oracle will only request what's either
already researched, or can be researched soon (that is, the
prerequisites of the research are complete and the research isn't locked
for any reason).
- If there's no research server at the moment when oracle demands an
item, it will only demand a plant
- When dispensing rewards, oracle can now spawn 1, 3, or 5 crystals at
once, and research disks worth 5k, 10k, and 20k research points. Each
one of those have different probabilities (3 crystals and 5k points
still prevail, but occasionally you can get the more expensive rewards).
- When dispensing rewards, oracle now throws them at the person who
fulfilled the request instead of simply spawning them at their feet.

Almost every oracle thing (except for throwing and the amount of liquid
dispensed) can now be configured in the yaml prototype of the oracle.

# TODO
Forget it

<details><summary><h1>Media</h1></summary><p>


https://github.com/user-attachments/assets/9d4be44f-37d5-4072-a6e6-f194764f7ff6


![image](https://github.com/user-attachments/assets/011761d4-8d73-4d65-ba9c-92b25a28e95f)

</p></details>

---

# Changelog
🆑
- tweak: Oracle requests are now more likely to be aligned with the
current research.
2024-08-05 18:10:42 +01:00

289 lines
11 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Botany;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Psionics;
using Content.Server.Research.Systems;
using Content.Shared.Abilities.Psionics;
using Content.Shared.Chat;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Components;
using Content.Shared.Psionics.Glimmer;
using Content.Shared.Random.Helpers;
using Content.Shared.Research.Components;
using Content.Shared.Research.Prototypes;
using Content.Shared.Throwing;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Research.Oracle;
public sealed class OracleSystem : EntitySystem
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly GlimmerSystem _glimmer = default!;
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly PuddleSystem _puddles = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ResearchSystem _research = default!;
[Dependency] private readonly SolutionContainerSystem _solutions = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<OracleComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (_timing.CurTime >= comp.NextDemandTime)
{
// Might be null if this is the first tick. In that case this will simply initialize it.
var last = (EntityPrototype?) comp.DesiredPrototype;
if (NextItem((uid, comp)))
comp.LastDesiredPrototype = last;
}
if (_timing.CurTime >= comp.NextBarkTime)
{
comp.NextBarkTime = _timing.CurTime + comp.BarkDelay;
var message = Loc.GetString(_random.Pick(comp.DemandMessages), ("item", comp.DesiredPrototype.Name)).ToUpper();
_chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, false);
}
}
query.Dispose();
}
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<OracleComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<OracleComponent, InteractUsingEvent>(OnInteractUsing);
}
private void OnInteractHand(Entity<OracleComponent> oracle, ref InteractHandEvent args)
{
if (!HasComp<PotentialPsionicComponent>(args.User) || HasComp<PsionicInsulationComponent>(args.User)
|| !TryComp<ActorComponent>(args.User, out var actor))
return;
SendTelepathicInfo(oracle, actor.PlayerSession.Channel,
Loc.GetString("oracle-current-item", ("item", oracle.Comp.DesiredPrototype.Name)));
if (oracle.Comp.LastDesiredPrototype != null)
SendTelepathicInfo(oracle, actor.PlayerSession.Channel,
Loc.GetString("oracle-previous-item", ("item", oracle.Comp.LastDesiredPrototype.Name)));
}
private void OnInteractUsing(Entity<OracleComponent> oracle, ref InteractUsingEvent args)
{
if (args.Handled)
return;
if (HasComp<MobStateComponent>(args.Used) || !TryComp<MetaDataComponent>(args.Used, out var meta) || meta.EntityPrototype == null)
return;
var requestValid = IsCorrectItem(meta.EntityPrototype, oracle.Comp.DesiredPrototype);
var updateRequest = true;
if (oracle.Comp.LastDesiredPrototype != null &&
IsCorrectItem(meta.EntityPrototype, oracle.Comp.LastDesiredPrototype))
{
updateRequest = false;
requestValid = true;
oracle.Comp.LastDesiredPrototype = null;
}
if (!requestValid)
{
if (!HasComp<RefillableSolutionComponent>(args.Used) &&
_timing.CurTime >= oracle.Comp.NextRejectTime)
{
oracle.Comp.NextRejectTime = _timing.CurTime + oracle.Comp.RejectDelay;
_chat.TrySendInGameICMessage(oracle, _random.Pick(oracle.Comp.RejectMessages), InGameICChatType.Speak, true);
}
return;
}
DispenseRewards(oracle, Transform(args.User).Coordinates);
QueueDel(args.Used);
if (updateRequest)
NextItem(oracle);
}
private void SendTelepathicInfo(Entity<OracleComponent> oracle, INetChannel client, string message)
{
var messageWrap = Loc.GetString("chat-manager-send-telepathic-chat-wrap-message",
("telepathicChannelName", Loc.GetString("chat-manager-telepathic-channel-name")),
("message", message));
_chatMan.ChatMessageToOne(ChatChannel.Telepathic,
message, messageWrap, oracle, false, client, Color.PaleVioletRed);
}
private bool IsCorrectItem(EntityPrototype given, EntityPrototype target)
{
// Nyano, what is this shit?
// Why are we comparing by name instead of prototype id?
// Why is this ever necessary?
// What were you trying to accomplish?!
if (given.Name == target.Name)
return true;
return false;
}
private void DispenseRewards(Entity<OracleComponent> oracle, EntityCoordinates throwTarget)
{
foreach (var rewardRandom in oracle.Comp.RewardEntities)
{
// Spawn each reward next to oracle and throw towards the target
var rewardProto = _protoMan.Index(rewardRandom).Pick(_random);
var reward = EntityManager.SpawnNextToOrDrop(rewardProto, oracle);
_throwing.TryThrow(reward, throwTarget, recoil: false);
}
DispenseLiquidReward(oracle);
}
private void DispenseLiquidReward(Entity<OracleComponent> oracle)
{
if (!_solutions.TryGetSolution(oracle.Owner, OracleComponent.SolutionName, out var fountainSol))
return;
// Why is this hardcoded?
var amount = MathF.Round(20 + _random.Next(1, 30) + _glimmer.Glimmer / 10f);
var temporarySol = new Solution();
var reagent = _protoMan.Index(oracle.Comp.RewardReagents).Pick(_random);
if (_random.Prob(oracle.Comp.AbnormalReagentChance))
{
var allReagents = _protoMan.EnumeratePrototypes<ReagentPrototype>()
.Where(x => !x.Abstract)
.Select(x => x.ID).ToList();
reagent = _random.Pick(allReagents);
}
temporarySol.AddReagent(reagent, amount);
_solutions.TryMixAndOverflow(fountainSol.Value, temporarySol, fountainSol.Value.Comp.Solution.MaxVolume, out var overflowing);
if (overflowing != null && overflowing.Volume > 0)
_puddles.TrySpillAt(oracle, overflowing, out var _);
}
private bool NextItem(Entity<OracleComponent> oracle)
{
oracle.Comp.NextBarkTime = oracle.Comp.NextRejectTime = TimeSpan.Zero;
oracle.Comp.NextDemandTime = _timing.CurTime + oracle.Comp.DemandDelay;
var protoId = GetDesiredItem(oracle);
if (protoId != null && _protoMan.TryIndex<EntityPrototype>(protoId, out var proto))
{
oracle.Comp.DesiredPrototype = proto;
return true;
}
return false;
}
// TODO: find a way to not just use string literals here (weighted random doesn't support enums)
private string? GetDesiredItem(Entity<OracleComponent> oracle)
{
var demand = _protoMan.Index(oracle.Comp.DemandTypes).Pick(_random);
string? proto;
if (demand == "tech" && GetRandomTechProto(oracle, out proto))
return proto;
// This is also a fallback for when there's no research server to form an oracle tech request.
if (demand is "plant" or "tech" && GetRandomPlantProto(oracle, out proto))
return proto;
return null;
}
private bool GetRandomTechProto(Entity<OracleComponent> oracle, [NotNullWhen(true)] out string? proto)
{
// Try to find the most advanced server.
var database = _research.GetServerIds()
.Select(x => _research.TryGetServerById(x, out var serverUid, out _) ? serverUid : null)
.Where(x => x != null && Transform(x.Value).GridUid == Transform(oracle).GridUid)
.Select(x =>
{
TryComp<TechnologyDatabaseComponent>(x!.Value, out var comp);
return new Entity<TechnologyDatabaseComponent?>(x.Value, comp);
})
.Where(x => x.Comp != null)
.OrderByDescending(x =>
_research.GetDisciplineTiers(x.Comp!).Select(pair => pair.Value).Max())
.FirstOrDefault(EntityUid.Invalid);
if (database.Owner == EntityUid.Invalid)
{
Log.Warning($"Cannot find an applicable server on grid {Transform(oracle).GridUid} to form an oracle request.");
proto = null;
return false;
}
// Select a technology that's either already unlocked, or can be unlocked from current research
var techs = _protoMan.EnumeratePrototypes<TechnologyPrototype>()
.Where(x => !x.Hidden)
.Where(x =>
_research.IsTechnologyUnlocked(database.Owner, x, database.Comp)
|| _research.IsTechnologyAvailable(database.Comp!, x))
.SelectMany(x => x.RecipeUnlocks)
.Select(x => _protoMan.Index(x).Result)
.Where(x => IsDemandValid(oracle, x))
.ToList();
// Unlikely.
if (techs.Count == 0)
{
proto = null;
return false;
}
proto = _random.Pick(techs);
return true;
}
private bool GetRandomPlantProto(Entity<OracleComponent> oracle, [NotNullWhen(true)] out string? proto)
{
var allPlants = _protoMan.EnumeratePrototypes<SeedPrototype>()
.Select(x => x.ProductPrototypes.FirstOrDefault())
.Where(x => IsDemandValid(oracle, x))
.ToList();
if (allPlants.Count == 0)
{
proto = null;
return false;
}
proto = _random.Pick(allPlants)!;
return true;
}
private bool IsDemandValid(Entity<OracleComponent> oracle, ProtoId<EntityPrototype>? id)
{
if (id == null || oracle.Comp.BlacklistedDemands.Contains(id.Value))
return false;
return _protoMan.TryIndex(id, out var proto) && proto.Components.ContainsKey("Item");
}
}