diff --git a/Content.Client/Traits/SingerSystem.cs b/Content.Client/Traits/SingerSystem.cs new file mode 100644 index 00000000000..4a7bd4ca043 --- /dev/null +++ b/Content.Client/Traits/SingerSystem.cs @@ -0,0 +1,13 @@ +using Content.Client.Instruments; +using Content.Shared.Instruments; +using Content.Shared.Traits.Assorted.Systems; + +namespace Content.Client.Traits; + +public sealed class SingerSystem : SharedSingerSystem +{ + protected override SharedInstrumentComponent EnsureInstrumentComp(EntityUid uid) + { + return EnsureComp(uid); // I hate this, but it's the only way. + } +} diff --git a/Content.Server/DeltaV/Harpy/HarpySingerSystem.cs b/Content.Server/DeltaV/Harpy/HarpySingerSystem.cs deleted file mode 100644 index 21b5b3a2456..00000000000 --- a/Content.Server/DeltaV/Harpy/HarpySingerSystem.cs +++ /dev/null @@ -1,182 +0,0 @@ -using Content.Server.Instruments; -using Content.Server.Speech.Components; -using Content.Server.UserInterface; -using Content.Shared.Instruments; -using Content.Shared.Instruments.UI; -using Content.Shared.ActionBlocker; -using Content.Shared.Damage; -using Content.Shared.Damage.ForceSay; -using Content.Shared.DeltaV.Harpy; -using Content.Shared.FixedPoint; -using Content.Shared.Inventory; -using Content.Shared.Inventory.Events; -using Content.Shared.Mobs; -using Content.Shared.Popups; -using Content.Shared.StatusEffect; -using Content.Shared.Stunnable; -using Content.Shared.UserInterface; -using Content.Shared.Zombies; -using Robust.Server.GameObjects; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Content.Shared.DeltaV.Harpy.Components; - -namespace Content.Server.DeltaV.Harpy -{ - public sealed class HarpySingerSystem : EntitySystem - { - [Dependency] private readonly InstrumentSystem _instrument = default!; - [Dependency] private readonly SharedPopupSystem _popupSystem = default!; - [Dependency] private readonly InventorySystem _inventorySystem = default!; - [Dependency] private readonly ActionBlockerSystem _blocker = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnMobStateChangedEvent); - SubscribeLocalEvent(OnEquip); - SubscribeLocalEvent(OnZombified); - SubscribeLocalEvent(OnKnockedDown); - SubscribeLocalEvent(OnStunned); - SubscribeLocalEvent(OnSleep); - SubscribeLocalEvent(OnStatusEffect); - SubscribeLocalEvent(OnDamageChanged); - SubscribeLocalEvent(OnBoundUIClosed); - SubscribeLocalEvent(OnBoundUIOpened); - - // This is intended to intercept the UI event and stop the MIDI UI from opening if the - // singer is unable to sing. Thus it needs to run before the ActivatableUISystem. - SubscribeLocalEvent(OnInstrumentOpen, before: new[] { typeof(ActivatableUISystem) }); - } - - private void OnEquip(GotEquippedEvent args) - { - // Check if an item that makes the singer mumble is equipped to their face - // (not their pockets!). As of writing, this should just be the muzzle. - if (TryComp(args.Equipment, out var accent) && - accent.ReplacementPrototype == "mumble" && - args.Slot == "mask") - { - CloseMidiUi(args.Equipee); - } - } - - private void OnMobStateChangedEvent(EntityUid uid, InstrumentComponent component, MobStateChangedEvent args) - { - if (args.NewMobState is MobState.Critical or MobState.Dead) - CloseMidiUi(args.Target); - } - - private void OnZombified(ref EntityZombifiedEvent args) - { - CloseMidiUi(args.Target); - } - - private void OnKnockedDown(EntityUid uid, InstrumentComponent component, ref KnockedDownEvent args) - { - CloseMidiUi(uid); - } - - private void OnStunned(EntityUid uid, InstrumentComponent component, ref StunnedEvent args) - { - CloseMidiUi(uid); - } - - private void OnSleep(EntityUid uid, InstrumentComponent component, ref SleepStateChangedEvent args) - { - if (args.FellAsleep) - CloseMidiUi(uid); - } - - private void OnStatusEffect(EntityUid uid, InstrumentComponent component, StatusEffectAddedEvent args) - { - if (args.Key == "Muted") - CloseMidiUi(uid); - } - - /// - /// Almost a copy of Content.Server.Damage.ForceSay.DamageForceSaySystem.OnDamageChanged. - /// Done so because DamageForceSaySystem doesn't output an event, and my understanding is - /// that we don't want to change upstream code more than necessary to avoid merge conflicts - /// and maintenance overhead. It still reuses the values from DamageForceSayComponent, so - /// any tweaks to that will keep ForceSay consistent with singing interruptions. - /// - private void OnDamageChanged(EntityUid uid, InstrumentComponent instrumentComponent, DamageChangedEvent args) - { - if (!TryComp(uid, out var component) || - args.DamageDelta == null || - !args.DamageIncreased || - args.DamageDelta.GetTotal() < component.DamageThreshold || - component.ValidDamageGroups == null) - return; - - var totalApplicableDamage = FixedPoint2.Zero; - foreach (var (group, value) in args.DamageDelta.GetDamagePerGroup(_prototype)) - { - if (!component.ValidDamageGroups.Contains(group)) - continue; - - totalApplicableDamage += value; - } - - if (totalApplicableDamage >= component.DamageThreshold) - CloseMidiUi(uid); - } - - /// - /// Closes the MIDI UI if it is open. - /// - private void CloseMidiUi(EntityUid uid) - { - if (HasComp(uid) && - TryComp(uid, out var actor)) - { - _instrument.ToggleInstrumentUi(uid, actor.PlayerSession); - } - } - - /// - /// Prevent the player from opening the MIDI UI under some circumstances. - /// - private void OnInstrumentOpen(EntityUid uid, HarpySingerComponent component, OpenUiActionEvent args) - { - // CanSpeak covers all reasons you can't talk, including being incapacitated - // (crit/dead), asleep, or for any reason mute inclding glimmer or a mime's vow. - var canNotSpeak = !_blocker.CanSpeak(uid); - var zombified = TryComp(uid, out var _); - var muzzled = _inventorySystem.TryGetSlotEntity(uid, "mask", out var maskUid) && - TryComp(maskUid, out var accent) && - accent.ReplacementPrototype == "mumble"; - - // Set this event as handled when the singer should be incapable of singing in order - // to stop the ActivatableUISystem event from opening the MIDI UI. - args.Handled = canNotSpeak || muzzled || zombified; - - // Tell the user that they can not sing. - if (args.Handled) - _popupSystem.PopupEntity(Loc.GetString("no-sing-while-no-speak"), uid, uid, PopupType.Medium); - } - - private void OnBoundUIClosed(EntityUid uid, HarpySingerComponent component, BoundUIClosedEvent args) - { - if (args.UiKey is not InstrumentUiKey) - return; - - TryComp(uid, out AppearanceComponent? appearance); - _appearance.SetData(uid, HarpyVisualLayers.Singing, SingingVisualLayer.False, appearance); - } - - private void OnBoundUIOpened(EntityUid uid, HarpySingerComponent component, BoundUIOpenedEvent args) - { - if (args.UiKey is not InstrumentUiKey) - return; - - TryComp(uid, out AppearanceComponent? appearance); - _appearance.SetData(uid, HarpyVisualLayers.Singing, SingingVisualLayer.True, appearance); - - } - } -} diff --git a/Content.Server/Traits/Assorted/SingerSystem.cs b/Content.Server/Traits/Assorted/SingerSystem.cs new file mode 100644 index 00000000000..7b560957b6e --- /dev/null +++ b/Content.Server/Traits/Assorted/SingerSystem.cs @@ -0,0 +1,161 @@ +using Content.Server.Instruments; +using Content.Server.Speech.Components; +using Content.Server.UserInterface; +using Content.Shared.ActionBlocker; +using Content.Shared.Damage; +using Content.Shared.Damage.ForceSay; +using Content.Shared.FixedPoint; +using Content.Shared.Instruments; +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Mobs; +using Content.Shared.Popups; +using Content.Shared.StatusEffect; +using Content.Shared.Stunnable; +using Content.Shared.Traits.Assorted.Components; +using Content.Shared.Traits.Assorted.Prototypes; +using Content.Shared.Traits.Assorted.Systems; +using Content.Shared.UserInterface; +using Content.Shared.Zombies; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.Traits.Assorted; + +public sealed class SingerSystem : SharedSingerSystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly InventorySystem _inventory = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly InstrumentSystem _instrument = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEquip); + SubscribeLocalEvent(OnMobStateChangedEvent); + SubscribeLocalEvent(OnKnockedDown); + SubscribeLocalEvent(OnStunned); + SubscribeLocalEvent(OnSleep); + SubscribeLocalEvent(OnStatusEffect); + SubscribeLocalEvent(OnDamageChanged); + // This is intended to intercept and cancel the UI event before it reaches ActivatableUISystem. + SubscribeLocalEvent(OnInstrumentOpen, before: [typeof(ActivatableUISystem)]); + } + + protected override SharedInstrumentComponent EnsureInstrumentComp(EntityUid uid) + { + return EnsureComp(uid); + } + + protected override void SetUpSwappableInstrument(EntityUid uid, SingerInstrumentPrototype singer) + { + if (singer.InstrumentList.Count <= 1) + return; + + var swappableComp = EnsureComp(uid); + swappableComp.InstrumentList = singer.InstrumentList; + } + + private void OnEquip(GotEquippedEvent args) + { + // Check if an item that makes the singer mumble is equipped to their face + // (not their pockets!). As of writing, this should just be the muzzle. + if (TryComp(args.Equipment, out var accent) && + accent.ReplacementPrototype == "mumble" && + args.Slot == "mask") + { + CloseMidiUi(args.Equipee); + } + } + + private void OnMobStateChangedEvent(EntityUid uid, SharedInstrumentComponent component, MobStateChangedEvent args) + { + if (args.NewMobState is MobState.Critical or MobState.Dead) + CloseMidiUi(args.Target); + } + + private void OnKnockedDown(EntityUid uid, SharedInstrumentComponent component, ref KnockedDownEvent args) + { + CloseMidiUi(uid); + } + + private void OnStunned(EntityUid uid, SharedInstrumentComponent component, ref StunnedEvent args) + { + CloseMidiUi(uid); + } + + private void OnSleep(EntityUid uid, SharedInstrumentComponent component, ref SleepStateChangedEvent args) + { + if (args.FellAsleep) + CloseMidiUi(uid); + } + + private void OnStatusEffect(EntityUid uid, SharedInstrumentComponent component, StatusEffectAddedEvent args) + { + if (args.Key == "Muted") + CloseMidiUi(uid); + } + + /// + /// Almost a copy of Content.Server.Damage.ForceSay.DamageForceSaySystem.OnDamageChanged. + /// Done so because DamageForceSaySystem doesn't output an event, and my understanding is + /// that we don't want to change upstream code more than necessary to avoid merge conflicts + /// and maintenance overhead. It still reuses the values from DamageForceSayComponent, so + /// any tweaks to that will keep ForceSay consistent with singing interruptions. + /// + private void OnDamageChanged(EntityUid uid, SharedInstrumentComponent instrumentComponent, DamageChangedEvent args) + { + if (!TryComp(uid, out var component) || + args.DamageDelta == null || + !args.DamageIncreased || + args.DamageDelta.GetTotal() < component.DamageThreshold || + component.ValidDamageGroups == null) + return; + + var totalApplicableDamage = FixedPoint2.Zero; + foreach (var (group, value) in args.DamageDelta.GetDamagePerGroup(ProtoMan)) + { + if (!component.ValidDamageGroups.Contains(group)) + continue; + + totalApplicableDamage += value; + } + + if (totalApplicableDamage >= component.DamageThreshold) + CloseMidiUi(uid); + } + + /// + /// Prevent the player from opening the MIDI UI under some circumstances. + /// + private void OnInstrumentOpen(EntityUid uid, SingerComponent component, OpenUiActionEvent args) + { + // CanSpeak covers all reasons you can't talk, including being incapacitated + // (crit/dead), asleep, or for any reason mute inclding glimmer or a mime's vow. + // TODO why the fuck is any of this hardcoded? + var canNotSpeak = !_actionBlocker.CanSpeak(uid); + var zombified = TryComp(uid, out var _); + var muzzled = _inventory.TryGetSlotEntity(uid, "mask", out var maskUid) && + TryComp(maskUid, out var accent) && + accent.ReplacementPrototype == "mumble"; + + // Set this event as handled when the singer should be incapable of singing in order + // to stop the ActivatableUISystem event from opening the MIDI UI. + args.Handled = canNotSpeak || muzzled || zombified; + + // Tell the user that they can not sing. + if (args.Handled) + _popup.PopupEntity(Loc.GetString("no-sing-while-no-speak"), uid, uid, PopupType.Medium); + } + + public override void CloseMidiUi(EntityUid uid) + { + if (HasComp(uid) && + TryComp(uid, out var actor)) + { + _instrument.ToggleInstrumentUi(uid, actor.PlayerSession); + } + } +} diff --git a/Content.Shared/DeltaV/Harpy/HarpySingerComponent.cs b/Content.Shared/DeltaV/Harpy/HarpySingerComponent.cs deleted file mode 100644 index f2edeeb8726..00000000000 --- a/Content.Shared/DeltaV/Harpy/HarpySingerComponent.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; -using Robust.Shared.Serialization; - -namespace Content.Shared.DeltaV.Harpy -{ - [RegisterComponent, NetworkedComponent] - public sealed partial class HarpySingerComponent : Component - { - [DataField("midiActionId", serverOnly: true, - customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? MidiActionId = "ActionHarpyPlayMidi"; - - [DataField("midiAction", serverOnly: true)] // server only, as it uses a server-BUI event !type - public EntityUid? MidiAction; - } -} diff --git a/Content.Shared/DeltaV/Harpy/HarpySingerSystem.cs b/Content.Shared/DeltaV/Harpy/HarpySingerSystem.cs deleted file mode 100644 index 50e8b6302c0..00000000000 --- a/Content.Shared/DeltaV/Harpy/HarpySingerSystem.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Content.Shared.Actions; - -namespace Content.Shared.DeltaV.Harpy -{ - public class HarpySingerSystem : EntitySystem - { - [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnStartup); - SubscribeLocalEvent(OnShutdown); - } - - private void OnStartup(EntityUid uid, HarpySingerComponent component, ComponentStartup args) - { - _actionsSystem.AddAction(uid, ref component.MidiAction, component.MidiActionId); - } - - private void OnShutdown(EntityUid uid, HarpySingerComponent component, ComponentShutdown args) - { - _actionsSystem.RemoveAction(uid, component.MidiAction); - } - } -} - diff --git a/Content.Shared/DeltaV/Harpy/HarpyVisualsSystem.cs b/Content.Shared/DeltaV/Harpy/HarpyVisualsSystem.cs index f75fcee8d42..edab8530228 100644 --- a/Content.Shared/DeltaV/Harpy/HarpyVisualsSystem.cs +++ b/Content.Shared/DeltaV/Harpy/HarpyVisualsSystem.cs @@ -16,11 +16,11 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnDidEquipEvent); - SubscribeLocalEvent(OnDidUnequipEvent); + SubscribeLocalEvent(OnDidEquipEvent); + SubscribeLocalEvent(OnDidUnequipEvent); } - private void OnDidEquipEvent(EntityUid uid, HarpySingerComponent component, DidEquipEvent args) + private void OnDidEquipEvent(EntityUid uid, Traits.Assorted.Components.SingerComponent component, DidEquipEvent args) { if (args.Slot == "outerClothing" && _tagSystem.HasTag(args.Equipment, HarpyWingsTag)) { @@ -29,7 +29,7 @@ private void OnDidEquipEvent(EntityUid uid, HarpySingerComponent component, DidE } } - private void OnDidUnequipEvent(EntityUid uid, HarpySingerComponent component, DidUnequipEvent args) + private void OnDidUnequipEvent(EntityUid uid, Traits.Assorted.Components.SingerComponent component, DidUnequipEvent args) { if (args.Slot == "outerClothing" && _tagSystem.HasTag(args.Equipment, HarpyWingsTag)) { diff --git a/Content.Shared/Traits/Assorted/Components/SingerComponent.cs b/Content.Shared/Traits/Assorted/Components/SingerComponent.cs new file mode 100644 index 00000000000..9c79166ef6c --- /dev/null +++ b/Content.Shared/Traits/Assorted/Components/SingerComponent.cs @@ -0,0 +1,16 @@ +using Content.Shared.Traits.Assorted.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Traits.Assorted.Components; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SingerComponent : Component +{ + // Traits are server-only, and is this is added via traits, it must be replicated to the client. + [DataField(required: true), AutoNetworkedField] + public ProtoId Proto = string.Empty; + + [DataField(serverOnly: true)] + public EntityUid? MidiAction; +} diff --git a/Content.Shared/Traits/Assorted/Prototypes/SingerInstrumentPrototype.cs b/Content.Shared/Traits/Assorted/Prototypes/SingerInstrumentPrototype.cs new file mode 100644 index 00000000000..6a49854f6ea --- /dev/null +++ b/Content.Shared/Traits/Assorted/Prototypes/SingerInstrumentPrototype.cs @@ -0,0 +1,35 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Traits.Assorted.Prototypes; + +[Prototype("SingerInstrument")] +public sealed class SingerInstrumentPrototype : IPrototype +{ + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// Configuration for SwappableInstrumentComponent. + /// string = display name of the instrument + /// byte 1 = instrument midi program + /// byte 2 = instrument midi bank + /// + [DataField(required: true)] + public Dictionary InstrumentList = new(); + + /// + /// Instrument in that is used by default. + /// + [DataField(required: true)] + public string DefaultInstrument = string.Empty; + + /// + /// The BUI configuration for the instrument. + /// + [DataField] + public PrototypeData? MidiUi; + + // The below is server only, as it uses a server-BUI event !type + [DataField(serverOnly: true, required: true)] + public EntProtoId MidiActionId; +} diff --git a/Content.Shared/Traits/Assorted/Systems/SharedSingerSystem.cs b/Content.Shared/Traits/Assorted/Systems/SharedSingerSystem.cs new file mode 100644 index 00000000000..08d0f5c76d0 --- /dev/null +++ b/Content.Shared/Traits/Assorted/Systems/SharedSingerSystem.cs @@ -0,0 +1,93 @@ +using Content.Shared.Actions; +using Content.Shared.DeltaV.Harpy.Components; +using Content.Shared.Instruments; +using Content.Shared.Traits.Assorted.Components; +using Content.Shared.Traits.Assorted.Prototypes; +using Content.Shared.Zombies; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Traits.Assorted.Systems; + +public abstract class SharedSingerSystem : EntitySystem +{ + [Dependency] protected readonly IPrototypeManager ProtoMan = default!; + + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedInstrumentSystem _instrument = default!; + [Dependency] private readonly SharedUserInterfaceSystem _ui = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnZombified); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnBoundUIClosed); + SubscribeLocalEvent(OnBoundUIOpened); + } + + private void OnStartup(Entity ent, ref ComponentStartup args) + { + if (!ProtoMan.TryIndex(ent.Comp.Proto, out var singer)) + return; + + _actionsSystem.AddAction(ent, ref ent.Comp.MidiAction, singer.MidiActionId); + + var instrumentComp = EnsureInstrumentComp(ent); + var defaultData = singer.InstrumentList[singer.DefaultInstrument]; + _instrument.SetInstrumentProgram(instrumentComp, defaultData.Item1, defaultData.Item2); + SetUpSwappableInstrument(ent, singer); + + if (singer.MidiUi is {} uiData && !_ui.TryGetUi(ent, uiData.UiKey, out _)) + _ui.AddUi(ent.Owner, uiData); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + _actionsSystem.RemoveAction(ent, ent.Comp.MidiAction); + } + + private void OnZombified(ref EntityZombifiedEvent args) + { + CloseMidiUi(args.Target); + } + + private void OnBoundUIClosed(EntityUid uid, SingerComponent component, BoundUIClosedEvent args) + { + if (args.UiKey is not InstrumentUiKey) + return; + + TryComp(uid, out AppearanceComponent? appearance); + _appearance.SetData(uid, HarpyVisualLayers.Singing, SingingVisualLayer.False, appearance); + } + + private void OnBoundUIOpened(EntityUid uid, SingerComponent component, BoundUIOpenedEvent args) + { + if (args.UiKey is not InstrumentUiKey) + return; + + TryComp(uid, out AppearanceComponent? appearance); + _appearance.SetData(uid, HarpyVisualLayers.Singing, SingingVisualLayer.True, appearance); + } + + /// + /// Closes the MIDI UI if it is open. Does nothing on client side. + /// + public virtual void CloseMidiUi(EntityUid uid) + { + } + + /// + /// Sets up the swappable instrument on the entity, only on the server. + /// + protected virtual void SetUpSwappableInstrument(EntityUid uid, SingerInstrumentPrototype singer) + { + } + + /// + /// Ensures an InstrumentComponent on the entity. Uses client-side comp on client and server-side comp on the server. + /// + protected abstract SharedInstrumentComponent EnsureInstrumentComp(EntityUid uid); +} diff --git a/Resources/Locale/en-US/traits/traits.ftl b/Resources/Locale/en-US/traits/traits.ftl index 46ee8572a0b..828afa76669 100644 --- a/Resources/Locale/en-US/traits/traits.ftl +++ b/Resources/Locale/en-US/traits/traits.ftl @@ -188,3 +188,5 @@ trait-description-WeaponsGeneralist = You are a jack of all trades with melee weapons, enabling you to be versatile with your weapon arsenal. Your melee damage bonus for all Brute damage types (Blunt, Slash, Piercing) becomes 25%. +trait-name-Singer = Singer +trait-description-Singer = You are naturally capable of singing simple melodies with your voice. \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml index 05d70e8adc5..f4055170b4c 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml @@ -5,30 +5,8 @@ id: MobHarpyBase abstract: true components: - - type: HarpySinger - - type: Instrument - allowPercussion: false - program: 52 - - type: SwappableInstrument - instrumentList: - "Voice": {52: 0} - "Trumpet": {56: 0} - "Electric": {27: 0} - "Bass": {33: 0} - "Rock": {29: 0} - "Acoustic": {24: 0} - "Flute": {73: 0} - "Sax": {66: 0} - - type: UserInterface - interfaces: - - key: enum.InstrumentUiKey.Key - type: InstrumentBoundUserInterface - - key: enum.VoiceMaskUIKey.Key - type: VoiceMaskBoundUserInterface - - key: enum.HumanoidMarkingModifierKey.Key - type: HumanoidMarkingModifierBoundUserInterface - - key: enum.StrippingUiKey.Key - type: StrippableBoundUserInterface + - type: Singer + proto: HarpySinger - type: Sprite scale: 0.9, 0.9 layers: @@ -140,6 +118,7 @@ understands: - GalacticCommon - SolCommon + - type: entity save: false name: Urist McHands diff --git a/Resources/Prototypes/Traits/Misc/singer_types.yml b/Resources/Prototypes/Traits/Misc/singer_types.yml new file mode 100644 index 00000000000..28e4712ee99 --- /dev/null +++ b/Resources/Prototypes/Traits/Misc/singer_types.yml @@ -0,0 +1,26 @@ +- type: SingerInstrument + id: HarpySinger + instrumentList: + "Voice": {52: 0} + "Trumpet": {56: 0} + "Electric": {27: 0} + "Bass": {33: 0} + "Rock": {29: 0} + "Acoustic": {24: 0} + "Flute": {73: 0} + "Sax": {66: 0} + defaultInstrument: "Voice" + midiUi: + key: enum.InstrumentUiKey.Key + type: InstrumentBoundUserInterface + midiActionId: ActionHarpyPlayMidi + +- type: SingerInstrument + id: NormalSinger + instrumentList: + "Voice": {52: 0} + defaultInstrument: "Voice" + midiUi: + key: enum.InstrumentUiKey.Key + type: InstrumentBoundUserInterface + midiActionId: ActionHarpyPlayMidi # TODO: custom action maybe? diff --git a/Resources/Prototypes/Traits/skills.yml b/Resources/Prototypes/Traits/skills.yml index 40198b724ed..6aaaeb4f24f 100644 --- a/Resources/Prototypes/Traits/skills.yml +++ b/Resources/Prototypes/Traits/skills.yml @@ -150,3 +150,16 @@ inverted: true species: - Felinid + +- type: trait + id: Singer + category: Auditory + points: -1 + requirements: + - !type:CharacterSpeciesRequirement + inverted: true + species: + - Harpy + components: + - type: Singer + proto: NormalSinger