6
2026-01-24 12:49:55 +03:00

482 lines
19 KiB
C#

using System.Linq;
using Content.Shared._EstacaoPirata.Cards.Card;
using Content.Shared._EstacaoPirata.Cards.Deck;
using Content.Shared._EstacaoPirata.Cards.Hand;
using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Storage.EntitySystems;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Shared._EstacaoPirata.Cards.Stack;
/// <summary>
/// This handles stack of cards.
/// It is used to shuffle, flip, insert, remove, and join stacks of cards.
/// It also handles the events related to the stack of cards.
/// </summary>
public sealed class CardStackSystem : EntitySystem
{
public const string ContainerId = "cardstack-container";
public const int MaxCardsInStack = 212; // Frontier: four 53-card decks.
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly EntityManager _entityManager = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly CardHandSystem _cardHandSystem = default!; // Frontier
[Dependency] private readonly SharedHandsSystem _hands = default!;
/// <inheritdoc/>
public override void Initialize()
{
// Pretty much a rip-off of the BinSystem
SubscribeLocalEvent<CardStackComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<CardStackComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<CardStackComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<CardStackComponent, GetVerbsEvent<AlternativeVerb>>(OnAlternativeVerb);
SubscribeLocalEvent<CardStackComponent, GetVerbsEvent<ActivationVerb>>(OnActivationVerb);
SubscribeLocalEvent<CardStackComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<CardStackComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<InteractUsingEvent>(OnInteractUsing);
}
public bool TryRemoveCard(EntityUid uid, EntityUid card, CardStackComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return false;
if (!TryComp(card, out CardComponent? _))
return false;
_container.Remove(card, comp.ItemContainer);
comp.Cards.Remove(card);
// If there is a final card left over, remove that card from the container and delete the stack alltogether
if (comp.Cards.Count == 1)
{
_container.Remove(comp.Cards.First(), comp.ItemContainer);
comp.Cards.Clear();
}
Dirty(uid, comp);
RaiseLocalEvent(uid, new CardStackQuantityChangeEvent(GetNetEntity(uid), GetNetEntity(card), StackQuantityChangeType.Removed));
RaiseNetworkEvent(new CardStackQuantityChangeEvent(GetNetEntity(uid), GetNetEntity(card), StackQuantityChangeType.Removed));
// Prevents prediction ruining things
if (_net.IsServer && comp.Cards.Count <= 0)
{
_entityManager.DeleteEntity(uid);
}
return true;
}
public bool TryInsertCard(EntityUid uid, EntityUid card, CardStackComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return false;
if (!TryComp(card, out CardComponent? _))
return false;
if (comp.Cards.Count >= MaxCardsInStack)
return false;
_container.Insert(card, comp.ItemContainer);
comp.Cards.Add(card);
Dirty(uid, comp);
RaiseLocalEvent(uid, new CardStackQuantityChangeEvent(GetNetEntity(uid), GetNetEntity(card), StackQuantityChangeType.Added));
RaiseNetworkEvent(new CardStackQuantityChangeEvent(GetNetEntity(uid), GetNetEntity(card), StackQuantityChangeType.Added));
return true;
}
public bool ShuffleCards(EntityUid uid, CardStackComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return false;
_random.Shuffle(comp.Cards);
Dirty(uid, comp);
RaiseLocalEvent(uid, new CardStackReorderedEvent(GetNetEntity(uid)));
RaiseNetworkEvent(new CardStackReorderedEvent(GetNetEntity(uid)));
return true;
}
/// <summary>
/// Server-Side only method to flip all cards within a stack. This starts CardFlipUpdatedEvent and CardStackFlippedEvent event
/// </summary>
/// <param name="uid"></param>
/// <param name="comp"></param>
/// <param name="isFlipped">If null, all cards will just invert direction, if it contains a value, then all cards will receive that value</param>
/// <returns></returns>
public bool FlipAllCards(EntityUid uid, CardStackComponent? comp = null, bool? isFlipped = null)
{
if (_net.IsClient)
return false;
if (!Resolve(uid, ref comp))
return false;
foreach (var card in comp.Cards)
{
if (!TryComp(card, out CardComponent? cardComponent))
continue;
cardComponent.Flipped = isFlipped ?? !cardComponent.Flipped;
Dirty(card, cardComponent);
RaiseNetworkEvent(new CardFlipUpdatedEvent(GetNetEntity(card)));
}
RaiseNetworkEvent(new CardStackFlippedEvent(GetNetEntity(uid)));
return true;
}
public bool TryJoinStacks(EntityUid firstStack, EntityUid secondStack, CardStackComponent? firstComp = null, CardStackComponent? secondComp = null, EntityUid? soundUser = null)
{
if (firstStack == secondStack)
return false;
if (!Resolve(firstStack, ref firstComp) || !Resolve(secondStack, ref secondComp))
return false;
bool changed = false;
var cardList = secondComp.Cards.ToList();
EntityUid? firstCard = secondComp.Cards.Count > 0 ? cardList[0] : null; // Cache the first card transferred for animations (better to have something moving than nothing, and we destroy the other stack)
foreach (var card in cardList)
{
if (firstComp.Cards.Count >= MaxCardsInStack)
break;
_container.Remove(card, secondComp.ItemContainer);
secondComp.Cards.Remove(card);
firstComp.Cards.Add(card);
_container.Insert(card, firstComp.ItemContainer);
changed = true;
}
if (changed)
{
if (soundUser != null)
{
_audio.PlayPredicted(firstComp.PlaceDownSound, Transform(firstStack).Coordinates, soundUser.Value);
if (_net.IsServer)
_storage.PlayPickupAnimation(firstCard!.Value, Transform(secondStack).Coordinates, Transform(firstStack).Coordinates, 0);
}
if (_net.IsClient)
return changed;
Dirty(firstStack, firstComp);
if (secondComp.Cards.Count <= 0)
{
_entityManager.DeleteEntity(secondStack);
}
else
{
Dirty(secondStack, secondComp);
RaiseLocalEvent(secondStack, new CardStackQuantityChangeEvent(GetNetEntity(secondStack), null, StackQuantityChangeType.Split));
RaiseNetworkEvent(new CardStackQuantityChangeEvent(GetNetEntity(secondStack), null, StackQuantityChangeType.Split));
}
RaiseLocalEvent(firstStack, new CardStackQuantityChangeEvent(GetNetEntity(firstStack), null, StackQuantityChangeType.Joined));
RaiseNetworkEvent(new CardStackQuantityChangeEvent(GetNetEntity(firstStack), null, StackQuantityChangeType.Joined));
}
return changed;
}
#region EventHandling
private void OnStartup(EntityUid uid, CardStackComponent component, ComponentStartup args)
{
component.ItemContainer = _container.EnsureContainer<Container>(uid, ContainerId);
}
private void OnMapInit(EntityUid uid, CardStackComponent comp, MapInitEvent args)
{
if (_net.IsClient)
return;
var coordinates = Transform(uid).Coordinates;
var spawnedEntities = new List<EntityUid>();
foreach (var id in comp.InitialContent)
{
var ent = Spawn(id, coordinates);
spawnedEntities.Add(ent);
if (TryInsertCard(uid, ent, comp))
continue;
Log.Error($"Entity {ToPrettyString(ent)} was unable to be initialized into stack {ToPrettyString(uid)}");
foreach (var spawned in spawnedEntities)
_entityManager.DeleteEntity(spawned);
return;
}
RaiseNetworkEvent(new CardStackInitiatedEvent(GetNetEntity(uid)));
}
// It seems the cards don't get removed if this event is not subscribed... strange right? thanks again bin system
private void OnEntRemoved(EntityUid uid, CardStackComponent component, EntRemovedFromContainerMessage args)
{
component.Cards.Remove(args.Entity);
}
private void OnExamine(EntityUid uid, CardStackComponent component, ExaminedEvent args)
{
args.PushText(Loc.GetString("card-stack-examine", ("count", component.Cards.Count)));
}
private void OnAlternativeVerb(EntityUid uid, CardStackComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (args.Using == args.Target)
return;
if (!TryComp(args.Target, out CardStackComponent? targetStack))
return;
if (TryComp(args.Using, out CardStackComponent? usingStack))
{
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("card-verb-join"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
Priority = 8,
Act = () => JoinStacks(args.User, args.Target, targetStack, (EntityUid)args.Using, usingStack)
});
}
else if (TryComp(args.Using, out CardComponent? usingCard)) // Frontier: single card interaction
{
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("card-verb-join"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/refresh.svg.192dpi.png")),
Priority = 8,
Act = () => InsertCardOnStack(args.User, args.Target, targetStack, (EntityUid)args.Using)
});
} // End Frontier: single card interaction
}
// Frontier: hacky misuse of the activation verb, but allows us a separate way to draw cards without needing additional buttons and event fiddling
private void OnActivationVerb(EntityUid uid, CardStackComponent component, GetVerbsEvent<ActivationVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Hands == null)
return;
if (args.Using == args.Target)
return;
if (args.Using == null)
{
args.Verbs.Add(new ActivationVerb()
{
Act = () => OnInteractHand(args.Target, component, args.User),
Text = Loc.GetString("cards-verb-draw"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
Priority = 16
});
}
else if (TryComp<CardStackComponent>(args.Using, out var cardStack))
{
args.Verbs.Add(new ActivationVerb()
{
Act = () => TransferNLastCardFromStacks(args.User, 1, args.Target, component, args.Using.Value, cardStack),
Text = Loc.GetString("cards-verb-draw"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
Priority = 16
});
}
else if (TryComp<CardComponent>(args.Using, out var card))
{
args.Verbs.Add(new ActivationVerb()
{
Act = () => _cardHandSystem.TrySetupHandFromStack(args.User, args.Using.Value, card, args.Target, component, true),
Text = Loc.GetString("cards-verb-draw"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
Priority = 16
});
}
}
// End Frontier
private void JoinStacks(EntityUid user, EntityUid first, CardStackComponent firstComp, EntityUid second, CardStackComponent secondComp)
{
TryJoinStacks(first, second, firstComp, secondComp, user);
}
public void InsertCardOnStack(EntityUid user, EntityUid stack, CardStackComponent stackComponent, EntityUid card)
{
if (!TryInsertCard(stack, card))
return;
_audio.PlayPredicted(stackComponent.PlaceDownSound, Transform(stack).Coordinates, user);
if (_net.IsClient)
return;
_storage.PlayPickupAnimation(card, Transform(user).Coordinates, Transform(stack).Coordinates, 0);
}
/// <summary>
/// This takes the last card from the first stack and inserts it into the second stack
/// </summary>
public void TransferNLastCardFromStacks(EntityUid user, int n, EntityUid first, CardStackComponent firstComp, EntityUid second, CardStackComponent secondComp)
{
if (firstComp.Cards.Count <= 0)
return;
var cards = firstComp.Cards.TakeLast(n).ToList(); // Frontier: make a copy we don't munge during iteration
var firstCard = cards.First(); // Cache first card for animation - enumerable changes in foreach
bool changed = false;
foreach (var card in cards)
{
if (secondComp.Cards.Count >= MaxCardsInStack)
break;
_container.Remove(card, firstComp.ItemContainer);
firstComp.Cards.Remove(card);
secondComp.Cards.Add(card);
_container.Insert(card, secondComp.ItemContainer);
changed = true;
}
if (changed)
{
_audio.PlayPredicted(firstComp.PlaceDownSound, Transform(second).Coordinates, user);
if (_net.IsClient)
return;
_storage.PlayPickupAnimation(firstCard, Transform(first).Coordinates, Transform(second).Coordinates, 0);
Dirty(second, secondComp);
if (firstComp.Cards.Count == 1)
{
var card = firstComp.Cards.First();
_container.Remove(card, firstComp.ItemContainer);
if (_hands.IsHolding(user, first))
{
_hands.TryDrop(user, first);
_hands.TryPickupAnyHand(user, card);
}
firstComp.Cards.Clear();
}
if (firstComp.Cards.Count <= 0)
{
_entityManager.DeleteEntity(first);
}
else
{
Dirty(first, firstComp);
RaiseLocalEvent(first, new CardStackQuantityChangeEvent(GetNetEntity(first), null, StackQuantityChangeType.Removed));
RaiseNetworkEvent(new CardStackQuantityChangeEvent(GetNetEntity(first), null, StackQuantityChangeType.Removed));
}
RaiseLocalEvent(second, new CardStackQuantityChangeEvent(GetNetEntity(second), null, StackQuantityChangeType.Added));
RaiseNetworkEvent(new CardStackQuantityChangeEvent(GetNetEntity(second), null, StackQuantityChangeType.Added));
}
}
private void OnInteractUsing(InteractUsingEvent args)
{
if (args.Handled)
return;
if (args.Target == args.Used)
return;
// This checks if the user is using an item with Stack component
if (TryComp(args.Used, out CardStackComponent? usedStack))
{
// If the target is a card, then it will insert the card into the stack
if (TryComp(args.Target, out CardComponent? _))
{
InsertCardOnStack(args.User, args.Used, usedStack, args.Target);
args.Handled = true;
return;
}
// If instead, the target is a stack, then it will join the two stacks
if (!TryComp(args.Target, out CardStackComponent? targetStack))
return;
TransferNLastCardFromStacks(args.User, 1, args.Target, targetStack, args.Used, usedStack);
args.Handled = true;
}
// This handles the reverse case, where the user is using a card and inserting it to a stack
else if (TryComp(args.Target, out CardStackComponent? stack))
{
//InsertCardOnStack(args.User, args.Target, stack, args.Used); // Frontier: old version
if (TryComp(args.Used, out CardComponent? card))
{
_cardHandSystem.TrySetupHandFromStack(args.User, args.Used, card, args.Target, stack, true);
args.Handled = true;
}
}
}
private void OnInteractHand(EntityUid uid, CardStackComponent component, EntityUid user)
{
var pickup = _hands.IsHolding(user, uid);
if (component.Cards.Count <= 0)
return;
if (!component.Cards.TryGetValue(component.Cards.Count - 1, out var card))
return;
if (!component.Cards.TryGetValue(component.Cards.Count - 2, out var under))
return;
if (!TryRemoveCard(uid, card, component))
return;
_hands.TryPickupAnyHand(user, card);
if (!Exists(uid) && pickup)
_hands.TryPickupAnyHand(user, under);
if (TryComp<CardDeckComponent>(uid, out var deck))
_audio.PlayPredicted(deck.PickUpSound, Transform(card).Coordinates, user);
else
_audio.PlayPredicted(component.PickUpSound, Transform(card).Coordinates, user);
}
private void OnActivate(EntityUid uid, CardStackComponent component, ActivateInWorldEvent args)
{
if (!args.Complex || args.Handled)
return;
if (!TryComp<HandsComponent>(args.User, out var hands))
{
args.Handled = true;
return;
}
var activeItem = _hands.GetActiveItem((args.User, hands));
if (activeItem == null)
{
// Runs if active item is nothing
// behavior is to draw one card from this target onto active hand as a standalone card
OnInteractHand(args.Target, component, args.User);
}
else if (activeItem == args.Target)
{
// Added from a Frontier PR. Don't want to draw a card from a stack onto itself.
args.Handled = true;
return;
}
else if (TryComp<CardStackComponent>(activeItem, out var cardStack))
{
// If the active item contains a card stack, behavior is to draw from Target and place onto activeHand.
TransferNLastCardFromStacks(args.User, 1, args.Target, component, activeItem.Value, cardStack);
}
else if (TryComp<CardComponent>(activeItem, out var card))
{
_cardHandSystem.TrySetupHandFromStack(args.User, activeItem.Value, card, args.Target, component, true);
}
args.Handled = true;
}
#endregion
}