diff --git a/Content.Client/LegallyDistinctSpaceFerret/CanBackflipSystem.cs b/Content.Client/LegallyDistinctSpaceFerret/CanBackflipSystem.cs new file mode 100644 index 0000000000..5a56f0cc9b --- /dev/null +++ b/Content.Client/LegallyDistinctSpaceFerret/CanBackflipSystem.cs @@ -0,0 +1,54 @@ +using Content.Shared.LegallyDistinctSpaceFerret; +using Robust.Client.Animations; +using Robust.Client.Audio; +using Robust.Client.GameObjects; +using Robust.Shared.Animations; +using Robust.Shared.Audio; +using Robust.Shared.Player; + +namespace Content.Client.LegallyDistinctSpaceFerret; + +public sealed class CanBackflipSystem : EntitySystem +{ + [Dependency] private readonly AnimationPlayerSystem _animation = default!; + [Dependency] private readonly AudioSystem _audio = default!; + + public const string BackflipKey = "backflip"; + + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(OnBackflipEvent); + } + + public void OnBackflipEvent(DoABackFlipEvent args) + { + if (!TryGetEntity(args.Actioner, out var uid)) + { + return; + } + + _animation.Play(uid.Value, new Animation + { + Length = TimeSpan.FromSeconds(0.66), + AnimationTracks = + { + new AnimationTrackComponentProperty() + { + ComponentType = typeof(SpriteComponent), + Property = nameof(SpriteComponent.Rotation), + InterpolationMode = AnimationInterpolationMode.Linear, + KeyFrames = + { + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), 0f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(180), 0.33f), + new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(360), 0.33f), + } + } + } + }, BackflipKey); + + _audio.PlayEntity(new SoundPathSpecifier(args.SfxSource), Filter.Local(), uid.Value, false); + } +} diff --git a/Content.Client/LegallyDistinctSpaceFerret/CanHibernateSystem.cs b/Content.Client/LegallyDistinctSpaceFerret/CanHibernateSystem.cs new file mode 100644 index 0000000000..e6e89b8db8 --- /dev/null +++ b/Content.Client/LegallyDistinctSpaceFerret/CanHibernateSystem.cs @@ -0,0 +1,31 @@ +using Content.Shared.LegallyDistinctSpaceFerret; +using Robust.Client.Audio; +using Robust.Client.GameObjects; +using Robust.Shared.Audio; + +namespace Content.Client.LegallyDistinctSpaceFerret; + +public sealed class CanHibernateSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeNetworkEvent(OnHibernateEvent); + } + + public void OnHibernateEvent(EntityHasHibernated args) + { + if (!TryGetEntity(args.Hibernator, out var uid)) + { + return; + } + + if (!TryComp(uid, out var comp)) + { + return; + } + + comp.LayerSetState(0, args.SpriteStateId); + } +} diff --git a/Content.Server/Interaction/InteractionPopupSystem.cs b/Content.Server/Interaction/InteractionPopupSystem.cs index 77b76f898a..505e53ea8a 100644 --- a/Content.Server/Interaction/InteractionPopupSystem.cs +++ b/Content.Server/Interaction/InteractionPopupSystem.cs @@ -13,6 +13,11 @@ using Robust.Shared.Timing; namespace Content.Server.Interaction; +public sealed class InteractionAttemptFailed(EntityUid target) +{ + public EntityUid Target = target; +} + public sealed class InteractionPopupSystem : EntitySystem { [Dependency] private readonly IGameTiming _gameTiming = default!; @@ -96,6 +101,8 @@ public sealed class InteractionPopupSystem : EntitySystem if (component.InteractFailureSpawn != null) Spawn(component.InteractFailureSpawn, _transform.GetMapCoordinates(uid)); + + RaiseLocalEvent(target, new InteractionAttemptFailed(target)); } if (component.MessagePerceivedByOthers != null) diff --git a/Content.Server/LegallyDistinctSpaceFerret/BrainrotSystem.cs b/Content.Server/LegallyDistinctSpaceFerret/BrainrotSystem.cs new file mode 100644 index 0000000000..66ada6b44e --- /dev/null +++ b/Content.Server/LegallyDistinctSpaceFerret/BrainrotSystem.cs @@ -0,0 +1,154 @@ +using System.Threading; +using Content.Server.Popups; +using Content.Server.Speech; +using Content.Shared.LegallyDistinctSpaceFerret; +using Content.Shared.Mind; +using Content.Shared.Physics; +using Robust.Shared.Physics.Collision.Shapes; +using Robust.Shared.Physics.Components; +using Robust.Shared.Physics.Events; +using Robust.Shared.Physics.Systems; +using Robust.Shared.Random; +using Timer = Robust.Shared.Timing.Timer; + +namespace Content.Server.LegallyDistinctSpaceFerret; + +/// +/// Raised locally when a mind gets too close to a Brainrot-causer. +/// +/// Who got brainrot +/// Who caused brainrot +/// How long will they have it for +public struct EntityGivenBrainrotEvent(EntityUid target, EntityUid cause, float time) +{ + public EntityUid Cause = cause; + public EntityUid Target = target; + public float Time = time; +} + +/// +/// Raised locally when a mind is freed from brainrot +/// +/// Who had brainrot +public struct EntityLostBrainrotEvent(EntityUid target) +{ + public EntityUid Target = target; +} + +public sealed class BrainrotSystem : EntitySystem +{ + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly FixtureSystem _fixtures = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + + public const string BrainrotFixture = "brainrot"; + public const string BrainRotApplied = "brainrot-applied"; + public const string BrainRotLost = "brainrot-lost"; + public readonly string[] BrainRotReplacementStrings = + [ + "brainrot-replacement-1", + "brainrot-replacement-2", + "brainrot-replacement-3", + "brainrot-replacement-4", + "brainrot-replacement-5", + "brainrot-replacement-6" + ]; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnCollide); + SubscribeLocalEvent(EntityGivenBrainrot); + SubscribeLocalEvent(EntityLostBrainrot); + SubscribeLocalEvent(ApplyBrainRot); + } + + private void OnStartup(EntityUid uid, BrainrotAuraComponent component, ComponentStartup args) + { + if (!TryComp(uid, out var body)) + return; + + var shape = new PhysShapeCircle(component.Range); + + _fixtures.TryCreateFixture(uid, shape, BrainrotFixture, collisionLayer: (int) CollisionGroup.MobMask, hard: false, body: body); + } + + private void OnShutdown(EntityUid uid, BrainrotAuraComponent component, ComponentShutdown args) + { + if (!TryComp(uid, out var body) || + MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating) + { + return; + } + + _fixtures.DestroyFixture(uid, BrainrotFixture, body: body); + } + + private void OnCollide(EntityUid uid, BrainrotAuraComponent component, ref StartCollideEvent args) + { + // No effect if the mob is mindless + if (!_mind.TryGetMind(args.OtherEntity, out _, out _)) + { + return; + } + + // No effect if the mob has brainrot or causes brainrot + if (HasComp(args.OtherEntity) || HasComp(args.OtherEntity)) + { + return; + } + + RaiseLocalEvent(new EntityGivenBrainrotEvent(args.OtherEntity, uid, component.Time)); + } + + private void EntityGivenBrainrot(EntityGivenBrainrotEvent args) + { + if (TryComp(args.Target, out var comp)) + { + comp.Duration += args.Time; + } + else + { + comp = EnsureComp(args.Target); + } + + var cancel = new CancellationTokenSource(); + + Timer.Spawn(TimeSpan.FromSeconds(comp.Duration), () => + { + comp.Duration -= args.Time; + + if (comp.Duration <= 0.0f) + { + RaiseLocalEvent(new EntityLostBrainrotEvent(args.Target)); + } + }, cancel.Token); + + comp.Cancels.Add(cancel); + + _popup.PopupEntity(Loc.GetString(BrainRotApplied), args.Target, args.Target); + } + + private void EntityLostBrainrot(EntityLostBrainrotEvent args) + { + if (TryComp(args.Target, out var comp)) + { + foreach (var cancel in comp.Cancels) + { + cancel.Cancel(); + } + } + + RemComp(args.Target); + _popup.PopupEntity(Loc.GetString(BrainRotLost), args.Target, args.Target); + } + + private void ApplyBrainRot(EntityUid entity, BrainrotComponent comp, AccentGetEvent args) + { + args.Message = Loc.GetString(_random.Pick(BrainRotReplacementStrings)); + } +} diff --git a/Content.Server/LegallyDistinctSpaceFerret/CanBackflipSystem.cs b/Content.Server/LegallyDistinctSpaceFerret/CanBackflipSystem.cs new file mode 100644 index 0000000000..df73a64021 --- /dev/null +++ b/Content.Server/LegallyDistinctSpaceFerret/CanBackflipSystem.cs @@ -0,0 +1,29 @@ +using Content.Server.Actions; +using Content.Shared.LegallyDistinctSpaceFerret; + +namespace Content.Server.LegallyDistinctSpaceFerret; + +public sealed class CanBackflipSystem : EntitySystem +{ + [Dependency] private readonly ActionsSystem _actions = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnBackflipAction); + } + + private void OnInit(EntityUid uid, CanBackflipComponent component, MapInitEvent args) + { + _actions.AddAction(uid, ref component.BackflipActionEntity, component.BackflipAction, uid); + } + + public void OnBackflipAction(EntityUid uid, CanBackflipComponent comp, BackflipActionEvent args) + { + RaiseNetworkEvent(new DoABackFlipEvent(GetNetEntity(uid), comp.ClappaSfx)); + + args.Handled = true; + } +} diff --git a/Content.Server/LegallyDistinctSpaceFerret/CanHibernateSystem.cs b/Content.Server/LegallyDistinctSpaceFerret/CanHibernateSystem.cs new file mode 100644 index 0000000000..d9591baf8c --- /dev/null +++ b/Content.Server/LegallyDistinctSpaceFerret/CanHibernateSystem.cs @@ -0,0 +1,86 @@ +using Content.Server.Actions; +using Content.Server.Atmos.Piping.Unary.Components; +using Content.Server.GameTicking; +using Content.Server.Ghost.Roles.Components; +using Content.Server.Popups; +using Content.Shared.Interaction.Components; +using Content.Shared.LegallyDistinctSpaceFerret; +using Content.Shared.Mind; +using Content.Shared.NPC; +using Content.Shared.Popups; +using Robust.Server.Audio; +using Robust.Shared.Audio; + +namespace Content.Server.LegallyDistinctSpaceFerret; + +public sealed class CanHibernateSystem : EntitySystem +{ + [Dependency] private readonly ActionsSystem _actions = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly GameTicker _ticker = default!; + [Dependency] private readonly AudioSystem _audio = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnEepyAction); + } + + private void OnInit(EntityUid uid, CanHibernateComponent component, MapInitEvent args) + { + _actions.AddAction(uid, ref component.EepyActionEntity, component.EepyAction, uid); + } + + public void OnEepyAction(EntityUid uid, CanHibernateComponent comp, EepyActionEvent args) + { + // If you require food before hibernating, assert that this condition has been fulfilled. + if (_mind.TryGetObjectiveComp(uid, out var nutrientsCondition) && nutrientsCondition.NutrientsConsumed / nutrientsCondition.NutrientsRequired < 1.0) + { + _popup.PopupEntity(Loc.GetString(comp.NotEnoughNutrientsMessage), uid, PopupType.SmallCaution); + + return; + } + + // Assert that you're near a hibernation point (scrubbers) + var scrubbers = _lookup.GetEntitiesInRange(Transform(uid).Coordinates, 2f); + if (scrubbers.Count <= 0) + { + _popup.PopupEntity(Loc.GetString(comp.TooFarFromHibernationSpot), uid, PopupType.SmallCaution); + + return; + } + + if (_mind.TryGetObjectiveComp(uid, out var hibernateCondition)) + { + _audio.PlayPvs(new SoundPathSpecifier(hibernateCondition.SuccessSfx), uid); + _popup.PopupEntity(Loc.GetString(hibernateCondition.SuccessMessage), uid, PopupType.Large); + hibernateCondition.Hibernated = true; + } + + // Kick player out + var mind = _mind.GetMind(uid); + if (mind != null) + { + _ticker.OnGhostAttempt(mind.Value, false); + } + + // End ghost-role + AddComp(uid); + RemComp(uid); + RemComp(uid); + + // Notify + RaiseNetworkEvent(new EntityHasHibernated(GetNetEntity(uid), comp.SpriteStateId)); + + args.Handled = true; + } +} + +public struct EntityHibernateAttemptSuccessEvent(EntityUid entity) +{ + public EntityUid Entity = entity; +} diff --git a/Content.Server/LegallyDistinctSpaceFerret/ConsumeNutrientsObjective.cs b/Content.Server/LegallyDistinctSpaceFerret/ConsumeNutrientsObjective.cs new file mode 100644 index 0000000000..3819c9ac96 --- /dev/null +++ b/Content.Server/LegallyDistinctSpaceFerret/ConsumeNutrientsObjective.cs @@ -0,0 +1,19 @@ +using Content.Shared.LegallyDistinctSpaceFerret; +using Content.Shared.Objectives.Components; + +namespace Content.Server.LegallyDistinctSpaceFerret; + +public sealed class ConsumeNutrientsObjectiveSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnConsumeNutrientsGetProgress); + } + + private static void OnConsumeNutrientsGetProgress(EntityUid uid, ConsumeNutrientsConditionComponent comp, ref ObjectiveGetProgressEvent args) + { + args.Progress = comp.NutrientsConsumed / comp.NutrientsRequired; + } +} diff --git a/Content.Server/LegallyDistinctSpaceFerret/HibernateObjectiveSystem.cs b/Content.Server/LegallyDistinctSpaceFerret/HibernateObjectiveSystem.cs new file mode 100644 index 0000000000..5250fbee5f --- /dev/null +++ b/Content.Server/LegallyDistinctSpaceFerret/HibernateObjectiveSystem.cs @@ -0,0 +1,19 @@ +using Content.Shared.LegallyDistinctSpaceFerret; +using Content.Shared.Objectives.Components; + +namespace Content.Server.LegallyDistinctSpaceFerret; + +public sealed class HibernateObjectiveSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnHibernateGetProgress); + } + + private static void OnHibernateGetProgress(EntityUid uid, HibernateConditionComponent comp, ref ObjectiveGetProgressEvent args) + { + args.Progress = comp.Hibernated ? 1.0f : 0.0f; + } +} diff --git a/Content.Server/LegallyDistinctSpaceFerret/LegallyDistinctSpaceFerretSystem.cs b/Content.Server/LegallyDistinctSpaceFerret/LegallyDistinctSpaceFerretSystem.cs new file mode 100644 index 0000000000..01ca018a56 --- /dev/null +++ b/Content.Server/LegallyDistinctSpaceFerret/LegallyDistinctSpaceFerretSystem.cs @@ -0,0 +1,87 @@ +using Content.Server.Chat.Managers; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.GenericAntag; +using Content.Server.Interaction; +using Content.Server.Popups; +using Content.Server.Roles; +using Content.Server.StationEvents.Events; +using Content.Shared.LegallyDistinctSpaceFerret; +using Content.Shared.Mind; +using Content.Shared.Nutrition.EntitySystems; +using Content.Shared.Roles; +using Robust.Server.Audio; +using Robust.Shared.Audio; + +namespace Content.Server.LegallyDistinctSpaceFerret; + +public sealed class LegallyDistinctSpaceFerretSystem : EntitySystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly AudioSystem _audio = default!; + [Dependency] private readonly RoleSystem _role = default!; + [Dependency] private readonly IChatManager _chatMan = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly GameTicker _ticker = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnInteractFailed); + SubscribeLocalEvent(OnHungerModified); + } + + private void OnInit(EntityUid uid, LegallyDistinctSpaceFerretComponent component, GenericAntagCreatedEvent args) + { + var mind = args.Mind; + + if (mind.Session == null) + return; + + var session = mind.Session; + _role.MindAddRole(args.MindId, new RoleBriefingComponent + { + Briefing = Loc.GetString(component.RoleBriefing) + }, mind); + _role.MindAddRole(args.MindId, new LegallyDistinctSpaceFerretRoleComponent() + { + PrototypeId = component.AntagProtoId + }, mind); + _role.MindPlaySound(args.MindId, new SoundPathSpecifier(component.RoleIntroSfx), mind); + _chatMan.DispatchServerMessage(session, Loc.GetString(component.RoleGreeting)); + } + + public void OnInteractFailed(EntityUid uid, LegallyDistinctSpaceFerretComponent _, InteractionAttemptFailed args) + { + RaiseLocalEvent(uid, new BackflipActionEvent()); + } + + private void OnHungerModified(EntityUid uid, LegallyDistinctSpaceFerretComponent comp, HungerModifiedEvent args) + { + if (_mind.TryGetObjectiveComp(uid, out var nutrientsCondition) && args.Amount > 0) + { + nutrientsCondition.NutrientsConsumed += args.Amount; + } + } +} + +[RegisterComponent, Access(typeof(LegallyDistinctSpaceFerretSystem)), ExclusiveAntagonist] +public sealed partial class LegallyDistinctSpaceFerretRoleComponent : AntagonistRoleComponent; + +[RegisterComponent] +public sealed partial class LegallyDistinctSpaceFerretSpawnRuleComponent : Component; + +public sealed class LegallyDistinctSpaceFerretSpawnRule : StationEventSystem +{ + protected override void Started(EntityUid uid, LegallyDistinctSpaceFerretSpawnRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, comp, gameRule, args); + + TryFindRandomTile(out _, out _, out _, out var coords); + Sawmill.Info($"Creating ferret spawn point at {coords}"); + Spawn("SpawnPointGhostLegallyDistinctSpaceFerret", coords); + } +} diff --git a/Content.Shared/LegallyDistinctSpaceFerret/BrainrotAuraComponent.cs b/Content.Shared/LegallyDistinctSpaceFerret/BrainrotAuraComponent.cs new file mode 100644 index 0000000000..e0a4decc6d --- /dev/null +++ b/Content.Shared/LegallyDistinctSpaceFerret/BrainrotAuraComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.LegallyDistinctSpaceFerret; + +[RegisterComponent] +public sealed partial class BrainrotAuraComponent : Component +{ + [DataField] + public float Range = 3.0f; + + [DataField] + public float Time = 10.0f; +} diff --git a/Content.Shared/LegallyDistinctSpaceFerret/BrainrotComponent.cs b/Content.Shared/LegallyDistinctSpaceFerret/BrainrotComponent.cs new file mode 100644 index 0000000000..4be3d76ba8 --- /dev/null +++ b/Content.Shared/LegallyDistinctSpaceFerret/BrainrotComponent.cs @@ -0,0 +1,12 @@ +using System.Threading; + +namespace Content.Shared.LegallyDistinctSpaceFerret; + +[RegisterComponent] +public sealed partial class BrainrotComponent : Component +{ + [DataField] + public float Duration = 10.0f; + + public List Cancels = []; +} diff --git a/Content.Shared/LegallyDistinctSpaceFerret/CanBackflipComponent.cs b/Content.Shared/LegallyDistinctSpaceFerret/CanBackflipComponent.cs new file mode 100644 index 0000000000..73ad7e5826 --- /dev/null +++ b/Content.Shared/LegallyDistinctSpaceFerret/CanBackflipComponent.cs @@ -0,0 +1,29 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.LegallyDistinctSpaceFerret; + +[RegisterComponent] +public sealed partial class CanBackflipComponent : Component +{ + [DataField] + public EntProtoId BackflipAction = "ActionBackflip"; + + [DataField] + public EntityUid? BackflipActionEntity; + + [DataField] + public string ClappaSfx = ""; +} + +public sealed partial class BackflipActionEvent : InstantActionEvent +{ +} + +[Serializable, NetSerializable] +public sealed class DoABackFlipEvent(NetEntity actioner, string sfxSource) : EntityEventArgs +{ + public NetEntity Actioner = actioner; + public string SfxSource = sfxSource; +} diff --git a/Content.Shared/LegallyDistinctSpaceFerret/CanHibernateComponent.cs b/Content.Shared/LegallyDistinctSpaceFerret/CanHibernateComponent.cs new file mode 100644 index 0000000000..2a95105d09 --- /dev/null +++ b/Content.Shared/LegallyDistinctSpaceFerret/CanHibernateComponent.cs @@ -0,0 +1,35 @@ +using Content.Shared.Actions; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.LegallyDistinctSpaceFerret; + +[RegisterComponent] +public sealed partial class CanHibernateComponent : Component +{ + [DataField] + public EntProtoId EepyAction = "ActionEepy"; + + [DataField] + public EntityUid? EepyActionEntity; + + [DataField] + public string NotEnoughNutrientsMessage = ""; + + [DataField] + public string TooFarFromHibernationSpot = ""; + + [DataField] + public string SpriteStateId = ""; +} + +public sealed partial class EepyActionEvent : InstantActionEvent +{ +} + +[Serializable, NetSerializable] +public sealed class EntityHasHibernated(NetEntity hibernator, string spriteStateId) : EntityEventArgs +{ + public NetEntity Hibernator = hibernator; + public string SpriteStateId = spriteStateId; +} diff --git a/Content.Shared/LegallyDistinctSpaceFerret/ConsumeNutrientsConditionComponent.cs b/Content.Shared/LegallyDistinctSpaceFerret/ConsumeNutrientsConditionComponent.cs new file mode 100644 index 0000000000..bc2c3ed70c --- /dev/null +++ b/Content.Shared/LegallyDistinctSpaceFerret/ConsumeNutrientsConditionComponent.cs @@ -0,0 +1,10 @@ +namespace Content.Shared.LegallyDistinctSpaceFerret; + +[RegisterComponent] +public sealed partial class ConsumeNutrientsConditionComponent : Component +{ + [DataField] + public float NutrientsRequired = 150.0f; + + public float NutrientsConsumed; +} diff --git a/Content.Shared/LegallyDistinctSpaceFerret/HibernateConditionComponent.cs b/Content.Shared/LegallyDistinctSpaceFerret/HibernateConditionComponent.cs new file mode 100644 index 0000000000..f994e34970 --- /dev/null +++ b/Content.Shared/LegallyDistinctSpaceFerret/HibernateConditionComponent.cs @@ -0,0 +1,13 @@ +namespace Content.Shared.LegallyDistinctSpaceFerret; + +[RegisterComponent] +public sealed partial class HibernateConditionComponent : Component +{ + public bool Hibernated; + + [DataField] + public string SuccessMessage = ""; + + [DataField] + public string SuccessSfx = ""; +} diff --git a/Content.Shared/LegallyDistinctSpaceFerret/LegallyDistinctSpaceFerretComponent.cs b/Content.Shared/LegallyDistinctSpaceFerret/LegallyDistinctSpaceFerretComponent.cs new file mode 100644 index 0000000000..1b1386ffa4 --- /dev/null +++ b/Content.Shared/LegallyDistinctSpaceFerret/LegallyDistinctSpaceFerretComponent.cs @@ -0,0 +1,25 @@ +using System.Threading; +using Content.Shared.Actions; +using Content.Shared.Roles; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.LegallyDistinctSpaceFerret; + +[RegisterComponent] +public sealed partial class LegallyDistinctSpaceFerretComponent : Component +{ + [DataField] + public string RoleIntroSfx = ""; + + [DataField] + public ProtoId AntagProtoId = "LegallyDistinctSpaceFerret"; + + [DataField] + public string RoleBriefing = ""; + + [DataField] + public string RoleGreeting = ""; +} diff --git a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs index 89aae57074..70d457096e 100644 --- a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs @@ -69,6 +69,8 @@ public sealed class HungerSystem : EntitySystem if (!Resolve(uid, ref component)) return; SetHunger(uid, component.CurrentHunger + amount, component); + + RaiseLocalEvent(uid, new HungerModifiedEvent(amount)); } /// @@ -211,3 +213,7 @@ public sealed class HungerSystem : EntitySystem } } +public sealed class HungerModifiedEvent(float amount) : EntityEventArgs +{ + public float Amount = amount; +} diff --git a/Resources/Audio/Animals/attributions.yml b/Resources/Audio/Animals/attributions.yml index c34832a807..a8f421a712 100644 --- a/Resources/Audio/Animals/attributions.yml +++ b/Resources/Audio/Animals/attributions.yml @@ -7,7 +7,7 @@ license: "CC-BY-3.0" copyright: "Modified from 'Meow 4.wav' by freesound user 'TRNGLE. The original audio was trimmed, split to mono, and converted from WAV to OGG format" source: "https://freesound.org/people/TRNGLE/sounds/368006/" - + - files: ["cat_meow2.ogg"] license: "CC-BY-3.0" copyright: "Created by freesound user 'TRNGLE. The original audio split to mono, and converted from WAV to OGG format" @@ -117,24 +117,28 @@ license: "CC-BY-4.0" copyright: "Audio is recorded/created by Pfranzen 'FreeSound.org'. The original audio was trimmed and renamed" source: "https://freesound.org/people/pfranzen/sounds/322744/" - + - files: ["dog_bark1.ogg"] license: "CC0-1.0" copyright: "Audio is recorded/created by KFerentchak 'FreeSound.org'. The original audio was trimmed and renamed" - source: "https://freesound.org/people/KFerentchak/sounds/235912/" - + source: "https://freesound.org/people/KFerentchak/sounds/235912/" + - files: ["dog_bark2.ogg"] license: "CC0-1.0" copyright: "Audio is recorded/created by KFerentchak 'FreeSound.org'. The original audio was trimmed and renamed" - source: "https://freesound.org/people/KFerentchak/sounds/235912/" - + source: "https://freesound.org/people/KFerentchak/sounds/235912/" + - files: ["dog_bark3.ogg"] license: "CC0-1.0" copyright: "Audio is recorded/created by KFerentchak 'FreeSound.org'. The original audio was trimmed and renamed" source: "https://freesound.org/people/KFerentchak/sounds/235912/" - + - files: ["nymph_chirp.ogg"] license: "CC-BY-SA-3.0" copyright: "Taken from ParadiseSS13" source: "https://github.com/ParadiseSS13/Paradise/commit/a34f1054cef5a44a67fdac3b67b811137c6071dd" - \ No newline at end of file + +- files: ["wawa_intro.ogg", "wawa_outro.ogg", "wawa_chatter.ogg", "wawa_chillin.ogg", "wawa_exclaim.ogg", "wawa_question.ogg", "wawa_statement.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Hannah 'FairlySadPanda' Dawson, 2024. Industrial audio sample taken from GDC 2015 bundle - amb_JD_drone_gates_ofmydian" + source: "https://sonniss.com/gameaudiogdc" diff --git a/Resources/Audio/Animals/slugclappa.ogg b/Resources/Audio/Animals/slugclappa.ogg new file mode 100644 index 0000000000..86bbd528c0 Binary files /dev/null and b/Resources/Audio/Animals/slugclappa.ogg differ diff --git a/Resources/Audio/Animals/wawa_chatter.ogg b/Resources/Audio/Animals/wawa_chatter.ogg new file mode 100644 index 0000000000..7e2a7ec3ca Binary files /dev/null and b/Resources/Audio/Animals/wawa_chatter.ogg differ diff --git a/Resources/Audio/Animals/wawa_chillin.ogg b/Resources/Audio/Animals/wawa_chillin.ogg new file mode 100644 index 0000000000..5955fdf967 Binary files /dev/null and b/Resources/Audio/Animals/wawa_chillin.ogg differ diff --git a/Resources/Audio/Animals/wawa_exclaim.ogg b/Resources/Audio/Animals/wawa_exclaim.ogg new file mode 100644 index 0000000000..99f280091f Binary files /dev/null and b/Resources/Audio/Animals/wawa_exclaim.ogg differ diff --git a/Resources/Audio/Animals/wawa_intro.ogg b/Resources/Audio/Animals/wawa_intro.ogg new file mode 100644 index 0000000000..ca6eedb6be Binary files /dev/null and b/Resources/Audio/Animals/wawa_intro.ogg differ diff --git a/Resources/Audio/Animals/wawa_outro.ogg b/Resources/Audio/Animals/wawa_outro.ogg new file mode 100644 index 0000000000..3877f4a7ba Binary files /dev/null and b/Resources/Audio/Animals/wawa_outro.ogg differ diff --git a/Resources/Audio/Animals/wawa_question.ogg b/Resources/Audio/Animals/wawa_question.ogg new file mode 100644 index 0000000000..ce9008a5d8 Binary files /dev/null and b/Resources/Audio/Animals/wawa_question.ogg differ diff --git a/Resources/Audio/Animals/wawa_statement.ogg b/Resources/Audio/Animals/wawa_statement.ogg new file mode 100644 index 0000000000..21f9e8a23e Binary files /dev/null and b/Resources/Audio/Animals/wawa_statement.ogg differ diff --git a/Resources/Locale/en-US/legallydistinctspaceferret/role.ftl b/Resources/Locale/en-US/legallydistinctspaceferret/role.ftl new file mode 100644 index 0000000000..836007c1ce --- /dev/null +++ b/Resources/Locale/en-US/legallydistinctspaceferret/role.ftl @@ -0,0 +1,52 @@ +legallydistinctspaceferret-round-end-agent-name = ninja + +objective-issuer-legallydistinctspaceferret = [color=#33cc00]Space Ferret Tribe[/color] + +legallydistinctspaceferret-role-greeting = + I've been sent by my tribe to the surface to scavenge, and now I'm hungry and alone. :( + I need to eat a bunch of nutrients and find a safe place to hibernate! + Hopefully these strange tall creatures don't notice me. + +objective-condition-legallydistinctspaceferret-title = Prepare for hibernation! +objective-condition-legallydistinctspaceferret-description = You need to eat {$count} nutrients. + +roles-antag-legallydistinctspaceferret-name = Legally Distinct Space Ferret +roles-antag-legallydistinctspaceferret-objective = Find as much food as possible, then hibernate in a gas scrubber. + +legallydistinctspaceferret-role-briefing = You're a small, potentially-innocent creature lost on a big, scary space station. Eat as much food as you need to hibernate, then hibernate in a gas scrubber. You have no time limit, but leaving on the evac shuttle does NOT count as a draw or lesser success. Your hibernating body does not need to survive the remaining round, so feel free to clock out once you've had your fun. Happy April Fools! + +petting-success-legallydistinctspaceferret = You pet {THE($target)} on {POSS-ADJ($target)} legally distinct head. +petting-failure-legallydistinctspaceferret = You reach out to pet {THE($target)}, but {SUBJECT($target)} does a backflip! + +ghost-role-information-legallydistinctspaceferret-name = Legally Distinct Space Ferret +ghost-role-information-legallydistinctspaceferret-description = Wawa! Wawawa. Wa, wawawa, wa wa wa. +ghost-role-information-legallydistinctspaceferret-rules = You're a neutral antagonist, much like the closet skeleton, but have two objectives to fulfil. You don't remember anything from your previous life, unless you were a Legally Distinct Space Ferret. Consider yourself valid for random murder by the crew. We suggest you act adorable, are helpful, or become extremely robust. Go live your best life, wawa. + +accent-words-legallydistinctspaceferret-1 = Wa! +accent-words-legallydistinctspaceferret-2 = Wa? +accent-words-legallydistinctspaceferret-3 = Wa. +accent-words-legallydistinctspaceferret-4 = Wa... +accent-words-legallydistinctspaceferret-5 = Wawa! +accent-words-legallydistinctspaceferret-6 = Wawa? +accent-words-legallydistinctspaceferret-7 = Wawa. +accent-words-legallydistinctspaceferret-8 = Wawa... + +station-event-random-sentience-flavor-legallydistinctspaceferret = legally distinct space ferret + +chat-speech-verb-wawa-1 = intones +chat-speech-verb-wawa-2 = states +chat-speech-verb-wawa-3 = declares + +brainrot-applied = You feel strangeg, andug yourug mikug skug skug skug... +brainrot-lost = Skug skug skur skind slear from the influence of Brainrot. + +brainrot-replacement-1 = SCUG!! +brainrot-replacement-2 = Scug! +brainrot-replacement-3 = Scug. +brainrot-replacement-4 = Scug... +brainrot-replacement-5 = Scug? +brainrot-replacement-6 = Scug!? + +legallydistinctspaceferret-out-of-range = You need to find an air scrubber to hibernate in, wawa! +legallydistinctspaceferret-not-enough-nutrients = You've not consumed enough nutrients to hibernate yet, wawa! +legallydistinctspaceferret-you-win-popup = Until the next cycle... Happy April Fools, wawa! diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/legallydistinctspaceferret.yml b/Resources/Prototypes/Entities/Mobs/NPCs/legallydistinctspaceferret.yml new file mode 100644 index 0000000000..20bfbff112 --- /dev/null +++ b/Resources/Prototypes/Entities/Mobs/NPCs/legallydistinctspaceferret.yml @@ -0,0 +1,305 @@ +- type: entity + name: Legally Distinct Space Ferret + id: MobLegallyDistinctSpaceFerret + parent: MobBaseAncestor + description: There's apparently a cult who worship these adorable vermin. It's a shame they cause Brainrot. Skug. + components: + # Basics + - type: LegallyDistinctSpaceFerret + roleIntroSfx: /Audio/Animals/wawa_intro.ogg + antagProtoId: LegallyDistinctSpaceFerret + roleBriefing: legallydistinctspaceferret-role-briefing + roleGreeting: legallydistinctspaceferret-role-greeting + - type: BrainrotAura + - type: CanBackflip + clappaSfx: /Audio/Animals/slugclappa.ogg + - type: CanHibernate + tooFarFromHibernationSpot: legallydistinctspaceferret-out-of-range + notEnoughNutrientsMessage: legallydistinctspaceferret-not-enough-nutrients + spriteStateId: legallydistinctspaceferret_eepy + - type: NameIdentifier + group: LegallyDistinctSpaceFerret + - type: ReplacementAccent + accent: legallydistinctspaceferret + - type: Sprite + drawdepth: SmallMobs + layers: + - map: ["enum.DamageStateVisualLayers.Base"] + sprite: Mobs/Animals/legallydistinctspaceferret.rsi + state: legallydistinctspaceferret + - map: [ "enum.HumanoidVisualLayers.Handcuffs" ] + color: "#ffffff" + sprite: Objects/Misc/handcuffs.rsi + state: body-overlay-2 + visible: false + - type: RandomSprite + getAllGroups: true + available: + - enum.DamageStateVisualLayers.Base: + legallydistinctspaceferret: LegallyDistinctSpaceFerretColors + - type: DamageStateVisuals + states: + Alive: + Base: legallydistinctspaceferret + Critical: + Base: legallydistinctspaceferret_oof + Dead: + Base: legallydistinctspaceferret_rip + - type: MobThresholds + thresholds: + 0: Alive + 60: Critical + 125: Dead + - type: Temperature + heatDamageThreshold: 360 + coldDamageThreshold: 285 + currentTemperature: 310.15 + specificHeat: 42 + - type: Butcherable + butcheringType: Spike + spawned: + - id: FoodMeat + amount: 4 # Good eatin', though. You monster. + # They wawa + - type: Speech + speechVerb: Wawa + speechSounds: Wawa + - type: TypingIndicator + proto: moth + # They hard to pet + - type: InteractionPopup + successChance: 0.5 + interactSuccessString: petting-success-legallydistinctspaceferret + interactFailureString: petting-failure-legallydistinctspaceferret + interactSuccessSpawn: EffectHearts + interactSuccessSound: + path: /Audio/Animals/wawa_chillin.ogg + interactFailureSound: + path: /Audio/Animals/wawa_chatter.ogg + # They nyoom and go under doors/tables + - type: MovementSpeedModifier + baseWalkSpeed: 5 # nyoom + baseSprintSpeed: 7 # NYOOOOOM + - type: MeleeWeapon + soundHit: + path: /Audio/Effects/bite.ogg + angle: 30 + animation: WeaponArcBite + damage: + types: + Piercing: 15 # NOM - Do NOT mess with skugs. + - type: Physics + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.2 + density: 120 + mask: + - SmallMobMask + layer: + - SmallMobLayer + # Round impact + - type: GhostRole + prob: 1 + makeSentient: true + name: ghost-role-information-legallydistinctspaceferret-name + description: ghost-role-information-legallydistinctspaceferret-description + - type: Food + - type: Hunger + baseDecayRate: 0.3 + - type: GenericAntag + rule: LegallyDistinctSpaceFerretRule + # L.D.S.F.'s can have IDs because ... why not, honestly + - type: Inventory + templateId: legallydistinctspaceferret + - type: InventorySlots + + # Midround antagonist, spawned via random event +- type: entity + parent: BaseGameRule + id: LegallyDistinctSpaceFerretSpawn + noSpawn: true + components: + - type: StationEvent + weight: 10 + duration: 1 + minimumPlayers: 10 + - type: LegallyDistinctSpaceFerretSpawnRule + +- type: entity + id: SpawnPointGhostLegallyDistinctSpaceFerret + name: ghost role spawn point + suffix: legally distinct space ferret + parent: MarkerBase + components: + - type: GhostRole + name: ghost-role-information-legallydistinctspaceferret-name + description: ghost-role-information-legallydistinctspaceferret-description + rules: ghost-role-information-legallydistinctspaceferret-rules + - type: GhostRoleMobSpawner + prototype: MobLegallyDistinctSpaceFerret + - type: Sprite + sprite: Mobs/Animals/legallydistinctspaceferret.rsi + layers: + - state: legallydistinctspaceferret_eepy + +- type: entity + noSpawn: true + parent: BaseGameRule + id: LegallyDistinctSpaceFerretRule + components: + - type: GenericAntagRule + agentName: legallydistinctspaceferret-round-end-agent-name + objectives: + - ConsumeNutrientsObjective + - HibernateObjective + +- type: entity + abstract: true + parent: BaseObjective + id: BaseLegallyDistinctSpaceFerretObjective + components: + - type: Objective + difficulty: 1.5 + issuer: legallydistinctspaceferret + - type: RoleRequirement + roles: + components: + - LegallyDistinctSpaceFerretRole + +- type: entity + parent: BaseLegallyDistinctSpaceFerretObjective + id: ConsumeNutrientsObjective + description: Consume enough food to survive. + components: + - type: Objective + icon: + sprite: Mobs/Animals/legallydistinctspaceferret.rsi + state: legallydistinctspaceferret_eepy + - type: ConsumeNutrientsCondition + nutrientsRequired: 10.0 + +- type: entity + parent: BaseLegallyDistinctSpaceFerretObjective + id: HibernateObjective + description: Hibernate in a gas scrubber. + components: + - type: Objective + icon: + sprite: Mobs/Animals/legallydistinctspaceferret.rsi + state: legallydistinctspaceferret_eepy + - type: HibernateCondition + successMessage: legallydistinctspaceferret-you-win-popup + successSfx: /Audio/Animals/wawa_outro.ogg + +- type: accent + id: legallydistinctspaceferret + fullReplacements: + - accent-words-legallydistinctspaceferret-1 + - accent-words-legallydistinctspaceferret-2 + - accent-words-legallydistinctspaceferret-3 + - accent-words-legallydistinctspaceferret-4 + - accent-words-legallydistinctspaceferret-5 + - accent-words-legallydistinctspaceferret-6 + - accent-words-legallydistinctspaceferret-7 + - accent-words-legallydistinctspaceferret-8 + +- type: speechVerb + id: Wawa + speechVerbStrings: + - chat-speech-verb-wawa-1 + - chat-speech-verb-wawa-2 + - chat-speech-verb-wawa-3 + +- type: palette + id: LegallyDistinctSpaceFerretColors + name: LegallyDistinctSpaceFerretColors + colors: + Default: "#ffffff" + Innocent: "#f6f439" + Angry: "#dc5864" + Eldritch: "#dc5864" + Forgotten: "#111111" + Fat: "#fefcab" + WarCriminal: "#ab3430" + Cocaine: "#679cfe" + Mutant: "#7824a0" + Damned: "#98e652" + +- type: entity + id: ActionBackflip + name: Backflip + description: DO A BACKFLIP! + noSpawn: true + components: + - type: InstantAction + useDelay: 3 + icon: deprecated.rsi/deprecated.png + event: !type:BackflipActionEvent + checkCanInteract: false + +- type: entity + id: ActionEepy + name: Eepy + description: Hibernate. This ends your time as a Legally Distinct Space Ferret. Happy April Fools! + noSpawn: true + components: + - type: InstantAction + useDelay: 100 + icon: { sprite: Mobs/Animals/legallydistinctspaceferret.rsi, state: legallydistinctspaceferret_eepy } + event: !type:EepyActionEvent + checkCanInteract: false + +- type: speechSounds + id: Wawa + saySound: + path: /Audio/Animals/wawa_statement.ogg + askSound: + path: /Audio/Animals/wawa_question.ogg + exclaimSound: + path: /Audio/Animals/wawa_exclaim.ogg + +- type: antag + id: LegallyDistinctSpaceFerret + name: roles-antag-legallydistinctspaceferret-agent-name + antagonist: true + setPreference: false + objective: roles-antag-legallydistinctspaceferret-objective + +- type: inventoryTemplate + id: legallydistinctspaceferret + slots: + - name: head + slotTexture: head + slotFlags: HEAD + uiWindowPos: 1,2 + strippingWindowPos: 0,0 + displayName: Head + - name: ears + slotTexture: ears + slotFlags: EARS + stripTime: 3 + uiWindowPos: 0,2 + strippingWindowPos: 1,2 + displayName: Ears + - name: mask + slotTexture: mask + slotFlags: MASK + uiWindowPos: 0,1 + strippingWindowPos: 1,1 + displayName: Mask + - name: id + slotTexture: id + slotFlags: IDCARD + slotGroup: SecondHotbar + stripTime: 6 + uiWindowPos: 2,1 + strippingWindowPos: 2,4 + displayName: ID + +- type: nameIdentifierGroup + id: LegallyDistinctSpaceFerret + minValue: 1 + maxValue: 99 # I fucking dare you to spawn more than 100 wawa diff --git a/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret.png b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret.png new file mode 100644 index 0000000000..7562f35fab Binary files /dev/null and b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret.png differ diff --git a/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret_eepy.png b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret_eepy.png new file mode 100644 index 0000000000..e01ad55569 Binary files /dev/null and b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret_eepy.png differ diff --git a/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret_oof.png b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret_oof.png new file mode 100644 index 0000000000..045d6ca15c Binary files /dev/null and b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret_oof.png differ diff --git a/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret_rip.png b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret_rip.png new file mode 100644 index 0000000000..775b89641f Binary files /dev/null and b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/legallydistinctspaceferret_rip.png differ diff --git a/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/meta.json b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/meta.json new file mode 100644 index 0000000000..9ff10c5326 --- /dev/null +++ b/Resources/Textures/Mobs/Animals/legallydistinctspaceferret.rsi/meta.json @@ -0,0 +1,24 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from Yogstation at https://github.com/yogstation13/Yogstation/blob/b5a72f35980b15404d3c507d2ad0af06010f6056/icons/mob/pets.dmi, some modifications by Hannah 'FairlySadPanda' Dawson.", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "legallydistinctspaceferret", + "directions": 4 + }, + { + "name": "legallydistinctspaceferret_eepy" + }, + { + "name": "legallydistinctspaceferret_rip" + }, + { + "name": "legallydistinctspaceferret_oof" + } + ] +}