diff --git a/Content.Server/Holiday/Christmas/LimitedItemGiverComponent.cs b/Content.Server/Holiday/Christmas/LimitedItemGiverComponent.cs new file mode 100644 index 0000000000..119987c8cf --- /dev/null +++ b/Content.Server/Holiday/Christmas/LimitedItemGiverComponent.cs @@ -0,0 +1,41 @@ +using Content.Shared.Storage; +using Robust.Shared.Network; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Holiday.Christmas; + +/// +/// This is used for granting items to lucky souls, exactly once. +/// +[RegisterComponent, Access(typeof(LimitedItemGiverSystem))] +public sealed class LimitedItemGiverComponent : Component +{ + /// + /// Santa knows who you are behind the screen, only one gift per player per round! + /// + public readonly HashSet GrantedPlayers = new(); + + /// + /// Selects what entities can be given out by the giver. + /// + [DataField("spawnEntries", required: true)] + public List SpawnEntries = default!; + + /// + /// The (localized) message shown upon receiving something. + /// + [DataField("receivedPopup", required: true)] + public string ReceivedPopup = default!; + + /// + /// The (localized) message shown upon being denied. + /// + [DataField("deniedPopup", required: true)] + public string DeniedPopup = default!; + + /// + /// The holiday required for this giver to work, if any. + /// + [DataField("requiredHoliday", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string? RequiredHoliday = null; +} diff --git a/Content.Server/Holiday/Christmas/LimitedItemGiverSystem.cs b/Content.Server/Holiday/Christmas/LimitedItemGiverSystem.cs new file mode 100644 index 0000000000..d0790dd1a6 --- /dev/null +++ b/Content.Server/Holiday/Christmas/LimitedItemGiverSystem.cs @@ -0,0 +1,50 @@ +using Content.Server.Hands.Systems; +using Content.Server.Mind.Components; +using Content.Server.Popups; +using Content.Shared.Interaction; +using Content.Shared.Storage; +using Robust.Server.GameObjects; + +namespace Content.Server.Holiday.Christmas; + +/// +/// This handles handing out items from item givers. +/// +public sealed class LimitedItemGiverSystem : EntitySystem +{ + [Dependency] private readonly HandsSystem _hands = default!; + [Dependency] private readonly HolidaySystem _holiday = default!; + [Dependency] private readonly PopupSystem _popup = default!; + /// + public override void Initialize() + { + SubscribeLocalEvent(OnInteractHand); + } + + private void OnInteractHand(EntityUid uid, LimitedItemGiverComponent component, InteractHandEvent args) + { + if (!TryComp(args.User, out var actor)) + return; + + if (component.GrantedPlayers.Contains(actor.PlayerSession.UserId) || (component.RequiredHoliday is not null && !_holiday.IsCurrentlyHoliday(component.RequiredHoliday))) + { + _popup.PopupEntity(Loc.GetString(component.DeniedPopup), uid, args.User); + return; + } + + var toGive = EntitySpawnCollection.GetSpawns(component.SpawnEntries); + var coords = Transform(args.User).Coordinates; + + foreach (var item in toGive) + { + if (item is null) + continue; + + var spawned = Spawn(item, coords); + _hands.PickupOrDrop(args.User, spawned); + } + + component.GrantedPlayers.Add(actor.PlayerSession.UserId); + _popup.PopupEntity(Loc.GetString(component.ReceivedPopup), uid, args.User); + } +} diff --git a/Content.Server/Holiday/Christmas/RandomGiftComponent.cs b/Content.Server/Holiday/Christmas/RandomGiftComponent.cs new file mode 100644 index 0000000000..e76f1ee9c4 --- /dev/null +++ b/Content.Server/Holiday/Christmas/RandomGiftComponent.cs @@ -0,0 +1,43 @@ +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Holiday.Christmas; + +/// +/// This is used for gifts with COMPLETELY random things. +/// +[RegisterComponent, Access(typeof(RandomGiftSystem))] +public sealed class RandomGiftComponent : Component +{ + /// + /// The wrapper entity to spawn when unwrapping the gift. + /// + [DataField("wrapper", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] + public string? Wrapper; + + /// + /// A sound to play when the items are spawned. For example, gift boxes being unwrapped. + /// + [DataField("sound", required: true)] + public SoundSpecifier? Sound; + + /// + /// Whether or not the gift should be limited only to actual items. + /// + [DataField("insaneMode", required: true), ViewVariables(VVAccess.ReadWrite)] + public bool InsaneMode; + + /// + /// What entities are allowed to examine this gift to see its contents. + /// + [DataField("contentsViewers", required: true)] + public EntityWhitelist ContentsViewers = default!; + + /// + /// The currently selected entity to give out. Used so contents viewers can see inside. + /// + [DataField("selectedEntity"), ViewVariables(VVAccess.ReadWrite)] + public string? SelectedEntity; +} diff --git a/Content.Server/Holiday/Christmas/RandomGiftSystem.cs b/Content.Server/Holiday/Christmas/RandomGiftSystem.cs new file mode 100644 index 0000000000..a830b85721 --- /dev/null +++ b/Content.Server/Holiday/Christmas/RandomGiftSystem.cs @@ -0,0 +1,102 @@ +using Content.Server.Hands.Systems; +using Content.Shared.Examine; +using Content.Shared.Interaction.Events; +using Content.Shared.Item; +using Robust.Server.GameObjects; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Holiday.Christmas; + +/// +/// This handles granting players their gift. +/// +public sealed class RandomGiftSystem : EntitySystem +{ + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly HandsSystem _hands = default!; + [Dependency] private readonly IComponentFactory _componentFactory = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IRobustRandom _random = default!; + + private readonly List _possibleGiftsSafe = new(); + private readonly List _possibleGiftsUnsafe = new(); + + /// + public override void Initialize() + { + _prototype.PrototypesReloaded += OnPrototypesReloaded; + SubscribeLocalEvent(OnGiftMapInit); + SubscribeLocalEvent(OnUseInHand); + SubscribeLocalEvent(OnExamined); + BuildIndex(); + } + + private void OnExamined(EntityUid uid, RandomGiftComponent component, ExaminedEvent args) + { + if (!component.ContentsViewers.IsValid(args.Examiner, EntityManager) || component.SelectedEntity is null) + return; + + var name = _prototype.Index(component.SelectedEntity).Name; + args.Message.PushNewline(); + args.Message.AddText(Loc.GetString("gift-packin-contains", ("name", name))); + } + + private void OnUseInHand(EntityUid uid, RandomGiftComponent component, UseInHandEvent args) + { + if (args.Handled) + return; + + if (component.SelectedEntity is null) + return; + + var coords = Transform(args.User).Coordinates; + var handsEnt = Spawn(component.SelectedEntity, coords); + EnsureComp(handsEnt); // For insane mode. + if (component.Wrapper is not null) + Spawn(component.Wrapper, coords); + + args.Handled = true; + _audio.PlayPvs(component.Sound, args.User); + Del(uid); + _hands.PickupOrDrop(args.User, handsEnt); + + } + + private void OnGiftMapInit(EntityUid uid, RandomGiftComponent component, MapInitEvent args) + { + if (component.InsaneMode) + component.SelectedEntity = _random.Pick(_possibleGiftsUnsafe); + else + component.SelectedEntity = _random.Pick(_possibleGiftsSafe); + } + + private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj) + { + BuildIndex(); + } + + private void BuildIndex() + { + _possibleGiftsSafe.Clear(); + _possibleGiftsUnsafe.Clear(); + var itemCompName = _componentFactory.GetComponentName(typeof(ItemComponent)); + var mapGridCompName = _componentFactory.GetComponentName(typeof(MapGridComponent)); + var physicsCompName = _componentFactory.GetComponentName(typeof(PhysicsComponent)); + + foreach (var proto in _prototype.EnumeratePrototypes()) + { + if (proto.Abstract || proto.NoSpawn || proto.Components.ContainsKey(mapGridCompName) || !proto.Components.ContainsKey(physicsCompName)) + continue; + + _possibleGiftsUnsafe.Add(proto.ID); + + if (!proto.Components.ContainsKey(itemCompName)) + continue; + + _possibleGiftsSafe.Add(proto.ID); + } + } +} diff --git a/Content.Server/Holiday/Christmas/SantaComponent.cs b/Content.Server/Holiday/Christmas/SantaComponent.cs new file mode 100644 index 0000000000..3769803f93 --- /dev/null +++ b/Content.Server/Holiday/Christmas/SantaComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Server.Holiday.Christmas; + +/// +/// This is used as a marker component, allows them to see gift contents. +/// +[RegisterComponent] +public sealed class SantaComponent : Component +{ + +} diff --git a/Resources/Locale/en-US/holiday/gifts.ftl b/Resources/Locale/en-US/holiday/gifts.ftl new file mode 100644 index 0000000000..3374dc61d7 --- /dev/null +++ b/Resources/Locale/en-US/holiday/gifts.ftl @@ -0,0 +1,3 @@ +gift-packin-contains = This present appears to contain {INDEFINITE($name)} {$name}. +christmas-tree-got-gift = After a bit of digging, you find a present with your name on it! +christmas-tree-no-gift = There isn't a gift under the tree for you... diff --git a/Resources/Prototypes/Entities/Objects/Decoration/flora.yml b/Resources/Prototypes/Entities/Objects/Decoration/flora.yml index fd29dcba7f..72e31e3830 100644 --- a/Resources/Prototypes/Entities/Objects/Decoration/flora.yml +++ b/Resources/Prototypes/Entities/Objects/Decoration/flora.yml @@ -334,6 +334,7 @@ - type: entity parent: BaseTreeConifer id: FloraTreeChristmas02 + suffix: PresentsGiver name: christmas tree components: - type: Sprite @@ -346,6 +347,19 @@ density: 4500 layer: - WallLayer + - type: LimitedItemGiver + spawnEntries: + - id: PresentRandom + orGroup: present + - id: PresentRandomUnsafe + prob: 0.5 + orGroup: present + - id: PresentRandomInsane + prob: 0.2 + orGroup: present + receivedPopup: christmas-tree-got-gift + deniedPopup: christmas-tree-no-gift + requiredHoliday: FestiveSeason - type: entity parent: BaseTreeConifer diff --git a/Resources/Prototypes/Entities/Objects/Decoration/present.yml b/Resources/Prototypes/Entities/Objects/Decoration/present.yml index 081732a83b..fb186f7b07 100644 --- a/Resources/Prototypes/Entities/Objects/Decoration/present.yml +++ b/Resources/Prototypes/Entities/Objects/Decoration/present.yml @@ -1,32 +1,53 @@ - type: entity - id: Present - parent: BaseStorageItem + id: PresentBase name: Present - suffix: Empty description: A little box with incredible surprises inside. + abstract: true components: - type: Sprite sprite: Objects/Decoration/present.rsi netsync: false layers: - state: present + +- type: entity + id: Present + parent: [PresentBase, BaseStorageItem] + suffix: Empty + components: - type: Item size: 30 - type: Storage capacity: 30 - type: entity - id: PresentRandom - parent: BaseItem - name: Present - suffix: Filled Random - description: A little box with incredible surprises inside. + id: PresentRandomUnsafe + parent: [PresentBase, BaseItem] + suffix: Filled Unsafe + components: + - type: RandomGift + wrapper: PresentTrash + sound: + path: /Audio/Effects/unwrap.ogg + insaneMode: false + contentsViewers: + components: + - Ghost + - Santa + +- type: entity + id: PresentRandomInsane + parent: PresentRandomUnsafe + suffix: Filled Insane + components: + - type: RandomGift + insaneMode: true + +- type: entity + id: PresentRandom + parent: [PresentBase, BaseItem] + suffix: Filled Safe components: - - type: Sprite - sprite: Objects/Decoration/present.rsi - netsync: false - layers: - - state: present - type: SpawnItemsOnUse items: - id: PresentTrash