Files
tbd-station-14/Content.Server/Salvage/JobBoard/SalvageJobBoardSystem.cs
2025-06-01 17:06:34 +10:00

295 lines
9.9 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Cargo.Components;
using Content.Server.Cargo.Systems;
using Content.Server.Radio.EntitySystems;
using Content.Server.Station.Systems;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Labels.EntitySystems;
using Content.Shared.Paper;
using Content.Shared.Radio;
using Content.Shared.Salvage.JobBoard;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Salvage.JobBoard;
public sealed class SalvageJobBoardSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly CargoSystem _cargo = default!;
[Dependency] private readonly LabelSystem _label = default!;
[Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
/// <summary>
/// Radio channel that unlock messages are broadcast on.
/// </summary>
private static readonly ProtoId<RadioChannelPrototype> UnlockChannel = "Supply";
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<EntitySoldEvent>(OnSold);
SubscribeLocalEvent<SalvageJobBoardConsoleComponent, BoundUIOpenedEvent>(OnBUIOpened);
Subs.BuiEvents<SalvageJobBoardConsoleComponent>(SalvageJobBoardUiKey.Key,
subs =>
{
subs.Event<JobBoardPrintLabelMessage>(OnPrintLabelMessage);
});
}
private void OnSold(ref EntitySoldEvent args)
{
if (!TryComp<SalvageJobsDataComponent>(args.Station, out var salvageJobsData))
return;
foreach (var sold in args.Sold)
{
if (!FulfillsSalvageJob(sold, (args.Station, salvageJobsData), out var jobId))
continue;
TryCompleteSalvageJob((args.Station, salvageJobsData), jobId.Value);
}
}
/// <summary>
/// Gets the jobs that the station can currently access.
/// </summary>
public List<ProtoId<CargoBountyPrototype>> GetAvailableJobs(Entity<SalvageJobsDataComponent> ent)
{
var outJobs = new List<ProtoId<CargoBountyPrototype>>();
var availableGroups = new HashSet<ProtoId<CargoBountyGroupPrototype>>();
var completedCount = ent.Comp.CompletedJobs.Count;
foreach (var (thresholds, rank) in ent.Comp.RankThresholds)
{
if (completedCount < thresholds)
continue;
if (rank.BountyGroup == null)
continue;
availableGroups.Add(rank.BountyGroup.Value);
}
foreach (var bounty in _prototypeManager.EnumeratePrototypes<CargoBountyPrototype>())
{
if (ent.Comp.CompletedJobs.Contains(bounty))
continue;
if (availableGroups.Contains(bounty.Group))
outJobs.Add(bounty);
}
return outJobs;
}
/// <summary>
/// Gets the "progression" of a rank, expressed as on the range [0, 1]
/// </summary>
public float GetRankProgression(Entity<SalvageJobsDataComponent> ent)
{
// Need to have at least two of these.
if (ent.Comp.RankThresholds.Count <= 1)
return 1;
var completedCount = ent.Comp.CompletedJobs.Count;
for (var i = ent.Comp.RankThresholds.Count - 1; i >= 0; i--)
{
var low = ent.Comp.RankThresholds.Keys.ElementAt(i);
if (completedCount < low)
continue;
// don't worry abooouuuuut it (it'll be O K !)
var high = i != ent.Comp.RankThresholds.Count - 1
? ent.Comp.RankThresholds.Keys.ElementAt(i + 1)
: _prototypeManager.EnumeratePrototypes<CargoBountyPrototype>()
.Count(p => ent.Comp.RankThresholds.Values
.Select(r => r.BountyGroup)
.Contains(p.Group));
return (completedCount - low) / (float)(high - low);
}
return 1f;
}
/// <summary>
/// Checks if the current station is the max rank
/// </summary>
public bool IsMaxRank(Entity<SalvageJobsDataComponent> ent)
{
return GetAvailableJobs(ent).Count == 0;
}
/// <summary>
/// Gets the current rank of the station
/// </summary>
public SalvageRankDatum GetRank(Entity<SalvageJobsDataComponent> ent)
{
if (IsMaxRank(ent))
return ent.Comp.MaxRank;
var completedCount = ent.Comp.CompletedJobs.Count;
foreach (var (threshold, rank) in ent.Comp.RankThresholds.Reverse())
{
if (completedCount < threshold)
continue;
return rank;
}
// base case
return ent.Comp.RankThresholds[0];
}
/// <summary>
///
/// </summary>
/// <param name="ent"></param>
/// <param name="job"></param>
/// <returns></returns>
public bool TryCompleteSalvageJob(Entity<SalvageJobsDataComponent> ent, ProtoId<CargoBountyPrototype> job)
{
if (!GetAvailableJobs(ent).Contains(job))
return false;
var jobProto = _prototypeManager.Index(job);
var oldRank = GetRank(ent);
ent.Comp.CompletedJobs.Add(job);
var newRank = GetRank(ent);
// Add reward
if (TryComp<StationBankAccountComponent>(ent, out var stationBankAccount))
{
_cargo.UpdateBankAccount(
(ent.Owner, stationBankAccount),
jobProto.Reward,
_cargo.CreateAccountDistribution((ent, stationBankAccount)));
}
// We ranked up!
if (oldRank != newRank)
{
// We need to find a computer to send the message from.
var computerQuery = EntityQueryEnumerator<SalvageJobBoardConsoleComponent>();
while (computerQuery.MoveNext(out var uid, out _))
{
var message = Loc.GetString("job-board-radio-announce", ("rank", FormattedMessage.RemoveMarkupPermissive(Loc.GetString(newRank.Title))));
_radio.SendRadioMessage(uid, message, UnlockChannel, uid, false);
break;
}
if (newRank.UnlockedMarket is { } market &&
TryComp<StationCargoOrderDatabaseComponent>(ent, out var stationCargoOrder))
{
stationCargoOrder.Markets.Add(market);
}
}
var enumerator = EntityQueryEnumerator<SalvageJobBoardConsoleComponent>();
while (enumerator.MoveNext(out var consoleUid, out var console))
{
UpdateUi((consoleUid, console), ent);
}
return true;
}
/// <summary>
/// Checks if a given entity fulfills a bounty for the station.
/// </summary>
public bool FulfillsSalvageJob(EntityUid uid, Entity<SalvageJobsDataComponent>? station, [NotNullWhen(true)] out ProtoId<CargoBountyPrototype>? job)
{
job = null;
if (!_label.TryGetLabel<JobBoardLabelComponent>(uid, out var labelEnt))
return false;
if (labelEnt.Value.Comp.JobId is not { } jobId)
return false;
job = jobId;
if (station is null)
{
if (_station.GetOwningStation(uid) is not { } stationUid ||
!TryComp<SalvageJobsDataComponent>(stationUid, out var stationComp))
return false;
station = (stationUid, stationComp);
}
if (!GetAvailableJobs((station.Value, station.Value.Comp)).Contains(job.Value))
return false;
if (!_cargo.IsBountyComplete(uid, job))
return false;
return true;
}
private void OnBUIOpened(Entity<SalvageJobBoardConsoleComponent> ent, ref BoundUIOpenedEvent args)
{
if (args.UiKey is not SalvageJobBoardUiKey.Key)
return;
if (_station.GetOwningStation(ent.Owner) is not { } station ||
!TryComp<SalvageJobsDataComponent>(station, out var jobData))
return;
UpdateUi(ent, (station, jobData));
}
private void OnPrintLabelMessage(Entity<SalvageJobBoardConsoleComponent> ent, ref JobBoardPrintLabelMessage args)
{
if (_timing.CurTime < ent.Comp.NextPrintTime)
return;
if (_station.GetOwningStation(ent) is not { } station ||
!TryComp<SalvageJobsDataComponent>(station, out var jobsData))
return;
if (!_prototypeManager.TryIndex<CargoBountyPrototype>(args.JobId, out var job))
return;
if (!GetAvailableJobs((station, jobsData)).Contains(args.JobId))
return;
_audio.PlayPvs(ent.Comp.PrintSound, ent);
var label = SpawnAtPosition(ent.Comp.LabelEntity, Transform(ent).Coordinates);
EnsureComp<JobBoardLabelComponent>(label).JobId = job.ID;
var target = new List<string>();
foreach (var entry in job.Entries)
{
target.Add(Loc.GetString("bounty-console-manifest-entry",
("amount", entry.Amount),
("item", Loc.GetString(entry.Name))));
}
_paper.SetContent(label, Loc.GetString("job-board-label-text", ("target", string.Join(',', target)), ("reward", job.Reward)));
ent.Comp.NextPrintTime = _timing.CurTime + ent.Comp.PrintDelay;
}
private void UpdateUi(Entity<SalvageJobBoardConsoleComponent> ent, Entity<SalvageJobsDataComponent> stationEnt)
{
var state = new SalvageJobBoardConsoleState(
GetRank(stationEnt).Title,
GetRankProgression(stationEnt),
GetAvailableJobs(stationEnt));
_ui.SetUiState(ent.Owner, SalvageJobBoardUiKey.Key, state);
}
}