diff --git a/Content.Server/Animals/Components/EggLayerComponent.cs b/Content.Server/Animals/Components/EggLayerComponent.cs new file mode 100644 index 0000000000..84faa0ef27 --- /dev/null +++ b/Content.Server/Animals/Components/EggLayerComponent.cs @@ -0,0 +1,55 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Sound; +using Content.Shared.Storage; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Server.Animals.Components; + +/// +/// This component handles animals which lay eggs (or some other item) on a timer, using up hunger to do so. +/// It also grants an action to players who are controlling these entities, allowing them to do it manually. +/// +[RegisterComponent] +public sealed class EggLayerComponent : Component +{ + [DataField("eggLayAction", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string EggLayAction = "AnimalLayEgg"; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("hungerUsage")] + public float HungerUsage = 60f; + + /// + /// Minimum cooldown used for the automatic egg laying. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("eggLayCooldownMin")] + public float EggLayCooldownMin = 60f; + + /// + /// Maximum cooldown used for the automatic egg laying. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("eggLayCooldownMax")] + public float EggLayCooldownMax = 120f; + + /// + /// Set during component init. + /// + [ViewVariables(VVAccess.ReadWrite)] + public float CurrentEggLayCooldown; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("eggSpawn", required: true)] + public List EggSpawn = default!; + + [DataField("eggLaySound")] + public SoundSpecifier EggLaySound = new SoundPathSpecifier("/Audio/Effects/pop.ogg"); + + [DataField("accumulatedFrametime")] + public float AccumulatedFrametime; +} + +public sealed class EggLayInstantActionEvent : InstantActionEvent {} diff --git a/Content.Server/Animals/Systems/EggLayerSystem.cs b/Content.Server/Animals/Systems/EggLayerSystem.cs new file mode 100644 index 0000000000..32fdb0bc24 --- /dev/null +++ b/Content.Server/Animals/Systems/EggLayerSystem.cs @@ -0,0 +1,95 @@ +using Content.Server.Actions; +using Content.Server.Animals.Components; +using Content.Server.Nutrition.Components; +using Content.Server.Popups; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Storage; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Server.Animals.Systems; + +public sealed class EggLayerSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnEggLayAction); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var eggLayer in EntityQuery()) + { + // Players should be using the action. + if (HasComp(eggLayer.Owner)) + return; + + eggLayer.AccumulatedFrametime += frameTime; + + if (eggLayer.AccumulatedFrametime < eggLayer.CurrentEggLayCooldown) + continue; + + eggLayer.AccumulatedFrametime -= eggLayer.CurrentEggLayCooldown; + eggLayer.CurrentEggLayCooldown = _random.NextFloat(eggLayer.EggLayCooldownMin, eggLayer.EggLayCooldownMax); + + TryLayEgg(eggLayer.Owner, eggLayer); + } + } + + private void OnComponentInit(EntityUid uid, EggLayerComponent component, ComponentInit args) + { + if (!_prototype.TryIndex(component.EggLayAction, out var action)) + return; + + _actions.AddAction(uid, new InstantAction(action), uid); + component.CurrentEggLayCooldown = _random.NextFloat(component.EggLayCooldownMin, component.EggLayCooldownMax); + } + + private void OnEggLayAction(EntityUid uid, EggLayerComponent component, EggLayInstantActionEvent args) + { + args.Handled = TryLayEgg(uid, component); + } + + public bool TryLayEgg(EntityUid uid, EggLayerComponent? component) + { + if (!Resolve(uid, ref component)) + return false; + + // Allow infinitely laying eggs if they can't get hungry + if (TryComp(uid, out var hunger)) + { + if (hunger.CurrentHunger < component.HungerUsage) + { + _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-too-hungry"), uid, Filter.Entities(uid)); + return false; + } + + hunger.CurrentHunger -= component.HungerUsage; + } + + foreach (var ent in EntitySpawnCollection.GetSpawns(component.EggSpawn, _random)) + { + Spawn(ent, Transform(uid).Coordinates); + } + + // Sound + popups + SoundSystem.Play(component.EggLaySound.GetSound(), Filter.Pvs(uid), uid, component.EggLaySound.Params); + _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-user"), uid, Filter.Entities(uid)); + _popup.PopupEntity(Loc.GetString("action-popup-lay-egg-others", ("entity", uid)), uid, Filter.PvsExcept(uid)); + + return true; + } +} diff --git a/Content.Shared/Storage/EntitySpawnEntry.cs b/Content.Shared/Storage/EntitySpawnEntry.cs index 9748f8dc30..2efbea010f 100644 --- a/Content.Shared/Storage/EntitySpawnEntry.cs +++ b/Content.Shared/Storage/EntitySpawnEntry.cs @@ -12,12 +12,14 @@ namespace Content.Shared.Storage; [DataDefinition] public struct EntitySpawnEntry : IPopulateDefaultValues { + [ViewVariables(VVAccess.ReadWrite)] [DataField("id", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))] public string PrototypeId; /// /// The probability that an item will spawn. Takes decimal form so 0.05 is 5%, 0.50 is 50% etc. /// + [ViewVariables(VVAccess.ReadWrite)] [DataField("prob")] public float SpawnProbability; /// @@ -41,8 +43,10 @@ public struct EntitySpawnEntry : IPopulateDefaultValues /// /// /// + [ViewVariables(VVAccess.ReadWrite)] [DataField("orGroup")] public string? GroupId; + [ViewVariables(VVAccess.ReadWrite)] [DataField("amount")] public int Amount; /// @@ -50,6 +54,7 @@ public struct EntitySpawnEntry : IPopulateDefaultValues /// If this is lesser or equal to , it will spawn exactly. /// Otherwise, it chooses a random value between and on spawn. /// + [ViewVariables(VVAccess.ReadWrite)] [DataField("maxAmount")] public int MaxAmount; public void PopulateDefaultValues() diff --git a/Resources/Audio/Effects/licenses.txt b/Resources/Audio/Effects/licenses.txt index ee7381179e..18145786fc 100644 --- a/Resources/Audio/Effects/licenses.txt +++ b/Resources/Audio/Effects/licenses.txt @@ -37,3 +37,5 @@ The following sounds are taken from TGstation github (licensed under CC by 3.0): demon_consume.ogg: taken at https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0 demon_dies.ogg: taken at https://github.com/tgstation/tgstation/commit/d4f678a1772007ff8d7eddd21cf7218c8e07bfc0 + +pop.ogg licensed under CC0 1.0 by mirrorcult \ No newline at end of file diff --git a/Resources/Audio/Effects/pop.ogg b/Resources/Audio/Effects/pop.ogg new file mode 100644 index 0000000000..97b7468e19 Binary files /dev/null and b/Resources/Audio/Effects/pop.ogg differ diff --git a/Resources/Locale/en-US/accent/accents.ftl b/Resources/Locale/en-US/accent/accents.ftl index 603505e780..54f4be86a5 100644 --- a/Resources/Locale/en-US/accent/accents.ftl +++ b/Resources/Locale/en-US/accent/accents.ftl @@ -46,3 +46,15 @@ accent-words-generic-aggressive-1 = Grr! accent-words-generic-aggressive-2 = Rrrr! accent-words-generic-aggressive-3 = Grr... accent-words-generic-aggressive-4 = Grrow!! + +# Duck +accent-words-duck-1 = Quack! +accent-words-duck-2 = Quack. +accent-words-duck-3 = Quack? +accent-words-duck-4 = Quack quack! + +# Chicken +accent-words-chicken-1 = Cluck! +accent-words-chicken-2 = Cluck. +accent-words-chicken-3 = Cluck? +accent-words-chicken-4 = Cluck cluck! diff --git a/Resources/Locale/en-US/actions/actions/egg-lay.ftl b/Resources/Locale/en-US/actions/actions/egg-lay.ftl new file mode 100644 index 0000000000..404eeee7d0 --- /dev/null +++ b/Resources/Locale/en-US/actions/actions/egg-lay.ftl @@ -0,0 +1,6 @@ +action-name-lay-egg = Lay egg +action-description-lay-egg = Uses hunger to lay an egg. + +action-popup-lay-egg-user = You lay an egg. +action-popup-lay-egg-others = {CAPITALIZE(THE($entity))} lays an egg. +action-popup-lay-egg-too-hungry = You need more food before you can lay another egg! diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index 29a85ce3e3..d30a32993c 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -69,3 +69,11 @@ icon: Objects/Weapons/Melee/shields.rsi/teleriot-icon.png iconOn: Objects/Weapons/Melee/shields.rsi/teleriot-on.png event: !type:ToggleActionEvent + +- type: instantAction + id: AnimalLayEgg + name: action-name-lay-egg + description: action-description-lay-egg + icon: Objects/Consumable/Food/egg.rsi/icon.png + useDelay: 60 + serverEvent: !type:EggLayInstantActionEvent diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 0b7cb19938..2aa24672df 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -193,6 +193,13 @@ path: /Audio/Animals/chicken_cluck_happy.ogg - type: Bloodstream bloodMaxVolume: 100 + - type: EggLayer + eggSpawn: + - id: FoodEgg + - type: ReplacementAccent + accent: chicken + - type: SentienceTarget + flavorKind: organic - type: entity name: mallard duck #Quack @@ -237,6 +244,13 @@ path: /Audio/Animals/duck_quack_happy.ogg - type: Bloodstream bloodMaxVolume: 100 + - type: EggLayer + eggSpawn: + - id: FoodEgg + - type: ReplacementAccent + accent: duck + - type: SentienceTarget + flavorKind: organic - type: entity name: white duck #Quack diff --git a/Resources/Prototypes/accents.yml b/Resources/Prototypes/accents.yml index e728a6fc82..ae32d9371f 100644 --- a/Resources/Prototypes/accents.yml +++ b/Resources/Prototypes/accents.yml @@ -62,3 +62,19 @@ - accent-words-generic-aggressive-2 - accent-words-generic-aggressive-3 - accent-words-generic-aggressive-4 + +- type: accent + id: duck + words: + - accent-words-duck-1 + - accent-words-duck-2 + - accent-words-duck-3 + - accent-words-duck-4 + +- type: accent + id: chicken + words: + - accent-words-chicken-1 + - accent-words-chicken-2 + - accent-words-chicken-3 + - accent-words-chicken-4