diff --git a/Content.Server/ADT/Abilities/Felinid/CoughingUpHairballComponent.cs b/Content.Server/ADT/Abilities/Felinid/CoughingUpHairballComponent.cs new file mode 100644 index 00000000000..9e97f837deb --- /dev/null +++ b/Content.Server/ADT/Abilities/Felinid/CoughingUpHairballComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Server.ADT.Abilities.Felinid; + +[RegisterComponent] +public sealed partial class CoughingUpHairballComponent : Component +{ + [DataField("accumulator")] + public float Accumulator = 0f; + + [DataField("coughUpTime")] + public TimeSpan CoughUpTime = TimeSpan.FromSeconds(2.15); // length of hairball.ogg +} diff --git a/Content.Server/ADT/Abilities/Felinid/FelinidComponent.cs b/Content.Server/ADT/Abilities/Felinid/FelinidComponent.cs new file mode 100644 index 00000000000..c953034595d --- /dev/null +++ b/Content.Server/ADT/Abilities/Felinid/FelinidComponent.cs @@ -0,0 +1,20 @@ +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Prototypes; + +namespace Content.Server.ADT.Abilities.Felinid; + +[RegisterComponent] +public sealed partial class FelinidComponent : Component +{ + /// + /// The hairball prototype to use. + /// + [DataField("hairballPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string HairballPrototype = "Hairball"; + + public EntityUid? HairballAction = null; + + public EntityUid? EatMouse = null; + + public EntityUid? PotentialTarget = null; +} diff --git a/Content.Server/ADT/Abilities/Felinid/FelinidFoodComponent.cs b/Content.Server/ADT/Abilities/Felinid/FelinidFoodComponent.cs new file mode 100644 index 00000000000..3bb3fff9317 --- /dev/null +++ b/Content.Server/ADT/Abilities/Felinid/FelinidFoodComponent.cs @@ -0,0 +1,5 @@ +namespace Content.Server.ADT.Abilities.Felinid; + +[RegisterComponent] +public sealed partial class FelinidFoodComponent : Component +{} diff --git a/Content.Server/ADT/Abilities/Felinid/FelinidSystem.cs b/Content.Server/ADT/Abilities/Felinid/FelinidSystem.cs new file mode 100644 index 00000000000..bc8469faac7 --- /dev/null +++ b/Content.Server/ADT/Abilities/Felinid/FelinidSystem.cs @@ -0,0 +1,197 @@ +using Content.Server.Actions; +using Content.Shared.Actions; +using Content.Shared.StatusEffect; +using Content.Shared.Throwing; +using Content.Shared.Item; +using Content.Shared.Inventory; +using Content.Shared.Hands; +using Content.Shared.IdentityManagement; +using Content.Shared.Nutrition.Components; +using Content.Shared.Nutrition.EntitySystems; +using Content.Server.Body.Components; +using Content.Server.Medical; +using Content.Server.Nutrition.EntitySystems; +using Content.Server.Popups; +using Content.Shared.ADT.Psionics.Events; +using Content.Shared.Chemistry.EntitySystems; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Random; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; +using Content.Server.Clothing; + +namespace Content.Server.ADT.Abilities.Felinid; + +public sealed class FelinidSystem : EntitySystem +{ + + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly HungerSystem _hungerSystem = default!; + [Dependency] private readonly VomitSystem _vomitSystem = default!; + [Dependency] private readonly SolutionContainerSystem _solutionSystem = default!; + [Dependency] private readonly IRobustRandom _robustRandom = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnHairball); + SubscribeLocalEvent(OnEatMouse); + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnUnequipped); + SubscribeLocalEvent(OnHairballHit); + SubscribeLocalEvent(OnHairballPickupAttempt); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var entityUid, out var hairballComp, out var catComp)) + { + hairballComp.Accumulator += frameTime; + if (hairballComp.Accumulator < hairballComp.CoughUpTime.TotalSeconds) + continue; + + hairballComp.Accumulator = 0; + SpawnHairball(entityUid, catComp); + RemCompDeferred(entityUid); + } + } + + [ValidatePrototypeId] private const string ActionHairball = "ActionHairball"; + [ValidatePrototypeId] private const string ActionEatMouse = "ActionEatMouse"; + + private void OnInit(EntityUid uid, FelinidComponent component, ComponentInit args) + { + _actions.AddAction(uid, ref component.HairballAction, ActionHairball); + + if (_actions.TryGetActionData(component.HairballAction, out var action) && action?.UseDelay != null) + { + _actions.SetCooldown(component.HairballAction, + _gameTiming.CurTime, _gameTiming.CurTime + (TimeSpan) action?.UseDelay!); + } + } + + private void OnEquipped(EntityUid uid, FelinidComponent component, DidEquipHandEvent args) + { + if (!HasComp(args.Equipped)) + return; + + component.PotentialTarget = args.Equipped; + + _actions.AddAction(uid, ref component.EatMouse, ActionEatMouse); + } + + private void OnUnequipped(EntityUid uid, FelinidComponent component, DidUnequipHandEvent args) + { + if (args.Unequipped == component.PotentialTarget) + { + component.PotentialTarget = null; + + _actions.RemoveAction(uid, component.EatMouse); + } + } + + private static readonly SoundSpecifier HairballPlay = new SoundPathSpecifier("/Audio/ADT/Felinid/hairball.ogg", + AudioParams.Default.WithVariation(0.15f)); + + private void OnHairball(EntityUid uid, FelinidComponent component, HairballActionEvent args) + { + //if (_inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) && + // EntityManager.TryGetComponent(maskUid, out var blocker) && + // blocker.Enabled) + //{ + // _popupSystem.PopupEntity(Loc.GetString("hairball-mask", ("mask", maskUid)), uid, uid); + // return; + //} + + _popupSystem.PopupEntity(Loc.GetString("hairball-cough", ("name", Identity.Entity(uid, EntityManager))), uid); + _audio.PlayPredicted(HairballPlay, uid, null); + + EnsureComp(uid); + args.Handled = true; + } + + private static readonly SoundSpecifier EatMousePlay = new SoundPathSpecifier("/Audio/Items/eatfood.ogg", + AudioParams.Default.WithVariation(0.15f)); + + private void OnEatMouse(EntityUid uid, FelinidComponent component, EatMouseActionEvent args) + { + if (component.PotentialTarget == null) + return; + + if (!TryComp(uid, out var hunger)) + return; + + if (hunger.CurrentThreshold == Shared.Nutrition.Components.HungerThreshold.Overfed) + { + _popupSystem.PopupEntity(Loc.GetString("food-system-you-cannot-eat-any-more"), uid, uid, Shared.Popups.PopupType.SmallCaution); + return; + } + + //if (_inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) && + // EntityManager.TryGetComponent(maskUid, out var blocker) && + // blocker.Enabled) + //{ + // _popupSystem.PopupEntity(Loc.GetString("hairball-mask", ("mask", maskUid)), uid, uid, Shared.Popups.PopupType.SmallCaution); + // return; + //} + + if (component.HairballAction != null && _actions.TryGetActionData(component.HairballAction, out var skill)) + { + _actionsSystem.SetCharges(component.HairballAction, skill?.Charges + 1); + _actionsSystem.SetEnabled(component.HairballAction, true); + } + Del(component.PotentialTarget.Value); + component.PotentialTarget = null; + + _audio.PlayPredicted(EatMousePlay, uid, null); + + _hungerSystem.ModifyHunger(uid, 70f, hunger); + + _actions.RemoveAction(uid, component.EatMouse); + } + + private void SpawnHairball(EntityUid uid, FelinidComponent component) + { + var hairball = EntityManager.SpawnEntity(component.HairballPrototype, Transform(uid).Coordinates); + var hairballComp = Comp(hairball); + + if (TryComp(uid, out var bloodstream)) + { + var temp = bloodstream.ChemicalSolution.SplitSolution(20); + + if (_solutionSystem.TryGetSolution(hairball, hairballComp.SolutionName, out var hairballSolution)) + { + _solutionSystem.TryAddSolution(hairball, hairballSolution, temp); + } + } + } + private void OnHairballHit(EntityUid uid, HairballComponent component, ThrowDoHitEvent args) + { + if (HasComp(args.Target) || !HasComp(args.Target)) + return; + if (_robustRandom.Prob(0.2f)) + _vomitSystem.Vomit(args.Target); + } + + private void OnHairballPickupAttempt(EntityUid uid, HairballComponent component, GettingPickedUpAttemptEvent args) + { + if (HasComp(args.User) || !HasComp(args.User)) + return; + + if (_robustRandom.Prob(0.2f)) + { + _vomitSystem.Vomit(args.User); + args.Cancel(); + } + } +} diff --git a/Content.Server/ADT/Abilities/Felinid/HairballComponent.cs b/Content.Server/ADT/Abilities/Felinid/HairballComponent.cs new file mode 100644 index 00000000000..82f02db2ec2 --- /dev/null +++ b/Content.Server/ADT/Abilities/Felinid/HairballComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.ADT.Abilities.Felinid; + +[RegisterComponent] +public sealed partial class HairballComponent : Component +{ + public string SolutionName = "hairball"; +} diff --git a/Content.Server/Clothing/MaskSystem.cs b/Content.Server/Clothing/MaskSystem.cs new file mode 100644 index 00000000000..2154f3b5765 --- /dev/null +++ b/Content.Server/Clothing/MaskSystem.cs @@ -0,0 +1,124 @@ +//using Content.Server.Actions; +//using Content.Server.Atmos.Components; +//using Content.Server.Atmos.EntitySystems; +//using Content.Server.Body.Components; +//using Content.Server.Body.Systems; +//using Content.Server.Clothing.Components; +//using Content.Server.IdentityManagement; +//using Content.Server.Nutrition.EntitySystems; +//using Content.Server.Popups; +//using Content.Server.VoiceMask; +//using Content.Shared.Actions; +//using Content.Shared.Clothing; +//using Content.Shared.Clothing.Components; +//using Content.Shared.Clothing.EntitySystems; +//using Content.Shared.IdentityManagement.Components; +//using Content.Shared.Inventory; +//using Content.Shared.Inventory.Events; + +//namespace Content.Server.Clothing +//{ +// public sealed class MaskSystem : EntitySystem +// { +// [Dependency] private readonly ActionsSystem _actionSystem = default!; +// [Dependency] private readonly AtmosphereSystem _atmos = default!; +// [Dependency] private readonly InternalsSystem _internals = default!; +// [Dependency] private readonly InventorySystem _inventorySystem = default!; +// [Dependency] private readonly PopupSystem _popupSystem = default!; +// [Dependency] private readonly IdentitySystem _identity = default!; +// [Dependency] private readonly ClothingSystem _clothing = default!; + +// public override void Initialize() +// { +// base.Initialize(); + +// SubscribeLocalEvent(OnToggleMask); +// SubscribeLocalEvent(OnGetActions); +// SubscribeLocalEvent(OnGotUnequipped); +// } + +// private void OnGetActions(EntityUid uid, MaskComponent component, GetItemActionsEvent args) +// { +// if (!args.InHands) +// args.AddAction(ref component.ToggleActionEntity, component.ToggleAction); +// } + +// private void OnToggleMask(Entity ent, ref ToggleMaskEvent args) +// { +// var (uid, mask) = ent; +// if (mask.ToggleActionEntity == null) +// return; + +// if (!_inventorySystem.TryGetSlotEntity(args.Performer, "mask", out var existing) || !uid.Equals(existing)) +// return; + +// //mask.IsToggled ^= true; +// //_actionSystem.SetToggled(mask.ToggleActionEntity, mask.IsToggled); + +// // Pulling mask down can change identity, so we want to update that +// _identity.QueueIdentityUpdate(args.Performer); + +// if (mask.IsToggled) +// _popupSystem.PopupEntity(Loc.GetString("action-mask-pull-down-popup-message", ("mask", uid)), args.Performer, args.Performer); +// else +// _popupSystem.PopupEntity(Loc.GetString("action-mask-pull-up-popup-message", ("mask", uid)), args.Performer, args.Performer); + +// ToggleMaskComponents(uid, mask, args.Performer); +// } + +// // set to untoggled when unequipped, so it isn't left in a 'pulled down' state +// private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args) +// { +// if (mask.ToggleActionEntity == null) +// return; + +// //mask.IsToggled = false; +// //_actionSystem.SetToggled(mask.ToggleActionEntity, mask.IsToggled); + +// ToggleMaskComponents(uid, mask, args.Equipee, true); +// } + +// private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, bool isEquip = false) +// { +// // toggle visuals +// if (TryComp(uid, out var clothing)) +// { +// //TODO: sprites for 'pulled down' state. defaults to invisible due to no sprite with this prefix +// _clothing.SetEquippedPrefix(uid, mask.IsToggled ? "toggled" : null, clothing); +// } + +// // shouldn't this be an event? + +// // toggle ingestion blocking +// if (TryComp(uid, out var blocker)) +// blocker.Enabled = !mask.IsToggled; + +// // toggle identity +// if (TryComp(uid, out var identity)) +// identity.Enabled = !mask.IsToggled; + +// // toggle voice masking +// if (TryComp(wearer, out var voiceMask)) +// voiceMask.Enabled = !mask.IsToggled; + +// // toggle breath tool connection (skip during equip since that is handled in LungSystem) +// if (isEquip || !TryComp(uid, out var breathTool)) +// return; + +// if (mask.IsToggled) +// { +// _atmos.DisconnectInternals(breathTool); +// } +// else +// { +// breathTool.IsFunctional = true; + +// if (TryComp(wearer, out InternalsComponent? internals)) +// { +// breathTool.ConnectedInternalsEntity = wearer; +// _internals.ConnectBreathTool((wearer, internals), uid); +// } +// } +// } +// } +//} diff --git a/Content.Shared/ADT/Psionics/Events.cs b/Content.Shared/ADT/Psionics/Events.cs new file mode 100644 index 00000000000..1a475afe4ad --- /dev/null +++ b/Content.Shared/ADT/Psionics/Events.cs @@ -0,0 +1,52 @@ +using Content.Shared.Actions; +using Robust.Shared.Serialization; +using Content.Shared.DoAfter; + +namespace Content.Shared.ADT.Psionics.Events; + +[Serializable, NetSerializable] +public sealed partial class PsionicRegenerationDoAfterEvent : DoAfterEvent +{ + [DataField("startedAt", required: true)] + public TimeSpan StartedAt; + + private PsionicRegenerationDoAfterEvent() + { + } + + public PsionicRegenerationDoAfterEvent(TimeSpan startedAt) + { + StartedAt = startedAt; + } + + public override DoAfterEvent Clone() => this; +} + +[Serializable, NetSerializable] +public sealed partial class GlimmerWispDrainDoAfterEvent : SimpleDoAfterEvent +{ +} + +public sealed partial class PsionicInvisibilityPowerActionEvent : InstantActionEvent {} +public sealed partial class PsionicInvisibilityPowerOffActionEvent : InstantActionEvent {} +public sealed partial class HairballActionEvent : InstantActionEvent {} +public sealed partial class EatMouseActionEvent : InstantActionEvent {} +public sealed partial class MetapsionicPowerActionEvent : InstantActionEvent {} +public sealed partial class TelegnosisPowerReturnActionEvent : InstantActionEvent {} +public sealed partial class TelegnosisPowerActionEvent : InstantActionEvent {} +public sealed partial class PsionicRegenerationPowerActionEvent : InstantActionEvent {} +public sealed partial class NoosphericZapPowerActionEvent : EntityTargetActionEvent {} +public sealed partial class DispelPowerActionEvent : EntityTargetActionEvent {} + +public sealed partial class DispelledEvent : HandledEntityEventArgs {} +public sealed partial class MindSwapPowerActionEvent : EntityTargetActionEvent +{ + +} + +public sealed partial class MindSwapPowerReturnActionEvent : InstantActionEvent +{ + +} +public sealed partial class PyrokinesisPowerActionEvent : EntityTargetActionEvent {} +public sealed partial class PsychokinesisPowerActionEvent : WorldTargetActionEvent {} diff --git a/Content.Shared/ADT/Psionics/PsychokinesisComponent.cs b/Content.Shared/ADT/Psionics/PsychokinesisComponent.cs new file mode 100644 index 00000000000..674a3d28e16 --- /dev/null +++ b/Content.Shared/ADT/Psionics/PsychokinesisComponent.cs @@ -0,0 +1,18 @@ +using Robust.Shared.Audio; + +namespace Content.Shared.ADT.Abilities.Psionics; + +[RegisterComponent] +public sealed partial class PsychokinesisPowerComponent : Component +{ + public EntityUid? PsychokinesisPowerAction = null; + + [DataField("waveSound")] + public SoundSpecifier WaveSound = new SoundPathSpecifier("/Audio/Nyanotrasen/Mobs/SilverGolem/wave.ogg"); + + /// + /// Volume control for the spell. + /// + [DataField("waveVolume")] + public float WaveVolume = 5f; +} diff --git a/Resources/Audio/ADT/Felinid/hairball.ogg b/Resources/Audio/ADT/Felinid/hairball.ogg new file mode 100644 index 00000000000..f7fb40de608 Binary files /dev/null and b/Resources/Audio/ADT/Felinid/hairball.ogg differ diff --git a/Resources/Audio/ADT/Felinid/license.txt b/Resources/Audio/ADT/Felinid/license.txt new file mode 100644 index 00000000000..0370cf25c61 --- /dev/null +++ b/Resources/Audio/ADT/Felinid/license.txt @@ -0,0 +1 @@ +hairball.ogg taken from https://en.wikipedia.org/wiki/File:Common_house_cat_coughing_hairball.ogv CC-BY-SA-3.0 diff --git a/Resources/Locale/ru-RU/ADT/Felinid/abilities/felinid.ftl b/Resources/Locale/ru-RU/ADT/Felinid/abilities/felinid.ftl new file mode 100644 index 00000000000..5a4df0fbd42 --- /dev/null +++ b/Resources/Locale/ru-RU/ADT/Felinid/abilities/felinid.ftl @@ -0,0 +1,8 @@ +hairball-action = Откашляться +hairball-action-desc = Очистите свою пищеварительную систему и получите крутой комок волос, который можно бросить в людей. +#hairball-mask = Снемите свою { $mask } для начала. +#hairball-cough = { CAPITALIZE(THE($name)) } начинает кашлять комком шерсти! +ent-Hairball = комок шерсти + .desc = Фелиниды, чувак... +action-name-eat-mouse = Сожрать мышку +action-description-eat-mouse = Съешьте мышь, которую держите в руке, и получите питательные вещества и заряд шерсти. diff --git a/Resources/Prototypes/ADT/Felinid/Mobs/Species/felinid.yml b/Resources/Prototypes/ADT/Felinid/Mobs/Species/felinid.yml index 734fd1dfc72..ce93ceb6e9e 100644 --- a/Resources/Prototypes/ADT/Felinid/Mobs/Species/felinid.yml +++ b/Resources/Prototypes/ADT/Felinid/Mobs/Species/felinid.yml @@ -5,11 +5,12 @@ id: MobFelinidBase abstract: true components: + - type: Felinid - type: Sprite netsync: false noRot: true drawdepth: Mobs - scale: 0.8, 0.8 + scale: 0.95, 0.95 layers: - map: [ "enum.HumanoidVisualLayers.Chest" ] - map: [ "enum.HumanoidVisualLayers.Head" ] @@ -167,3 +168,52 @@ - map: [ "head" ] - map: [ "pocket1" ] - map: [ "pocket2" ] + + +- type: entity + parent: BaseItem + id: Hairball + name: hairball + description: Felinids, man... Placeholder sprite. + components: + - type: Sprite + sprite: ADT/Objects/Specific/felinid.rsi + state: icon + - type: Hairball + - type: SolutionContainerManager + solutions: + hairball: + maxVol: 25 + reagents: + - ReagentId: Protein + Quantity: 2 + - type: Extractable + grindableSolutionName: hairball + # - type: Tag + # tags: + # - Recyclable + # - Trash + +- type: entity + id: ActionHairball + name: hairball-action + description: hairball-action-desc + noSpawn: true + components: + - type: InstantAction +# icon: { sprite: Backmen/Structures/web.rsi, state: web1 } + priority: -10 + event: !type:HairballActionEvent + charges: 100 #костыль + useDelay: 30 + +- type: entity + id: ActionEatMouse + name: action-name-eat-mouse + description: action-description-eat-mouse + noSpawn: true + components: + - type: InstantAction + useDelay: 10 + icon: ADT/Icons/verbiconfangs.png + event: !type:EatMouseActionEvent diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 16683b56644..88e6451f0ad 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1175,6 +1175,7 @@ id: MobMouse description: Squeak! components: + - type: FelinidFood - type: Body prototype: Mouse - type: GhostRole diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 5e691fb3e23..5f02d2e062b 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -155,6 +155,7 @@ description: He's da mini rat. He don't make da roolz. noSpawn: true #Must be configured to a King or the AI breaks. components: + - type: FelinidFood - type: CombatMode - type: MovementSpeedModifier baseWalkSpeed : 2.5 #Value was 3.5 (Nerf since 20.12.2022) diff --git a/Resources/Textures/ADT/Icons/verbiconfangs.png b/Resources/Textures/ADT/Icons/verbiconfangs.png new file mode 100644 index 00000000000..4511cbd21fd Binary files /dev/null and b/Resources/Textures/ADT/Icons/verbiconfangs.png differ diff --git a/Resources/Textures/ADT/Objects/Specific/felinid.rsi/icon.png b/Resources/Textures/ADT/Objects/Specific/felinid.rsi/icon.png new file mode 100644 index 00000000000..f9483ac80fc Binary files /dev/null and b/Resources/Textures/ADT/Objects/Specific/felinid.rsi/icon.png differ diff --git a/Resources/Textures/ADT/Objects/Specific/felinid.rsi/meta.json b/Resources/Textures/ADT/Objects/Specific/felinid.rsi/meta.json new file mode 100644 index 00000000000..aaad7ca5b98 --- /dev/null +++ b/Resources/Textures/ADT/Objects/Specific/felinid.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Made by QuizzyQuin", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + } + ] +}