433 lines
16 KiB
C#
433 lines
16 KiB
C#
using Content.Server.Power.Components;
|
|
using Content.Shared.Audio;
|
|
using Content.Shared._Horizon.BluespaceHarvester;
|
|
using Content.Shared.Destructible;
|
|
using Content.Shared.Emag.Components;
|
|
using Robust.Server.GameObjects;
|
|
using Robust.Shared.Random;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using Content.Server.Power.EntitySystems;
|
|
using Robust.Shared.Audio.Systems;
|
|
using Robust.Shared.Map;
|
|
using Robust.Shared.Timing;
|
|
using Robust.Shared.Utility;
|
|
using Robust.Shared.Log;
|
|
|
|
namespace Content.Server._Horizon.BluespaceHarvester;
|
|
|
|
public sealed class BluespaceHarvesterSystem : EntitySystem
|
|
{
|
|
[Dependency] private readonly IGameTiming _timing = default!;
|
|
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
|
[Dependency] private readonly TransformSystem _transform = default!;
|
|
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
|
[Dependency] private readonly UserInterfaceSystem _ui = default!;
|
|
[Dependency] private readonly IRobustRandom _random = default!;
|
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
[Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!;
|
|
|
|
private readonly ISawmill _sawmill = Logger.GetSawmill("TEST.bluespaceHarvester");
|
|
|
|
private readonly List<BluespaceHarvesterTap> _taps =
|
|
[
|
|
new() { Level = 0, Visual = BluespaceHarvesterVisuals.Tap0 },
|
|
new() { Level = 1, Visual = BluespaceHarvesterVisuals.Tap1 },
|
|
new() { Level = 5, Visual = BluespaceHarvesterVisuals.Tap2 },
|
|
new() { Level = 10, Visual = BluespaceHarvesterVisuals.Tap3 },
|
|
new() { Level = 15, Visual = BluespaceHarvesterVisuals.Tap4 },
|
|
new() { Level = 20, Visual = BluespaceHarvesterVisuals.Tap5 },
|
|
];
|
|
|
|
private float _updateTimer;
|
|
private const float UpdateTime = 1.0f;
|
|
|
|
private EntityQuery<BluespaceHarvesterComponent> _harvesterQuery;
|
|
|
|
public override void Initialize()
|
|
{
|
|
base.Initialize();
|
|
|
|
_harvesterQuery = GetEntityQuery<BluespaceHarvesterComponent>();
|
|
|
|
SubscribeLocalEvent<BluespaceHarvesterComponent, ComponentStartup>(OnStartup);
|
|
SubscribeLocalEvent<BluespaceHarvesterComponent, ComponentRemove>(OnRemove);
|
|
|
|
SubscribeLocalEvent<BluespaceHarvesterComponent, PowerConsumerReceivedChanged>(ReceivedChanged);
|
|
SubscribeLocalEvent<BluespaceHarvesterComponent, BluespaceHarvesterTargetLevelMessage>(OnTargetLevel);
|
|
SubscribeLocalEvent<BluespaceHarvesterComponent, BluespaceHarvesterBuyMessage>(OnBuy);
|
|
SubscribeLocalEvent<BluespaceHarvesterComponent, DestructionEventArgs>(OnDestruction);
|
|
}
|
|
|
|
private void OnStartup(Entity<BluespaceHarvesterComponent> ent, ref ComponentStartup args)
|
|
{
|
|
_sawmill.Info($"Bluespace harvester {ToPrettyString(ent)} started on map {Transform(ent).MapID}");
|
|
UpdateCount();
|
|
}
|
|
|
|
private void OnRemove(Entity<BluespaceHarvesterComponent> ent, ref ComponentRemove args)
|
|
{
|
|
_sawmill.Info($"Bluespace harvester {ToPrettyString(ent)} removed from map {Transform(ent).MapID}");
|
|
UpdateCount();
|
|
}
|
|
|
|
private void UpdateCount()
|
|
{
|
|
var dictionary = new Dictionary<MapId, List<EntityUid>>();
|
|
|
|
var query = EntityQueryEnumerator<BluespaceHarvesterComponent>();
|
|
while (query.MoveNext(out var entityUid, out _))
|
|
{
|
|
var mapId = Transform(entityUid).MapID;
|
|
var list = dictionary.GetOrNew(mapId);
|
|
list.Add(entityUid);
|
|
}
|
|
|
|
query = EntityQueryEnumerator<BluespaceHarvesterComponent>();
|
|
while (query.MoveNext(out var entityUid, out var harvester))
|
|
{
|
|
var mapId = Transform(entityUid).MapID;
|
|
harvester.Harvesters = dictionary[mapId].Count;
|
|
}
|
|
}
|
|
|
|
private void ReceivedChanged(Entity<BluespaceHarvesterComponent> ent, ref PowerConsumerReceivedChanged args)
|
|
{
|
|
ent.Comp.ReceivedPower = args.ReceivedPower;
|
|
ent.Comp.DrawRate = args.DrawRate;
|
|
}
|
|
|
|
public override void Update(float frameTime)
|
|
{
|
|
base.Update(frameTime);
|
|
|
|
_updateTimer += frameTime;
|
|
|
|
if (_updateTimer < UpdateTime)
|
|
return;
|
|
|
|
_updateTimer -= UpdateTime;
|
|
|
|
var query = EntityQueryEnumerator<BluespaceHarvesterComponent, PowerConsumerComponent>();
|
|
while (query.MoveNext(out var uid, out var harvester, out var consumer))
|
|
{
|
|
// We start only after manual switching on.
|
|
if (harvester is { Reset: false, CurrentLevel: 0 })
|
|
harvester.Reset = true;
|
|
|
|
// The HV wires cannot transmit a lot of electricity so quickly,
|
|
// which is why it will not start.
|
|
// So this is simply using the amount of free electricity in the network.
|
|
if (harvester.ReceivedPower < GetUsagePower(harvester.CurrentLevel) && harvester.CurrentLevel != 0)
|
|
{
|
|
// If there is insufficient production,
|
|
// it will reset itself (turn off) and you will need to start it again,
|
|
// this will not allow you to set it to maximum and enjoy life
|
|
_sawmill.Warning($"Bluespace harvester {ToPrettyString(uid)} reset due to insufficient power. Received: {harvester.ReceivedPower}, Required: {GetUsagePower(harvester.CurrentLevel)}, Level: {harvester.CurrentLevel}");
|
|
Reset(uid, harvester);
|
|
}
|
|
|
|
if (harvester.Reset)
|
|
{
|
|
if (harvester.CurrentLevel < harvester.TargetLevel)
|
|
harvester.CurrentLevel++;
|
|
}
|
|
|
|
if (harvester.CurrentLevel > harvester.TargetLevel)
|
|
harvester.CurrentLevel--;
|
|
|
|
// Increasing the amount of energy regardless of its ability to generate it
|
|
// will make it impossible to set the desired value and go to rest.
|
|
consumer.DrawRate = GetUsagePower(harvester.CurrentLevel);
|
|
|
|
var generation = GetPointGeneration(uid, harvester);
|
|
harvester.Points += generation;
|
|
harvester.TotalPoints += generation;
|
|
|
|
// the generation of danger points can be negative, so there is this limitation here.
|
|
harvester.Danger += GetDangerPointGeneration(uid, harvester);
|
|
if (harvester.Danger < 0)
|
|
harvester.Danger = 0;
|
|
|
|
// If the danger points exceeded the DangerLimit and we were lucky enough to create a portal, then they will be created.
|
|
if (harvester.Danger > harvester.DangerLimit && _random.NextFloat(0.0f, 1.0f) <= GetRiftChance(uid, harvester))
|
|
{
|
|
_sawmill.Info($"Bluespace harvester {ToPrettyString(uid)} spawning rifts. Danger: {harvester.Danger}, Limit: {harvester.DangerLimit}");
|
|
SpawnRifts(uid, harvester);
|
|
}
|
|
|
|
if (TryComp<AmbientSoundComponent>(uid, out var ambient))
|
|
_ambientSound.SetAmbience(uid, harvester.Reset, ambient); // Bzhzh, bzhzh
|
|
|
|
UpdateAppearance(uid, harvester);
|
|
UpdateUI(uid, harvester);
|
|
}
|
|
}
|
|
|
|
private void OnDestruction(Entity<BluespaceHarvesterComponent> harvester, ref DestructionEventArgs args)
|
|
{
|
|
_sawmill.Warning($"Bluespace harvester {ToPrettyString(harvester.Owner)} destroyed, spawning emergency rifts");
|
|
SpawnRifts(harvester.Owner, harvester.Comp);
|
|
}
|
|
|
|
private void OnTargetLevel(Entity<BluespaceHarvesterComponent> harvester, ref BluespaceHarvesterTargetLevelMessage args)
|
|
{
|
|
// If we switch off, we don't need to be switched on.
|
|
if (!harvester.Comp.Reset)
|
|
{
|
|
_sawmill.Debug($"Bluespace harvester {ToPrettyString(harvester.Owner)} target level change rejected: harvester is reset");
|
|
return;
|
|
}
|
|
|
|
_sawmill.Info($"Bluespace harvester {ToPrettyString(harvester.Owner)} target level changed from {harvester.Comp.TargetLevel} to {args.TargetLevel}");
|
|
harvester.Comp.TargetLevel = args.TargetLevel;
|
|
UpdateUI(harvester.Owner, harvester.Comp);
|
|
}
|
|
|
|
private void OnBuy(Entity<BluespaceHarvesterComponent> harvester, ref BluespaceHarvesterBuyMessage args)
|
|
{
|
|
if (!harvester.Comp.Reset)
|
|
{
|
|
_sawmill.Debug($"Bluespace harvester {ToPrettyString(harvester.Owner)} purchase rejected: harvester is reset");
|
|
return;
|
|
}
|
|
|
|
if (!TryGetCategory(harvester.Owner, args.Category, out var info, harvester.Comp))
|
|
{
|
|
_sawmill.Warning($"Bluespace harvester {ToPrettyString(harvester.Owner)} purchase failed: category {args.Category} not found");
|
|
return;
|
|
}
|
|
|
|
var category = (BluespaceHarvesterCategoryInfo) info;
|
|
|
|
if (harvester.Comp.Points < category.Cost)
|
|
{
|
|
_sawmill.Debug($"Bluespace harvester {ToPrettyString(harvester.Owner)} purchase rejected: insufficient points. Have: {harvester.Comp.Points}, Need: {category.Cost}");
|
|
return;
|
|
}
|
|
|
|
_sawmill.Info($"Bluespace harvester {ToPrettyString(harvester.Owner)} purchased {category.PrototypeId} for {category.Cost} points. Remaining: {harvester.Comp.Points - category.Cost}");
|
|
harvester.Comp.Points -= category.Cost; // Damn capitalism.
|
|
SpawnLoot(harvester.Owner, category.PrototypeId, harvester.Comp);
|
|
}
|
|
|
|
private void UpdateAppearance(EntityUid uid, BluespaceHarvesterComponent? harvester = null)
|
|
{
|
|
if (!Resolve(uid, ref harvester))
|
|
return;
|
|
|
|
var level = harvester.CurrentLevel;
|
|
BluespaceHarvesterTap? max = null;
|
|
|
|
foreach (var tap in _taps)
|
|
{
|
|
if (tap.Level > level)
|
|
continue;
|
|
|
|
if (max == null || tap.Level > max.Level)
|
|
max = tap;
|
|
}
|
|
|
|
// We get the biggest Tap of all, and replace it with a harvester.
|
|
if (max == null)
|
|
return;
|
|
|
|
if (Emagged(uid))
|
|
_appearance.SetData(uid, BluespaceHarvesterVisualLayers.Base, (int) harvester.RedspaceTap);
|
|
else
|
|
_appearance.SetData(uid, BluespaceHarvesterVisualLayers.Base, (int) max.Visual);
|
|
|
|
_appearance.SetData(uid, BluespaceHarvesterVisualLayers.Effects, level != 0);
|
|
}
|
|
|
|
private void UpdateUI(EntityUid uid, BluespaceHarvesterComponent? harvester = null)
|
|
{
|
|
if (!Resolve(uid, ref harvester))
|
|
return;
|
|
|
|
_ui.SetUiState(uid,
|
|
BluespaceHarvesterUiKey.Key,
|
|
new BluespaceHarvesterBoundUserInterfaceState(
|
|
harvester.TargetLevel,
|
|
harvester.CurrentLevel,
|
|
harvester.MaxLevel,
|
|
GetUsagePower(harvester.CurrentLevel),
|
|
GetUsageNextPower(harvester.CurrentLevel),
|
|
harvester.Points,
|
|
harvester.TotalPoints,
|
|
GetPointGeneration(uid, harvester),
|
|
harvester.Categories
|
|
));
|
|
}
|
|
|
|
private uint GetUsageNextPower(int level)
|
|
{
|
|
return GetUsagePower(level + 1);
|
|
}
|
|
|
|
private uint GetUsagePower(int level)
|
|
{
|
|
// Hopefully in the future you will need to put a mathematical formula or function here.
|
|
return level switch
|
|
{
|
|
0 => 500,
|
|
1 => 1_000,
|
|
2 => 5_000,
|
|
3 => 50_000,
|
|
4 => 100_000,
|
|
5 => 500_000,
|
|
6 => 1_000_000,
|
|
7 => 2_000_000,
|
|
8 => 3_000_000,
|
|
9 => 5_000_000,
|
|
10 => 7_000_000,
|
|
11 => 9_000_000,
|
|
12 => 10_000_000,
|
|
13 => 12_000_000,
|
|
14 => 14_000_000,
|
|
15 => 16_000_000,
|
|
16 => 20_000_000,
|
|
17 => 40_000_000,
|
|
18 => 80_000_000,
|
|
19 => 100_000_000,
|
|
20 => 200_000_000,
|
|
_ => 1_000_000_000,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds a free point in space and creates a prototype there, similar to a bluespace anomaly.
|
|
/// </summary>
|
|
private EntityUid? SpawnLoot(EntityUid uid, string prototype, BluespaceHarvesterComponent? harvester = null)
|
|
{
|
|
if (!Resolve(uid, ref harvester))
|
|
return null;
|
|
|
|
var xform = Transform(uid);
|
|
var coords = xform.Coordinates;
|
|
var newCoords = coords.Offset(_random.NextVector2(harvester.SpawnRadius));
|
|
|
|
for (var i = 0; i < 20; i++)
|
|
{
|
|
var randVector = _random.NextVector2(harvester.SpawnRadius);
|
|
newCoords = coords.Offset(randVector);
|
|
|
|
if (!_lookup.GetEntitiesIntersecting(newCoords.ToMap(EntityManager, _transform), LookupFlags.Static).Any())
|
|
break;
|
|
}
|
|
|
|
_sawmill.Debug($"Bluespace harvester {ToPrettyString(uid)} spawning loot {prototype} at {newCoords}");
|
|
_audio.PlayPvs(harvester.SpawnSound, uid);
|
|
Spawn(harvester.SpawnEffect, newCoords);
|
|
|
|
var result = Spawn(prototype, newCoords);
|
|
_sawmill.Debug($"Bluespace harvester {ToPrettyString(uid)} spawned {ToPrettyString(result)}");
|
|
return result;
|
|
}
|
|
|
|
private int GetPointGeneration(EntityUid uid, BluespaceHarvesterComponent? harvester = null)
|
|
{
|
|
if (!Resolve(uid, ref harvester))
|
|
return 0;
|
|
|
|
return harvester.CurrentLevel * 4 * (Emagged(uid) ? 2 : 1) * (harvester.ResetTime == TimeSpan.Zero ? 1 : 0);
|
|
}
|
|
|
|
private int GetDangerPointGeneration(EntityUid uid, BluespaceHarvesterComponent? harvester = null)
|
|
{
|
|
if (!Resolve(uid, ref harvester))
|
|
return 0;
|
|
|
|
var stable = GetStableLevel(uid, harvester);
|
|
if (harvester.CurrentLevel < stable && harvester.CurrentLevel != 0)
|
|
return -1;
|
|
|
|
if (harvester.CurrentLevel == stable)
|
|
return 0;
|
|
|
|
return (harvester.CurrentLevel - stable) * 4;
|
|
}
|
|
|
|
private float GetRiftChance(EntityUid uid, BluespaceHarvesterComponent? harvester = null)
|
|
{
|
|
if (!Resolve(uid, ref harvester))
|
|
return 0;
|
|
|
|
return Emagged(uid) ? harvester.EmaggedRiftChance : harvester.RiftChance;
|
|
}
|
|
|
|
private int GetStableLevel(EntityUid uid, BluespaceHarvesterComponent? harvester = null)
|
|
{
|
|
if (!Resolve(uid, ref harvester))
|
|
return 0;
|
|
|
|
if (harvester.Harvesters > 1)
|
|
return 0;
|
|
|
|
return Emagged(uid) ? harvester.EmaggedStableLevel : harvester.StableLevel;
|
|
}
|
|
|
|
private bool TryGetCategory(EntityUid uid, BluespaceHarvesterCategory target, [NotNullWhen(true)] out BluespaceHarvesterCategoryInfo? info, BluespaceHarvesterComponent? harvester = null)
|
|
{
|
|
info = null;
|
|
if (!Resolve(uid, ref harvester))
|
|
return false;
|
|
|
|
foreach (var category in harvester.Categories)
|
|
{
|
|
if (category.Type != target)
|
|
continue;
|
|
|
|
info = category;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void Reset(EntityUid uid, BluespaceHarvesterComponent? harvester = null)
|
|
{
|
|
if (!Resolve(uid, ref harvester))
|
|
return;
|
|
|
|
_sawmill.Info($"Bluespace harvester {ToPrettyString(uid)} reset. Danger increased by {harvester.DangerFromReset} (now {harvester.Danger + harvester.DangerFromReset})");
|
|
harvester.Danger += harvester.DangerFromReset;
|
|
harvester.Reset = false;
|
|
harvester.TargetLevel = 0;
|
|
}
|
|
|
|
private bool Emagged(EntityUid uid)
|
|
{
|
|
return HasComp<EmaggedComponent>(uid);
|
|
}
|
|
|
|
private void SpawnRifts(EntityUid uid, BluespaceHarvesterComponent? harvester = null, int? danger = null)
|
|
{
|
|
if (!Resolve(uid, ref harvester))
|
|
return;
|
|
|
|
int currentDanger = danger ?? harvester.Danger;
|
|
|
|
var count = _random.Next(harvester.RiftCount);
|
|
_sawmill.Info($"Bluespace harvester {ToPrettyString(uid)} spawning {count} rifts with total danger {currentDanger}");
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
// Haha loot!
|
|
var entity = SpawnLoot(uid, harvester.Rift, harvester);
|
|
if (entity == null)
|
|
{
|
|
_sawmill.Warning($"Bluespace harvester {ToPrettyString(uid)} failed to spawn rift {i + 1}/{count}");
|
|
continue;
|
|
}
|
|
|
|
var riftDanger = currentDanger / count;
|
|
EnsureComp<BluespaceHarvesterRiftComponent>((EntityUid) entity).Danger = riftDanger;
|
|
_sawmill.Debug($"Bluespace harvester {ToPrettyString(uid)} spawned rift {ToPrettyString(entity)} with danger {riftDanger}");
|
|
}
|
|
|
|
// We gave all the danger to the rifts.
|
|
harvester.Danger = 0;
|
|
}
|
|
}
|