using Content.Shared._DV.Objectives.Systems; using Content.Shared.Mind; using Content.Shared.Mind.Components; using Content.Shared.Objectives.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared._DV.Reputation; public sealed class ReputationSystem : EntitySystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedContractObjectiveSystem _contract = default!; [Dependency] private readonly SharedMindSystem _mind = default!; [Dependency] private readonly SharedObjectivesSystem _objectives = default!; [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; private List _levels = new(); public IReadOnlyList AllLevels => _levels; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnUnpaused); SubscribeLocalEvent(OnHandleState); Subs.BuiEvents(ContractsUiKey.Key, subs => { subs.Event(OnAcceptMessage); subs.Event(OnCompleteMessage); subs.Event(OnRejectMessage); }); SubscribeLocalEvent(OnPrototypesReloaded); CacheLevels(); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { PickOfferings((uid, comp)); } } #region Event Handlers private void OnInit(Entity ent, ref ComponentInit args) { _ui.SetUi(ent.Owner, ContractsUiKey.Key, new InterfaceData("ContractsBUI")); } private void OnMapInit(Entity ent, ref MapInitEvent args) { // creates the slots for fresh pdas UpdateLevel(ent); PickOfferings(ent); } private void OnShutdown(Entity ent, ref ComponentShutdown args) { // if the PDA is cremated or thrown in a singulo or something, // delete all the offerings and fail the active contracts foreach (var uid in ent.Comp.Offerings) { Del(uid); } foreach (var obj in ent.Comp.Objectives) { ContractFailed(ent, obj); } // unlink it from the mind if (TryComp(ent.Comp.Mind, out var mind)) mind.Pda = null; } private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) { for (var i = 0; i < ent.Comp.Slots.Count; i++) { var slot = ent.Comp.Slots[i]; slot.NextUnlock += args.PausedTime; ent.Comp.Slots[i] = slot; } for (var i = 0; i < ent.Comp.OfferingSlots.Count; i++) { var slot = ent.Comp.OfferingSlots[i]; slot.NextUnlock += args.PausedTime; ent.Comp.OfferingSlots[i] = slot; } Dirty(ent); } private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) { // update CurrentLevel for client after server changes it, so UI can use it UpdateLevel(ent); UpdateUI(ent); } private void OnAcceptMessage(Entity ent, ref ContractsAcceptMessage args) { var i = args.Index; if (i < 0 || i >= ent.Comp.Offerings.Count) return; if (ent.Comp.Offerings[i] is not {} objective || !TryTakeContract(ent, objective)) return; ent.Comp.Offerings[i] = null; ent.Comp.OfferingSlots[i] = new OfferingSlot { NextUnlock = _timing.CurTime + ent.Comp.AcceptDelay }; } private void OnCompleteMessage(Entity ent, ref ContractsCompleteMessage args) { TryCompleteContract(ent, args.Index); } private void OnRejectMessage(Entity ent, ref ContractsRejectMessage args) { TryRejectOffering(ent, args.Index); } #endregion #region Public API /// /// Add contracts to a traitor's PDA. /// Throws if you call this multiple times on the same mind or pda. /// public void AddContracts(EntityUid mob, EntityUid pda) { if (_mind.GetMind(mob) is not {} mindId) return; // AddComp so it will throw if you are trying to bulldoze a used mind or pda var contracts = AddComp(pda); var mind = AddComp(mindId); contracts.Mind = mindId; mind.Pda = pda; PickOfferings((pda, contracts)); } public void ToggleUI(EntityUid user, EntityUid uid) { UpdateUI(uid); _ui.TryToggleUi(uid, ContractsUiKey.Key, user); } private void UpdateUI(EntityUid uid) { _ui.SetUiState(uid, ContractsUiKey.Key, new ContractsState()); } /// /// Pick new offerings for open offering slots. /// public void PickOfferings(Entity ent) { if (GetMind(ent) is not {} mind || ent.Comp.CurrentLevel is not {} level) return; var difficulty = level.MaxDifficulty; var groups = level.OfferingGroups; for (var i = 0; i < ent.Comp.OfferingSlots.Count; i++) { // can't add a new offering yet if (ent.Comp.Offerings[i] != null || IsLocked(ent.Comp.OfferingSlots[i].NextUnlock)) continue; if (_objectives.GetRandomObjective(mind, mind, groups, difficulty) is not {} objective) { // prevent spinlock ent.Comp.OfferingSlots[i] = new OfferingSlot { NextUnlock = _timing.CurTime + ent.Comp.AcceptDelay }; Dirty(ent); continue; } ent.Comp.Offerings[i] = objective; ent.Comp.OfferingSlots[i] = new OfferingSlot { Title = _contract.ContractName(objective) }; Dirty(ent); } } /// /// Try to take a new contract by adding an existing objective entity. /// public bool TryTakeContract(Entity ent, EntityUid objective) { if (GetMind(ent) is not {} mind || FindOpenSlot(ent) is not {} index) { return false; } _mind.AddObjective(mind, mind, objective); ent.Comp.Objectives[index] = objective; var slot = ent.Comp.Slots[index]; slot.ObjectiveTitle = _contract.ContractName(objective); ent.Comp.Slots[index] = slot; Dirty(ent); var ev = new ContractTakenEvent(ent, mind); RaiseLocalEvent(objective, ref ev); return true; } /// /// If a contract's objective is complete, pays out etc and removes it. /// public bool TryCompleteContract(Entity ent, int index) { if (index < 0 || index >= ent.Comp.Slots.Count || ent.Comp.Objectives[index] is not {} objective || GetMind(ent) is not {} mind || !_objectives.IsCompleted(objective, mind)) { return false; } var ev = new ContractCompletedEvent(ent); RaiseLocalEvent(objective, ref ev); ClearSlot(ent, index, ent.Comp.CompleteDelay); return true; } public bool TryRejectOffering(Entity ent, int index) { if (index < 0 || index >= ent.Comp.OfferingSlots.Count || ent.Comp.Offerings[index] is not {} objective) { return false; } ent.Comp.Offerings[index] = null; ent.Comp.OfferingSlots[index] = new OfferingSlot { Title = null, NextUnlock = _timing.CurTime + ent.Comp.RejectDelay }; Dirty(ent); Del(objective); return true; } /// /// Call this to fail a contract if it becomes impossible to complete. /// E.g. trying to steal an item that gets deleted /// public bool TryFailContract(Entity ent, EntityUid objective) { if (FindContract(ent, objective) is not {} index) return false; ContractFailed(ent, objective); ClearSlot(ent, index, ent.Comp.CompleteDelay); return true; } /// /// Get the mind that belongs to a contracts PDA. /// public Entity? GetMind(Entity ent) { if (ent.Comp.Mind is not {} mindId) return null; if (!TryComp(mindId, out var mind)) return null; return (mindId, mind); } /// /// Get the contracts pda for a mind, if it exists. /// public Entity? GetMindContracts(EntityUid mindId) { if (CompOrNull(mindId)?.Pda is not {} pda) return null; if (!TryComp(pda, out var comp)) return null; return (pda, comp); } /// /// Gets the reputation for a mind, null if it had no . /// public int? GetMindReputation(EntityUid mindId) { if (CompOrNull(mindId)?.Pda is not {} pda) return null; return GetReputation(pda); } /// /// Gets the reputation for a PDA, null if it had no . /// public int? GetReputation(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return null; return ent.Comp.Reputation; } public bool GiveMindReputation(EntityUid mindId, int amount) { return amount != 0 && GetMindContracts(mindId) is {} contracts && GiveReputation(contracts, amount); } public bool GiveReputation(Entity ent, int amount) { if (amount == 0) return false; ent.Comp.Reputation = Math.Clamp(ent.Comp.Reputation + amount, 0, 100); Dirty(ent); if (TryComp(ent.Comp.Mind, out var mind)) mind.Reputation = ent.Comp.Reputation; UpdateLevel(ent); return true; } /// /// Gets the level prototype for a given reputation. /// public ReputationLevelPrototype? GetLevel(int rep) { foreach (var proto in _levels) { if (rep >= proto.Reputation) return proto; } return null; } #endregion private bool IsLocked(TimeSpan? nextUnlock) { return nextUnlock is {} unlock && _timing.CurTime < unlock; } private int? FindOpenSlot(Entity ent) { for (var i = 0; i < ent.Comp.Slots.Count; i++) { if (ent.Comp.Objectives[i] != null) continue; if (IsLocked(ent.Comp.Slots[i].NextUnlock)) continue; return i; } return null; } private int? FindContract(Entity ent, EntityUid objective) { for (var i = 0; i < ent.Comp.Slots.Count; i++) { if (ent.Comp.Objectives[i] == objective) return i; } return null; } private void ClearSlot(Entity ent, int index, TimeSpan delay) { // old objective is intentionally not deleted, objective stays in the character menu for your greentextful glory / redtextful shame ent.Comp.Objectives[index] = null; ent.Comp.Slots[index] = new ContractSlot() { NextUnlock = _timing.CurTime + delay }; Dirty(ent); } private void UpdateLevel(Entity ent) { var old = ent.Comp.CurrentLevel; ent.Comp.CurrentLevel = GetLevel(ent.Comp.Reputation); UpdateContractSlots(ent); UpdateOfferingSlots(ent); } private void UpdateContractSlots(Entity ent) { var oldSlots = ent.Comp.Slots.Count; var newSlots = ent.Comp.CurrentLevel?.MaxContracts ?? 0; if (oldSlots == newSlots) return; if (newSlots > oldSlots) { // levelling up, add new slot(s) for (var i = oldSlots; i < newSlots; i++) { ent.Comp.Objectives.Add(null); ent.Comp.Slots.Add(new ContractSlot()); } } else { // this should never happen but removing objectives just incase for (var i = newSlots; i > oldSlots; i--) { var j = i - 1; var objective = ent.Comp.Objectives[j]; ContractFailed(ent, objective); ent.Comp.Objectives.RemoveAt(j); ent.Comp.Slots.RemoveAt(j); } } Dirty(ent); } private void UpdateOfferingSlots(Entity ent) { var oldSlots = ent.Comp.OfferingSlots.Count; var newSlots = ent.Comp.CurrentLevel?.MaxOfferings ?? 0; if (oldSlots == newSlots) return; if (newSlots > oldSlots) { // levelling up, add new slot(s) for (var i = oldSlots; i < newSlots; i++) { ent.Comp.Offerings.Add(null); ent.Comp.OfferingSlots.Add(new OfferingSlot()); } } else { // this should never happen but removing objectives just incase for (var i = newSlots; i > oldSlots; i--) { var j = i - 1; var objective = ent.Comp.Offerings[j]; Del(objective); ent.Comp.Offerings.RemoveAt(j); ent.Comp.OfferingSlots.RemoveAt(j); } } Dirty(ent); } private void ContractFailed(Entity ent, EntityUid? uid) { if (GetMind(ent) is not {} mind) return; if (uid is not {} objective) return; var ev = new ContractFailedEvent(ent); RaiseLocalEvent(objective, ref ev); _mind.TryRemoveObjective(mind, objective); } private void OnPrototypesReloaded(PrototypesReloadedEventArgs args) { if (!args.WasModified()) return; CacheLevels(); } private void CacheLevels() { _levels.Clear(); foreach (var proto in _proto.EnumeratePrototypes()) { _levels.Add(proto); } // sort levels by their reputation requirement, descending // this allows GetLevel to work _levels.Sort((a, b) => (b.Reputation.CompareTo(a.Reputation))); } }