diff --git a/Content.Server/DeltaV/Implants/SubdermalBionicSyrinxImplantSystem.cs b/Content.Server/DeltaV/Implants/SubdermalBionicSyrinxImplantSystem.cs new file mode 100644 index 00000000000..65678668d7d --- /dev/null +++ b/Content.Server/DeltaV/Implants/SubdermalBionicSyrinxImplantSystem.cs @@ -0,0 +1,113 @@ +using Content.Server.Administration.Logs; +using Content.Server.Chat.Systems; +using Content.Server.Popups; +using Content.Server.VoiceMask; +using Content.Shared.Database; +using Content.Shared.Implants; +using Content.Shared.Implants.Components; +using Content.Shared.Popups; +using Content.Shared.Preferences; +using Content.Shared.Tag; +using Content.Shared.VoiceMask; +using Robust.Server.GameObjects; +using Robust.Shared.Containers; + +namespace Content.Server.Implants; + +public sealed class SubdermalBionicSyrinxImplantSystem : EntitySystem +{ + [Dependency] private readonly UserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly TagSystem _tag = default!; + + [ValidatePrototypeId] + public const string BionicSyrinxImplant = "BionicSyrinxImplant"; + + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInsert); + SubscribeLocalEvent(OnSpeakerNameTransform); + SubscribeLocalEvent(OnChangeName); + // We need to remove the SyrinxVoiceMaskComponent from the owner before the implant + // is removed, so we need to execute before the SubdermalImplantSystem. + SubscribeLocalEvent(OnRemove, before: new[] { typeof(SubdermalImplantSystem) }); + } + + private void OnInsert(EntityUid uid, VoiceMaskerComponent component, ImplantImplantedEvent args) + { + if (!args.Implanted.HasValue || + !_tag.HasTag(args.Implant, BionicSyrinxImplant)) + return; + + var voicemask = EnsureComp(args.Implanted.Value); + voicemask.VoiceName = MetaData(args.Implanted.Value).EntityName; + Dirty(args.Implanted.Value, voicemask); + } + + private void OnRemove(EntityUid uid, VoiceMaskerComponent component, EntGotRemovedFromContainerMessage args) + { + if (!TryComp(uid, out var implanted) || implanted.ImplantedEntity == null) + return; + + RemComp(implanted.ImplantedEntity.Value); + } + + /// + /// Copy from VoiceMaskSystem, adapted to work with SyrinxVoiceMaskComponent. + /// + private void OnChangeName(EntityUid uid, SyrinxVoiceMaskComponent component, VoiceMaskChangeNameMessage message) + { + if (message.Name.Length > HumanoidCharacterProfile.MaxNameLength || message.Name.Length <= 0) + { + _popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-failure"), uid, message.Session, PopupType.SmallCaution); + return; + } + + component.VoiceName = message.Name; + if (message.Session.AttachedEntity != null) + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(message.Session.AttachedEntity.Value):player} set voice of {ToPrettyString(uid):mask}: {component.VoiceName}"); + else + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"Voice of {ToPrettyString(uid):mask} set: {component.VoiceName}"); + + _popupSystem.PopupEntity(Loc.GetString("voice-mask-popup-success"), uid, message.Session); + TrySetLastKnownName(uid, message.Name); + UpdateUI(uid, component); + } + + /// + /// Copy from VoiceMaskSystem, adapted to work with SyrinxVoiceMaskComponent. + /// + private void TrySetLastKnownName(EntityUid implanted, string lastName) + { + if (!HasComp(implanted) + || !TryComp(implanted, out var maskComp)) + return; + + maskComp.LastSetName = lastName; + } + + /// + /// Copy from VoiceMaskSystem, adapted to work with SyrinxVoiceMaskComponent. + /// + private void UpdateUI(EntityUid owner, SyrinxVoiceMaskComponent? component = null) + { + if (!Resolve(owner, ref component, logMissing: false)) + return; + + if (_uiSystem.TryGetUi(owner, VoiceMaskUIKey.Key, out var bui)) + _uiSystem.SetUiState(bui, new VoiceMaskBuiState(component.VoiceName)); + } + + /// + /// Copy from VoiceMaskSystem, adapted to work with SyrinxVoiceMaskComponent. + /// + private void OnSpeakerNameTransform(EntityUid uid, SyrinxVoiceMaskComponent component, TransformSpeakerNameEvent args) + { + if (component.Enabled) + args.Name = component.VoiceName; + } +} diff --git a/Content.Server/DeltaV/VoiceMask/SyrinxVoiceMaskComponent.cs b/Content.Server/DeltaV/VoiceMask/SyrinxVoiceMaskComponent.cs new file mode 100644 index 00000000000..97c04039f92 --- /dev/null +++ b/Content.Server/DeltaV/VoiceMask/SyrinxVoiceMaskComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.VoiceMask; + +[RegisterComponent] +public sealed partial class SyrinxVoiceMaskComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] public bool Enabled = true; + + [ViewVariables(VVAccess.ReadWrite)] public string VoiceName = "Unknown"; +} diff --git a/Content.Server/VoiceMask/VoiceMaskSystem.cs b/Content.Server/VoiceMask/VoiceMaskSystem.cs index 8e1c2c66f2f..7b86bb17dc5 100644 --- a/Content.Server/VoiceMask/VoiceMaskSystem.cs +++ b/Content.Server/VoiceMask/VoiceMaskSystem.cs @@ -76,7 +76,8 @@ private void OnMaskToggled(Entity ent, ref WearerMaskToggled private void OpenUI(EntityUid player, ActorComponent? actor = null) { - if (!Resolve(player, ref actor)) + // Delta-V: `logMissing: false` because of syrinx. + if (!Resolve(player, ref actor, logMissing: false)) return; if (!_uiSystem.TryGetUi(player, VoiceMaskUIKey.Key, out var bui)) return; @@ -87,7 +88,8 @@ private void OpenUI(EntityUid player, ActorComponent? actor = null) private void UpdateUI(EntityUid owner, VoiceMaskComponent? component = null) { - if (!Resolve(owner, ref component)) + // Delta-V: `logMissing: false` because of syrinx + if (!Resolve(owner, ref component, logMissing: false)) { return; } diff --git a/Resources/Locale/en-US/deltav/store/uplink-catalog.ftl b/Resources/Locale/en-US/deltav/store/uplink-catalog.ftl new file mode 100644 index 00000000000..448ca88151d --- /dev/null +++ b/Resources/Locale/en-US/deltav/store/uplink-catalog.ftl @@ -0,0 +1,2 @@ +uplink-bionic-syrinx-implanter-name = Bionic Syrinx Implanter +uplink-bionic-syrinx-implanter-desc = An implant that enhances a harpy's natural talent for mimicry to let you adjust your voice to whoever you can think of. diff --git a/Resources/Prototypes/DeltaV/Catalog/uplink_catalog.yml b/Resources/Prototypes/DeltaV/Catalog/uplink_catalog.yml index a32f57beed8..8122c6165b2 100644 --- a/Resources/Prototypes/DeltaV/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/DeltaV/Catalog/uplink_catalog.yml @@ -17,3 +17,17 @@ # blacklist: # components: # - SurplusBundle + +- type: listing + id: UplinkBionicSyrinxImplanter + name: uplink-bionic-syrinx-implanter-name + description: uplink-bionic-syrinx-implanter-desc + productEntity: BionicSyrinxImplanter + cost: + Telecrystal: 2 + categories: + - UplinkImplants + conditions: + - !type:BuyerSpeciesCondition + whitelist: + - Harpy diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml index c2a2304a046..94b5c0c9355 100644 --- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/harpy.yml @@ -175,3 +175,14 @@ icon: DeltaV/Interface/Actions/harpy_sing.png event: !type:OpenUiActionEvent key: enum.InstrumentUiKey.Key + +- type: entity + id: ActionSyrinxChangeVoiceMask + name: Set name + description: Change the name others hear to something else. + noSpawn: true + components: + - type: InstantAction + icon: DeltaV/Interface/Actions/harpy_syrinx.png + itemIconStyle: BigAction + event: !type:VoiceMaskSetNameEvent diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Misc/implanters.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Misc/implanters.yml new file mode 100644 index 00000000000..9849642f939 --- /dev/null +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Misc/implanters.yml @@ -0,0 +1,7 @@ +- type: entity + id: BionicSyrinxImplanter + name: bionic syrinx implanter + parent: BaseImplantOnlyImplanterSyndi + components: + - type: Implanter + implant: BionicSyrinxImplant diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Misc/subdermal_implants.yml new file mode 100644 index 00000000000..93a9641f7be --- /dev/null +++ b/Resources/Prototypes/DeltaV/Entities/Objects/Misc/subdermal_implants.yml @@ -0,0 +1,17 @@ +- type: entity + parent: BaseSubdermalImplant + id: BionicSyrinxImplant + name: bionic syrinx implant + description: This implant lets a harpy adjust their voice to whoever they can think of. + noSpawn: true + components: + - type: SubdermalImplant + implantAction: ActionSyrinxChangeVoiceMask + whitelist: + components: + - HarpySinger + - type: VoiceMasker + - type: Tag + tags: + - SubdermalImplant + - BionicSyrinxImplant diff --git a/Resources/Prototypes/DeltaV/tags.yml b/Resources/Prototypes/DeltaV/tags.yml index 7b09bfd3dcd..0f003358262 100644 --- a/Resources/Prototypes/DeltaV/tags.yml +++ b/Resources/Prototypes/DeltaV/tags.yml @@ -26,3 +26,6 @@ - type: Tag id: PreventLabel + +- type: Tag + id: BionicSyrinxImplant diff --git a/Resources/Textures/DeltaV/Interface/Actions/harpy_syrinx.png b/Resources/Textures/DeltaV/Interface/Actions/harpy_syrinx.png new file mode 100644 index 00000000000..d09b090fd37 Binary files /dev/null and b/Resources/Textures/DeltaV/Interface/Actions/harpy_syrinx.png differ