diff --git a/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerComponent.cs b/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerComponent.cs new file mode 100644 index 0000000000..147441412d --- /dev/null +++ b/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerComponent.cs @@ -0,0 +1,90 @@ +using Robust.Shared.Audio; + +namespace Content.Server.SimpleStation14.LoudSpeakers; + +[RegisterComponent] +public sealed class LoudSpeakerComponent : Component +{ + public IPlayingAudioStream? CurrentPlayingSound; + + public TimeSpan NextPlayTime = TimeSpan.Zero; + + /// + /// The port to look for a signal on. + /// + public string PlaySoundPort = "Trigger"; + + /// + /// Whether or not this loudspeaker has ports. + /// + [DataField("ports")] + public bool Ports = true; + + /// + /// The name of the container the payload is in. + /// This is specified in the construction graph. + /// + [DataField("containerSlot")] + public string ContainerSlot = "payload"; + + /// + /// Can this loudspeaker be triggered by interacting with it? + /// + [DataField("triggerOnInteract")] + public bool TriggerOnInteract = false; + + /// + /// Should this loudspeaker interrupt already playing sounds if triggered? + /// If false, the sounds will overlap. + /// + /// + /// Warning: If this is false, the speaker will not clean up after itself properly. + /// Since it only saves one sound at a time Use with caution. + /// + [DataField("interrupt")] + public bool Interrupt = true; + + /// + /// Cool down time between playing sounds. + /// + [DataField("cooldown")] + public TimeSpan Cooldown = TimeSpan.FromSeconds(4); + + /// + /// The sound to play if no other sound is specified. + /// + [DataField("defaultSound")] + public SoundSpecifier DefaultSound = new SoundPathSpecifier("/Audio/SimpleStation14/Effects/metaldink.ogg"); + + /// + /// Default variance to be used, if no other variance is specified. + /// Is still subject to . + /// + [DataField("defaultVariance")] + public float DefaultVariance = 0.125f; + + /// + /// The amount to multiply the volume by. + /// + [DataField("volumeMod")] + public float VolumeMod = 3.5f; + + /// + /// The amount to multiply the range by. + /// + [DataField("rangeMod")] + public float RangeMod = 3.5f; + + /// + /// The amount to multiply the rolloff by. + /// + [DataField("rolloffMod")] + public float RolloffMod = 0.3f; + + /// + /// Amount to multiply the variance by, if the sound has one. + /// If the sound has a variance of 0, default variance is used. + /// + [DataField("varianceMod")] + public float VarianceMod = 1.5f; +} diff --git a/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerSystem.cs b/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerSystem.cs new file mode 100644 index 0000000000..8d7aceb5d7 --- /dev/null +++ b/Content.Server/SimpleStation14/Loudspeakers/LoudSpeakerSystem.cs @@ -0,0 +1,133 @@ +using Content.Server.MachineLinking.Events; +using Content.Server.MachineLinking.System; +using Content.Server.Power.Components; +using Robust.Shared.Audio; +using Robust.Shared.Player; +using Robust.Shared.Containers; +using Content.Server.Sound.Components; +using Content.Shared.Sound.Components; +using Content.Shared.Interaction; +using Robust.Shared.Timing; + +namespace Content.Server.SimpleStation14.LoudSpeakers; + +public sealed class DoorSignalControlSystem : EntitySystem +{ + [Dependency] private readonly SignalLinkerSystem _signal = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IGameTiming _timing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnShutdown); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnSignalReceived); + + SubscribeLocalEvent(OnInteractHand); + } + + private void OnShutdown(EntityUid uid, LoudSpeakerComponent component, ComponentShutdown args) + { + if (component.CurrentPlayingSound != null) + component.CurrentPlayingSound.Stop(); + } + + private void OnInit(EntityUid uid, LoudSpeakerComponent component, ComponentInit args) + { + if (component.Ports) + _signal.EnsureReceiverPorts(uid, component.PlaySoundPort); + } + + /// + /// Tries to play a loudspeaker. + /// + /// The Loudspeaker to play. + /// The Loudspeaker component. + /// True if the Loudspeaker was played, false otherwise. + public bool TryPlayLoudSpeaker(EntityUid uid, LoudSpeakerComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (component.NextPlayTime > _timing.CurTime) + return false; + + if (TryComp(uid, out var powerComp) && !powerComp.Powered) + return false; + + PlayLoudSpeaker(uid, component, GetSpeakerSound(uid, component)); + + return true; + } + + private void PlayLoudSpeaker(EntityUid uid, LoudSpeakerComponent component, SoundSpecifier sound) + { + var newParams = sound.Params + .WithVolume(sound.Params.Volume * component.VolumeMod) + .WithMaxDistance(sound.Params.MaxDistance * component.RangeMod) + .WithRolloffFactor(sound.Params.RolloffFactor * component.RolloffMod) + .WithVariation((sound.Params.Variation !> 0 ? component.DefaultVariance : sound.Params.Variation) * component.VarianceMod); + + if (component.Interrupt && component.CurrentPlayingSound != null) + component.CurrentPlayingSound.Stop(); + + component.NextPlayTime = _timing.CurTime + component.Cooldown; + + component.CurrentPlayingSound = _audio.Play(sound, Filter.Pvs(uid, component.RangeMod), uid, true, newParams); + } + + private SoundSpecifier GetSpeakerSound(EntityUid uid, LoudSpeakerComponent component) + { + if (!TryComp(uid, out var containerManager) || + !containerManager.TryGetContainer(component.ContainerSlot, out var container)) + return component.DefaultSound; + + if (container.ContainedEntities.Count == 0) + return component.DefaultSound; + + var entity = container.ContainedEntities[0]; + + switch (entity) + { + case { } when TryComp(entity, out var trigger) && trigger.Sound != null: + return trigger.Sound; + + case { } when TryComp(entity, out var activate) && activate.Sound != null: + return activate.Sound; + + case { } when TryComp(entity, out var use) && use.Sound != null: + return use.Sound; + + case { } when TryComp(entity, out var drop) && drop.Sound != null: + return drop.Sound; + + case { } when TryComp(entity, out var land) && land.Sound != null: + return land.Sound; + + default: + return component.DefaultSound; + } + } + + private void OnSignalReceived(EntityUid uid, LoudSpeakerComponent component, SignalReceivedEvent args) + { + if (args.Port == component.PlaySoundPort) + { + TryPlayLoudSpeaker(uid, component); + } + } + + private void OnInteractHand(EntityUid uid, LoudSpeakerComponent component, InteractHandEvent args) + { + if (!component.TriggerOnInteract) + return; + + if (!TryPlayLoudSpeaker(uid, component)) + return; + + args.Handled = true; + } +} diff --git a/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs b/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs index 63e55e0d14..02993ad224 100644 --- a/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs +++ b/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs @@ -3,6 +3,7 @@ using Robust.Shared.Serialization.Markdown.Mapping; using Robust.Shared.Serialization.Markdown.Validation; using Robust.Shared.Serialization.TypeSerializers.Interfaces; +using Content.Shared.SimpleStation14.Construction.Steps; // Parkstation-LoudSpeakers namespace Content.Shared.Construction.Steps { @@ -41,6 +42,13 @@ public sealed class ConstructionGraphStepTypeSerializer : ITypeReader CompBlacklist { get; } = new List(); + [DataField("tagBlacklist")] public List TagBlacklist { get; } = new List(); + + public override bool EntityValid(EntityUid uid, IEntityManager entityManager, IComponentFactory compFactory) + { + if (!entityManager.TryGetComponent(uid, out var item) || item.Size > Size) + return false; + + foreach (var component in CompBlacklist) + { + if (entityManager.HasComponent(uid, compFactory.GetComponent(component).GetType())) + return false; + } + + var tagSystem = entityManager.EntitySysManager.GetEntitySystem(); + + if (tagSystem.HasAnyTag(uid, TagBlacklist)) + return false; + + return true; + } + + public override void DoExamine(ExaminedEvent examinedEvent) + { + examinedEvent.Message.AddMarkup(string.IsNullOrEmpty(Name) + ? Loc.GetString( + "construction-insert-entity-below-size", + ("size", Size)) + : Loc.GetString( + "construction-insert-exact-entity", + ("entityName", Name))); + } + } +} diff --git a/Resources/Audio/SimpleStation14/Effects/attributions.yml b/Resources/Audio/SimpleStation14/Effects/attributions.yml new file mode 100644 index 0000000000..6c2a4d76d0 --- /dev/null +++ b/Resources/Audio/SimpleStation14/Effects/attributions.yml @@ -0,0 +1,9 @@ +- files: ["metaldink.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Recorded and edited by @Pspritechologist#9442" + source: "Parkstation COMMIT HERE" + +- files: ["buzzer_one.ogg", "buzzer_two.ogg"] + license: "Pixabay Content License" + copyright: "By Pixabay" + source: "https://pixabay.com/sound-effects/buzz-buzz-95806/" \ No newline at end of file diff --git a/Resources/Audio/SimpleStation14/Effects/buzzer_one.ogg b/Resources/Audio/SimpleStation14/Effects/buzzer_one.ogg new file mode 100644 index 0000000000..0cdeac73b7 Binary files /dev/null and b/Resources/Audio/SimpleStation14/Effects/buzzer_one.ogg differ diff --git a/Resources/Audio/SimpleStation14/Effects/buzzer_two.ogg b/Resources/Audio/SimpleStation14/Effects/buzzer_two.ogg new file mode 100644 index 0000000000..a2a417c9a1 Binary files /dev/null and b/Resources/Audio/SimpleStation14/Effects/buzzer_two.ogg differ diff --git a/Resources/Audio/SimpleStation14/Effects/metaldink.ogg b/Resources/Audio/SimpleStation14/Effects/metaldink.ogg new file mode 100644 index 0000000000..7acfd4617f Binary files /dev/null and b/Resources/Audio/SimpleStation14/Effects/metaldink.ogg differ diff --git a/Resources/Locale/en-US/construction/steps/size-construction-graph-step.ftl b/Resources/Locale/en-US/construction/steps/size-construction-graph-step.ftl new file mode 100644 index 0000000000..b2920740a7 --- /dev/null +++ b/Resources/Locale/en-US/construction/steps/size-construction-graph-step.ftl @@ -0,0 +1,3 @@ +# Parkstation +# Shown when examining an in-construction object +construction-insert-entity-below-size = Next, insert an entity no larger than {$size}. diff --git a/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/Electronics/loud_speaker.yml b/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/Electronics/loud_speaker.yml new file mode 100644 index 0000000000..a7e986b611 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/Electronics/loud_speaker.yml @@ -0,0 +1,16 @@ +- type: entity + id: LoudSpeakerElectronics + parent: BaseElectronics + name: loud speaker electronics + description: An electronics board used in loud speakers + components: + - type: Sprite + sprite: Objects/Misc/module.rsi + state: id_mod + - type: Tag + tags: + - DroneUsable + - LoudSpeakerElectronics + - type: ReverseEngineering + recipes: + - LoudSpeakerElectronics diff --git a/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/sound_boxes.yml b/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/sound_boxes.yml new file mode 100644 index 0000000000..be7e04e821 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Entities/Objects/Devices/sound_boxes.yml @@ -0,0 +1,27 @@ +- type: entity + abstract: true + id: SoundBoxBase + parent: BaseItem + name: sound box + description: A small box designed to play a specific audio. It has no way to play on its own. + components: + - type: Sprite + sprite: Objects/Misc/module.rsi + state: id_mod + - type: Tag + tags: + - SoundBox + - type: EmitSoundOnTrigger + sound: Buzzer + +- type: entity + id: SoundBoxBuzzer + parent: SoundBoxBase + name: buzzer sound box + description: A sound box containing a small metal disc that vibrates when electricity is applied to it. + components: + - type: EmitSoundOnTrigger + sound: Buzzer + - type: Sprite + sprite: Objects/Misc/module.rsi + state: id_mod diff --git a/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/loud_speaker.yml b/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/loud_speaker.yml new file mode 100644 index 0000000000..6e61764042 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Entities/Structures/Machines/loud_speaker.yml @@ -0,0 +1,148 @@ +- type: entity + # parent: BaseMachine # ConstructibleMachine Work out dual inheritance + id: LoudSpeakerBase + name: loudspeaker + description: An loudspeaker. For when the station just needs to know something. + components: + - type: WallMount + - type: ApcPowerReceiver + - type: Electrified + enabled: false + usesApcPower: true + - type: ExtensionCableReceiver + - type: Clickable + - type: InteractionOutline + - type: Appearance + - type: Sprite + noRot: false + sprite: Structures/Wallmounts/intercom.rsi + layers: + - state: base + - state: unshaded + map: ["enum.PowerDeviceVisualLayers.Powered"] + # - state: broadcasting + # map: ["enum.RadioDeviceVisualLayers.Broadcasting"] + # shader: unshaded + # visible: false + - type: Transform + noRot: false + anchored: true + # - type: Wires + # BoardName: "LoudSpeaker" + # LayoutId: LoudSpeaker + - type: LoudSpeaker + - type: Construction + graph: LoudSpeaker + node: loudspeaker + containers: + - board + - payload + - type: ContainerContainer + containers: + payload: !type:Container + board: !type:Container + - type: Damageable + damageContainer: Inorganic + damageModifierSet: Metallic + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 160 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] + - trigger: + !type:DamageTrigger + damage: 80 + behaviors: + - !type:PlaySoundBehavior + sound: + path: /Audio/Effects/metalbreak.ogg + - !type:EmptyAllContainersBehaviour + - !type:SpawnEntitiesBehavior + spawn: + SheetSteel1: + min: 1 + max: 2 + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: GenericVisualizer + visuals: + enum.PowerDeviceVisuals.Powered: + enum.PowerDeviceVisualLayers.Powered: + True: { visible: true } + False: { visible: false } + # enum.RadioDeviceVisuals.Broadcasting: + # enum.RadioDeviceVisualLayers.Broadcasting: + # True: { visible: true } + # False: { visible: false } + # placement: + # mode: SnapgridCenter + # snap: + # - Wallmount + +- type: entity + id: LoudSpeakerAssesmbly + name: loudspeaker assembly + description: A loudspeaker. It doesn't seem very helpful right now. + components: + - type: WallMount + - type: Clickable + - type: InteractionOutline + - type: Sprite + sprite: Structures/Wallmounts/intercom.rsi + state: build + - type: Construction + graph: LoudSpeaker + node: assembly + containers: + - board + - payload + - type: Transform + anchored: true + placement: + mode: SnapgridCenter + snap: + - Wallmount + +- type: entity + parent: LoudSpeakerBase + id: LoudSpeakerBuzzer + name: buzzer + description: A buzzer to indicate that someone wants your attention. + components: + - type: ContainerFill + containers: + payload: + - BikeHorn + board: + - LoudSpeakerElectronics + +- type: entity + parent: LoudSpeakerBase + id: LoudSpeakerBuzzerService + suffix: Service + name: buzzer + description: A buzzer to indicate that someone wants your attention. + components: + - type: ContainerFill + containers: + payload: + - DeskBell + board: + - LoudSpeakerElectronics + +- type: entity + parent: LoudSpeakerBase + id: LoudSpeakerSec + suffix: Sec + name: buzzer + description: A buzzer to indicate that someone wants your attention. + components: + - type: ContainerFill + containers: + payload: + - BikeHorn + board: + - LoudSpeakerElectronics diff --git a/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities.yml b/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities.yml new file mode 100644 index 0000000000..b693533466 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities.yml @@ -0,0 +1,17 @@ +- type: construction + name: loudspeaker + id: LoudSpeakerAssesmbly + graph: LoudSpeaker + startNode: start + targetNode: loudspeaker + category: construction-category-structures + description: A loudspeaker. When you want to make sure someone's attention is always ready to be grabbed. + icon: + sprite: Structures/Wallmounts/intercom.rsi + state: base + placementMode: SnapgridCenter + objectType: Structure + canRotate: true + canBuildInImpassable: true + conditions: + - !type:WallmountCondition {} diff --git a/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities/loud_speaker.yml b/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities/loud_speaker.yml new file mode 100644 index 0000000000..3e2428cfc0 --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Recipes/Construction/Graphs/utilities/loud_speaker.yml @@ -0,0 +1,113 @@ +- type: constructionGraph + id: LoudSpeaker + start: start + graph: + - node: start + edges: + - to: assembly + steps: + - material: Steel + amount: 2 + doAfter: 2.0 + + - node: assembly + entity: LoudSpeakerAssesmbly + edges: + - to: wired + steps: + - material: Cable + amount: 2 + doAfter: 1 + - to: start + completed: + - !type:GivePrototype + prototype: SheetSteel1 + amount: 2 + - !type:DeleteEntity {} + steps: + - tool: Welding + doAfter: 2 + + - node: wired + entity: LoudSpeakerAssesmbly + edges: + - to: electronics + steps: + - tag: LoudSpeakerElectronics + store: board + name: "loudspeaker electronics" + icon: + sprite: "Objects/Misc/module.rsi" + state: "id_mod" + doAfter: 1 + - to: assembly + completed: + - !type:GivePrototype + prototype: CableApcStack1 + amount: 2 + steps: + - tool: Cutting + doAfter: 1 + + - node: electronics + entity: LoudSpeakerAssesmbly + edges: + - to: payload + steps: + - size: 8 + store: payload + name: "payload" + compBlacklist: ["Tool"] + - tool: Screwing + doAfter: 3 + - to: payload + steps: + - tool: Screwing + doAfter: 2 + - to: wired + completed: + - !type:EmptyContainer + container: board + steps: + - tool: Prying + doAfter: 1 + + - node: payload + entity: LoudSpeakerAssesmbly + edges: + - to: loudspeaker + steps: + - tool: Screwing + doAfter: 1 + - to: electronics + conditions: + - !type:ContainerNotEmpty + container: board + - !type:ContainerNotEmpty + container: payload + completed: + - !type:EmptyContainer + container: payload + steps: + - tool: Prying + doAfter: 2 + - to: wired + conditions: + - !type:ContainerNotEmpty + container: board + - !type:ContainerEmpty + container: payload + completed: + - !type:EmptyContainer + container: board + steps: + - tool: Prying + doAfter: 1 + + - node: loudspeaker + entity: LoudSpeakerBase + edges: + - to: payload + steps: + - tool: Screwing + doAfter: 1 diff --git a/Resources/Prototypes/SimpleStation14/Recipes/Lathes/electronics.yml b/Resources/Prototypes/SimpleStation14/Recipes/Lathes/electronics.yml new file mode 100644 index 0000000000..db40c13dfe --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/Recipes/Lathes/electronics.yml @@ -0,0 +1,7 @@ +- type: latheRecipe + id: LoudSpeakerElectronics + result: LoudSpeakerElectronics + completetime: 2 + materials: + Steel: 40 + Plastic: 60 diff --git a/Resources/Prototypes/SimpleStation14/SoundCollections/Speakers/buzzer.yml b/Resources/Prototypes/SimpleStation14/SoundCollections/Speakers/buzzer.yml new file mode 100644 index 0000000000..6242ee3e3d --- /dev/null +++ b/Resources/Prototypes/SimpleStation14/SoundCollections/Speakers/buzzer.yml @@ -0,0 +1,5 @@ +- type: soundCollection + id: Buzzer + files: + - /Audio/SimpleStation14/Effects/buzzer_1.ogg + - /Audio/SimpleStation14/Effects/buzzer_2.ogg diff --git a/Resources/Prototypes/SimpleStation14/tags.yml b/Resources/Prototypes/SimpleStation14/tags.yml index ffe8954d1d..5452e9e3f8 100644 --- a/Resources/Prototypes/SimpleStation14/tags.yml +++ b/Resources/Prototypes/SimpleStation14/tags.yml @@ -1,6 +1,12 @@ +- type: Tag + id: SoundBox + - type: Tag id: GlassesNearsight +- type: Tag + id: LoudSpeakerElectronics + - type: Tag id: Plushie