Departmental Economy (#36445)

* Cargo Accounts, Request Consoles, and lock boxes

* Funding Allocation Computer

* final changes

* test fix

* remove dumb code

* ScarKy0 review

* first cour

* second cour

* Update machines.yml

* review

---------

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Milon <milonpl.git@proton.me>
This commit is contained in:
Nemanja
2025-04-13 09:22:36 -04:00
committed by GitHub
parent 5f78b72763
commit 12b75beeab
62 changed files with 2106 additions and 331 deletions

View File

@@ -8,15 +8,15 @@ public sealed class CargoConsoleInterfaceState : BoundUserInterfaceState
public string Name;
public int Count;
public int Capacity;
public int Balance;
public NetEntity Station;
public List<CargoOrderData> Orders;
public CargoConsoleInterfaceState(string name, int count, int capacity, int balance, List<CargoOrderData> orders)
public CargoConsoleInterfaceState(string name, int count, int capacity, NetEntity station, List<CargoOrderData> orders)
{
Name = name;
Count = count;
Capacity = capacity;
Balance = balance;
Station = station;
Orders = orders;
}
}
}

View File

@@ -1,25 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Cargo.Components;
/// <summary>
/// Makes an entity a client of the station's bank account.
/// When its balance changes it will have <see cref="BankBalanceUpdatedEvent"/> raised on it.
/// Other systems can then use this for logic or to update ui states.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedCargoSystem))]
[AutoGenerateComponentState]
public sealed partial class BankClientComponent : Component
{
/// <summary>
/// The balance updated for the last station this entity was a part of.
/// </summary>
[DataField, AutoNetworkedField]
public int Balance;
}
/// <summary>
/// Raised on an entity with <see cref="BankClientComponent"/> when the bank's balance is updated.
/// </summary>
[ByRefEvent]
public readonly record struct BankBalanceUpdatedEvent(EntityUid Station, int Balance);

View File

@@ -1,33 +1,121 @@
using Content.Shared.Access;
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Content.Shared.Radio;
using Content.Shared.Stacks;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Cargo.Components;
/// <summary>
/// Handles sending order requests to cargo. Doesn't handle orders themselves via shuttle or telepads.
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedCargoSystem))]
public sealed partial class CargoOrderConsoleComponent : Component
{
[DataField("soundError")] public SoundSpecifier ErrorSound =
new SoundPathSpecifier("/Audio/Effects/Cargo/buzz_sigh.ogg");
/// <summary>
/// The account that this console pulls from for ordering.
/// </summary>
[DataField]
public ProtoId<CargoAccountPrototype> Account = "Cargo";
[DataField("soundConfirm")]
public SoundSpecifier ConfirmSound = new SoundPathSpecifier("/Audio/Effects/Cargo/ping.ogg");
[DataField]
public SoundSpecifier ErrorSound = new SoundCollectionSpecifier("CargoError");
/// <summary>
/// Sound made when <see cref="TransferUnbounded"/> is toggled.
/// </summary>
[DataField]
public SoundSpecifier ToggleLimitSound = new SoundCollectionSpecifier("CargoToggleLimit");
/// <summary>
/// If true, account transfers have no limit and a lower cooldown.
/// </summary>
[DataField, AutoNetworkedField]
public bool TransferUnbounded;
[ViewVariables]
public float TransferLimit => TransferUnbounded ? 1 : BaseTransferLimit;
/// <summary>
/// The maximum percent of total funds that can be transferred or withdrawn in one action.
/// </summary>
[DataField, AutoNetworkedField]
public float BaseTransferLimit = 0.20f;
/// <summary>
/// The time at which account actions can be performed again.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
public TimeSpan NextAccountActionTime;
[ViewVariables]
public TimeSpan AccountActionDelay => TransferUnbounded ? UnboundedAccountActionDelay : BaseAccountActionDelay;
/// <summary>
/// The minimum time between account actions when <see cref="TransferUnbounded"/> is false
/// </summary>
[DataField]
public TimeSpan BaseAccountActionDelay = TimeSpan.FromMinutes(1);
/// <summary>
/// The minimum time between account actions when <see cref="TransferUnbounded"/> is true
/// </summary>
[DataField]
public TimeSpan UnboundedAccountActionDelay = TimeSpan.FromSeconds(10);
/// <summary>
/// The stack representing cash dispensed on withdrawals.
/// </summary>
[DataField]
public ProtoId<StackPrototype> CashType = "Credit";
/// <summary>
/// All of the <see cref="CargoProductPrototype.Group"/>s that are supported.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public List<string> AllowedGroups = new() { "market" };
/// <summary>
/// Access needed to toggle the limit on this console.
/// </summary>
[DataField]
public HashSet<ProtoId<AccessLevelPrototype>> RemoveLimitAccess = new();
/// <summary>
/// Radio channel on which order approval announcements are transmitted
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public ProtoId<RadioChannelPrototype> AnnouncementChannel = "Supply";
/// <summary>
/// Secondary radio channel which always receives order announcements.
/// </summary>
public static readonly ProtoId<RadioChannelPrototype> BaseAnnouncementChannel = "Supply";
}
/// <summary>
/// Withdraw funds from an account
/// </summary>
[Serializable, NetSerializable]
public sealed class CargoConsoleWithdrawFundsMessage : BoundUserInterfaceMessage
{
public ProtoId<CargoAccountPrototype>? Account;
public int Amount;
public CargoConsoleWithdrawFundsMessage(ProtoId<CargoAccountPrototype>? account, int amount)
{
Account = account;
Amount = amount;
}
}
/// <summary>
/// Toggle the limit on withdrawals and transfers.
/// </summary>
[Serializable, NetSerializable]
public sealed class CargoConsoleToggleLimitMessage : BoundUserInterfaceMessage;

View File

@@ -0,0 +1,49 @@
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Cargo.Components;
/// <summary>
/// A console that manipulates the distribution of revenue on the station.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedCargoSystem))]
public sealed partial class FundingAllocationConsoleComponent : Component
{
/// <summary>
/// Sound played when the budget distribution is set.
/// </summary>
[DataField]
public SoundSpecifier SetDistributionSound = new SoundCollectionSpecifier("CargoPing");
}
[Serializable, NetSerializable]
public sealed class SetFundingAllocationBuiMessage : BoundUserInterfaceMessage
{
public Dictionary<ProtoId<CargoAccountPrototype>, int> Percents;
public SetFundingAllocationBuiMessage(Dictionary<ProtoId<CargoAccountPrototype>, int> percents)
{
Percents = percents;
}
}
[Serializable, NetSerializable]
public sealed class FundingAllocationConsoleBuiState : BoundUserInterfaceState
{
public NetEntity Station;
public FundingAllocationConsoleBuiState(NetEntity station)
{
Station = station;
}
}
[Serializable, NetSerializable]
public enum FundingAllocationConsoleUiKey : byte
{
Key
}

View File

@@ -0,0 +1,17 @@
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Cargo.Components;
/// <summary>
/// Makes a sellable object portion out its value to a specified department rather than the station default
/// </summary>
[RegisterComponent]
public sealed partial class OverrideSellComponent : Component
{
/// <summary>
/// The account that will receive the primary funds from this being sold.
/// </summary>
[DataField(required: true)]
public ProtoId<CargoAccountPrototype> OverrideAccount;
}

View File

@@ -0,0 +1,77 @@
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Cargo.Components;
/// <summary>
/// Added to the abstract representation of a station to track its money.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedCargoSystem)), AutoGenerateComponentPause, AutoGenerateComponentState]
public sealed partial class StationBankAccountComponent : Component
{
/// <summary>
/// The account that receives funds by default
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<CargoAccountPrototype> PrimaryAccount = "Cargo";
/// <summary>
/// When giving funds to a particular account, the proportion of funds they should receive compared to remaining accounts.
/// </summary>
[DataField, AutoNetworkedField]
public double PrimaryCut = 0.75;
/// <summary>
/// A dictionary corresponding to the money held by each cargo account.
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<ProtoId<CargoAccountPrototype>, int> Accounts = new()
{
{ "Cargo", 2000 },
{ "Engineering", 1000 },
{ "Medical", 1000 },
{ "Science", 1000 },
{ "Security", 1000 },
{ "Service", 1000 },
};
/// <summary>
/// A baseline distribution used for income and dispersing leftovers after sale.
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<ProtoId<CargoAccountPrototype>, double> RevenueDistribution = new()
{
{ "Cargo", 0.00 },
{ "Engineering", 0.25 },
{ "Medical", 0.30 },
{ "Science", 0.15 },
{ "Security", 0.20 },
{ "Service", 0.10 },
};
/// <summary>
/// How much the bank balance goes up per second, every Delay period. Rounded down when multiplied.
/// </summary>
[DataField]
public int IncreasePerSecond = 2;
/// <summary>
/// The time at which the station will receive its next deposit of passive income
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextIncomeTime;
/// <summary>
/// How much time to wait (in seconds) before increasing bank accounts balance.
/// </summary>
[DataField]
public TimeSpan IncomeDelay = TimeSpan.FromSeconds(50);
}
/// <summary>
/// Broadcast and raised on station ent whenever its balance is updated.
/// </summary>
[ByRefEvent]
public readonly record struct BankBalanceUpdatedEvent(EntityUid Station, Dictionary<ProtoId<CargoAccountPrototype>, int> Balance);

View File

@@ -0,0 +1,39 @@
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
namespace Content.Shared.Cargo.Prototypes;
/// <summary>
/// This is a prototype for a single account that stores money on StationBankAccountComponent
/// </summary>
[Prototype]
public sealed partial class CargoAccountPrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; } = default!;
/// <summary>
/// Full IC name of the account.
/// </summary>
[DataField]
public LocId Name;
/// <summary>
/// A shortened code used to refer to the account in UIs
/// </summary>
[DataField]
public LocId Code;
/// <summary>
/// Color corresponding to the account.
/// </summary>
[DataField]
public Color Color;
/// <summary>
/// Channel used for announcing transactions.
/// </summary>
[DataField]
public ProtoId<RadioChannelPrototype> RadioChannel;
}

View File

@@ -1,7 +1,54 @@
using System.Linq;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Cargo;
public abstract class SharedCargoSystem : EntitySystem
{
/// <summary>
/// For a given station, retrieves the balance in a specific account.
/// </summary>
public int GetBalanceFromAccount(Entity<StationBankAccountComponent?> station, ProtoId<CargoAccountPrototype> account)
{
if (!Resolve(station, ref station.Comp))
return 0;
return station.Comp.Accounts.GetValueOrDefault(account);
}
/// <summary>
/// For a station, creates a distribution between one "primary" account and the other accounts.
/// The primary account receives the majority cut specified, with the remaining accounts getting cuts
/// distributed through the remaining amount, based on <see cref="StationBankAccountComponent.RevenueDistribution"/>
/// </summary>
public Dictionary<ProtoId<CargoAccountPrototype>, double> CreateAccountDistribution(
ProtoId<CargoAccountPrototype> primary,
StationBankAccountComponent stationBank,
double primaryCut = 1.0)
{
var distribution = new Dictionary<ProtoId<CargoAccountPrototype>, double>
{
{ primary, primaryCut }
};
var remaining = 1.0 - primaryCut;
var allAccountPercentages = new Dictionary<ProtoId<CargoAccountPrototype>, double>(stationBank.RevenueDistribution);
allAccountPercentages.Remove(primary);
var weightsSum = allAccountPercentages.Values.Sum();
foreach (var (account, percentage) in allAccountPercentages)
{
var adjustedPercentage = percentage / weightsSum;
distribution.Add(account, remaining * adjustedPercentage);
}
return distribution;
}
}
[NetSerializable, Serializable]
public enum CargoConsoleUiKey : byte
{
@@ -17,8 +64,6 @@ public enum CargoPalletConsoleUiKey : byte
Sale
}
public abstract class SharedCargoSystem : EntitySystem {}
[Serializable, NetSerializable]
public enum CargoTelepadState : byte
{