Files
wwdpublic/Content.Server/Store/Systems/StoreSystem.Ui.cs
VMSolidus 99125235e4 [Port] Uplink Discounts From White Dream (#930)
# Description

This is a port of
https://github.com/WWhiteDreamProject/wwdpublic/pull/11 from White
Dream. This feature selects random items in the traitor uplink each
round to be discounted and moved to the Discount tab, which are the same
for every traitor. This in theory helps encourage players to be
spontaneous, and use items that they otherwise might not normally
consider using, which helps mix things up from round to round.

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

> # Описание PR
> Порт скидок в аплинке.
> 
> # Изменения
> 🆑 Spatison
> 
> * add: Added discounts in uplink / Добавлены скидки в аплинк

</p>
</details>

# Changelog

🆑 Spatison
add: Added discounts in uplink / Добавлены скидки в аплинк

---------

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: Spatison <137375981+Spatison@users.noreply.github.com>
Co-authored-by: DEATHB4DEFEAT <77995199+DEATHB4DEFEAT@users.noreply.github.com>
2024-10-19 13:16:16 +07:00

389 lines
14 KiB
C#

using System.Linq;
using Content.Server.Actions;
using Content.Server.Administration.Logs;
using Content.Server.PDA.Ringer;
using Content.Server.Stack;
using Content.Server.Store.Components;
using Content.Shared.Actions;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Mind;
using Content.Shared.Store;
using Content.Shared.UserInterface;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Store.Systems;
public sealed partial class StoreSystem
{
[Dependency] private readonly IAdminLogManager _admin = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly ActionsSystem _actions = default!;
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly ActionUpgradeSystem _actionUpgrade = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly StackSystem _stack = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private void InitializeUi()
{
SubscribeLocalEvent<StoreComponent, StoreRequestUpdateInterfaceMessage>(OnRequestUpdate);
SubscribeLocalEvent<StoreComponent, StoreBuyListingMessage>(OnBuyRequest);
SubscribeLocalEvent<StoreComponent, StoreRequestWithdrawMessage>(OnRequestWithdraw);
SubscribeLocalEvent<StoreComponent, StoreRequestRefundMessage>(OnRequestRefund);
SubscribeLocalEvent<StoreComponent, RefundEntityDeletedEvent>(OnRefundEntityDeleted);
}
private void OnRefundEntityDeleted(Entity<StoreComponent> ent, ref RefundEntityDeletedEvent args)
{
ent.Comp.BoughtEntities.Remove(args.Uid);
}
/// <summary>
/// Toggles the store Ui open and closed
/// </summary>
/// <param name="user">the person doing the toggling</param>
/// <param name="storeEnt">the store being toggled</param>
/// <param name="component"></param>
public void ToggleUi(EntityUid user, EntityUid storeEnt, StoreComponent? component = null)
{
if (!Resolve(storeEnt, ref component))
return;
if (!TryComp<ActorComponent>(user, out var actor))
return;
if (!_ui.TryToggleUi(storeEnt, StoreUiKey.Key, actor.PlayerSession))
return;
UpdateUserInterface(user, storeEnt, component);
}
/// <summary>
/// Closes the store UI for everyone, if it's open
/// </summary>
public void CloseUi(EntityUid uid, StoreComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
_ui.TryCloseAll(uid, StoreUiKey.Key);
}
/// <summary>
/// Updates the user interface for a store and refreshes the listings
/// </summary>
/// <param name="user">The person who if opening the store ui. Listings are filtered based on this.</param>
/// <param name="store">The store entity itself</param>
/// <param name="component">The store component being refreshed.</param>
/// <param name="ui"></param>
public void UpdateUserInterface(EntityUid? user, EntityUid store, StoreComponent? component = null, PlayerBoundUserInterface? ui = null)
{
if (!Resolve(store, ref component))
return;
if (ui == null && !_ui.TryGetUi(store, StoreUiKey.Key, out ui))
return;
//this is the person who will be passed into logic for all listing filtering.
if (user != null) //if we have no "buyer" for this update, then don't update the listings
{
component.LastAvailableListings = GetAvailableListings(component.AccountOwner ?? user.Value, store, component).ToHashSet();
}
//dictionary for all currencies, including 0 values for currencies on the whitelist
Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> allCurrency = new();
foreach (var supported in component.CurrencyWhitelist)
{
allCurrency.Add(supported, FixedPoint2.Zero);
if (component.Balance.TryGetValue(supported, out var value))
allCurrency[supported] = value;
}
// TODO: if multiple users are supposed to be able to interact with a single BUI & see different
// stores/listings, this needs to use session specific BUI states.
// only tell operatives to lock their uplink if it can be locked
var showFooter = HasComp<RingerUplinkComponent>(store);
var state = new StoreUpdateState(component.LastAvailableListings, allCurrency, showFooter, component.RefundAllowed);
_ui.SetUiState(ui, state);
}
private void OnRequestUpdate(EntityUid uid, StoreComponent component, StoreRequestUpdateInterfaceMessage args)
{
UpdateUserInterface(args.Session.AttachedEntity, GetEntity(args.Entity), component);
}
private void BeforeActivatableUiOpen(EntityUid uid, StoreComponent component, BeforeActivatableUIOpenEvent args)
{
UpdateUserInterface(args.User, uid, component);
}
/// <summary>
/// Handles whenever a purchase was made.
/// </summary>
private void OnBuyRequest(EntityUid uid, StoreComponent component, StoreBuyListingMessage msg)
{
var listing = component.Listings.FirstOrDefault(x => x.Equals(msg.Listing));
if (listing == null) //make sure this listing actually exists
{
Log.Debug("listing does not exist");
return;
}
if (msg.Session.AttachedEntity is not { Valid: true } buyer)
return;
//verify that we can actually buy this listing and it wasn't added
if (!ListingHasCategory(listing, component.Categories))
return;
//condition checking because why not
if (listing.Conditions != null)
{
var args = new ListingConditionArgs(component.AccountOwner ?? buyer, uid, listing, EntityManager);
var conditionsMet = listing.Conditions.All(condition => condition.Condition(args));
if (!conditionsMet)
return;
}
//check that we have enough money
foreach (var currency in listing.Cost)
{
if (!component.Balance.TryGetValue(currency.Key, out var balance) || balance < currency.Value)
{
return;
}
}
if (!IsOnStartingMap(uid, component))
component.RefundAllowed = false;
//subtract the cash
foreach (var (currency, value) in listing.Cost)
{
component.Balance[currency] -= value;
component.BalanceSpent.TryAdd(currency, FixedPoint2.Zero);
component.BalanceSpent[currency] += value;
}
//spawn entity
if (listing.ProductEntity != null)
{
var product = Spawn(listing.ProductEntity, Transform(buyer).Coordinates);
_hands.PickupOrDrop(buyer, product);
HandleRefundComp(uid, component, product);
var xForm = Transform(product);
if (xForm.ChildCount > 0)
{
var childEnumerator = xForm.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
component.BoughtEntities.Add(child);
}
}
}
//give action
if (!string.IsNullOrWhiteSpace(listing.ProductAction))
{
EntityUid? actionId;
// I guess we just allow duplicate actions?
// Allow duplicate actions and just have a single list buy for the buy-once ones.
if (!_mind.TryGetMind(buyer, out var mind, out _))
actionId = _actions.AddAction(buyer, listing.ProductAction);
else
actionId = _actionContainer.AddAction(mind, listing.ProductAction);
// Add the newly bought action entity to the list of bought entities
// And then add that action entity to the relevant product upgrade listing, if applicable
if (actionId != null)
{
HandleRefundComp(uid, component, actionId.Value);
if (listing.ProductUpgradeID != null)
{
foreach (var upgradeListing in component.Listings)
{
if (upgradeListing.ID == listing.ProductUpgradeID)
{
upgradeListing.ProductActionEntity = actionId.Value;
break;
}
}
}
}
}
if (listing is { ProductUpgradeID: not null, ProductActionEntity: not null })
{
if (listing.ProductActionEntity != null)
{
component.BoughtEntities.Remove(listing.ProductActionEntity.Value);
}
if (!_actionUpgrade.TryUpgradeAction(listing.ProductActionEntity, out var upgradeActionId))
{
if (listing.ProductActionEntity != null)
HandleRefundComp(uid, component, listing.ProductActionEntity.Value);
return;
}
listing.ProductActionEntity = upgradeActionId;
if (upgradeActionId != null)
HandleRefundComp(uid, component, upgradeActionId.Value);
}
if (listing.ProductEvent != null)
{
if (!listing.RaiseProductEventOnUser)
RaiseLocalEvent(listing.ProductEvent);
else
RaiseLocalEvent(buyer, listing.ProductEvent);
}
//log dat shit.
_admin.Add(LogType.StorePurchase, LogImpact.Low,
$"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager)}\" from {ToPrettyString(uid)}");
listing.PurchaseAmount++; //track how many times something has been purchased
_audio.PlayEntity(component.BuySuccessSound, msg.Session, uid); //cha-ching!
if (listing.SaleLimit != 0 && listing.DiscountValue > 0 && listing.PurchaseAmount >= listing.SaleLimit)
{
listing.DiscountValue = 0;
listing.Cost = listing.OldCost;
}
UpdateUserInterface(buyer, uid, component);
}
/// <summary>
/// Handles dispensing the currency you requested to be withdrawn.
/// </summary>
/// <remarks>
/// This would need to be done should a currency with decimal values need to use it.
/// not quite sure how to handle that
/// </remarks>
private void OnRequestWithdraw(EntityUid uid, StoreComponent component, StoreRequestWithdrawMessage msg)
{
//make sure we have enough cash in the bank and we actually support this currency
if (!component.Balance.TryGetValue(msg.Currency, out var currentAmount) || currentAmount < msg.Amount)
return;
//make sure a malicious client didn't send us random shit
if (!_proto.TryIndex<CurrencyPrototype>(msg.Currency, out var proto))
return;
//we need an actually valid entity to spawn. This check has been done earlier, but just in case.
if (proto.Cash == null || !proto.CanWithdraw)
return;
if (msg.Session.AttachedEntity is not { Valid: true } buyer)
return;
FixedPoint2 amountRemaining = msg.Amount;
var coordinates = Transform(buyer).Coordinates;
var sortedCashValues = proto.Cash.Keys.OrderByDescending(x => x).ToList();
foreach (var value in sortedCashValues)
{
var cashId = proto.Cash[value];
var amountToSpawn = (int) MathF.Floor((float) (amountRemaining / value));
var ents = _stack.SpawnMultiple(cashId, amountToSpawn, coordinates);
_hands.PickupOrDrop(buyer, ents.First());
amountRemaining -= value * amountToSpawn;
}
component.Balance[msg.Currency] -= msg.Amount;
UpdateUserInterface(buyer, uid, component);
}
private void OnRequestRefund(EntityUid uid, StoreComponent component, StoreRequestRefundMessage args)
{
// TODO: Remove guardian/holopara
if (args.Session.AttachedEntity is not { Valid: true } buyer)
return;
if (!IsOnStartingMap(uid, component))
{
component.RefundAllowed = false;
UpdateUserInterface(buyer, uid, component);
}
if (!component.RefundAllowed || component.BoughtEntities.Count == 0)
return;
_admin.Add(LogType.StoreRefund, LogImpact.Low, $"{ToPrettyString(buyer):player} has refunded their purchases from {ToPrettyString(uid):store}");
for (var i = component.BoughtEntities.Count - 1; i >= 0; i--)
{
var purchase = component.BoughtEntities[i];
if (!Exists(purchase))
continue;
component.BoughtEntities.RemoveAt(i);
if (_actions.TryGetActionData(purchase, out var actionComponent, logError: false))
{
_actionContainer.RemoveAction(purchase, actionComponent);
}
EntityManager.DeleteEntity(purchase);
}
component.BoughtEntities.Clear();
foreach (var (currency, value) in component.BalanceSpent)
{
component.Balance[currency] += value;
}
// Reset store back to its original state
RefreshAllListings(component);
component.BalanceSpent = new();
UpdateUserInterface(buyer, uid, component);
}
private void HandleRefundComp(EntityUid uid, StoreComponent component, EntityUid purchase)
{
component.BoughtEntities.Add(purchase);
var refundComp = EnsureComp<StoreRefundComponent>(purchase);
refundComp.StoreEntity = uid;
}
private bool IsOnStartingMap(EntityUid store, StoreComponent component)
{
var xform = Transform(store);
return component.StartingMap == xform.MapUid;
}
/// <summary>
/// Disables refunds for this store
/// </summary>
public void DisableRefund(EntityUid store, StoreComponent? component = null)
{
if (!Resolve(store, ref component))
return;
component.RefundAllowed = false;
}
}