diff --git a/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs b/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs new file mode 100644 index 000000000000..33944973b51f --- /dev/null +++ b/Content.Client/Ghost/GhostRoleRadioBoundUserInterface.cs @@ -0,0 +1,29 @@ +using Content.Shared.Ghost.Roles; +using Robust.Client.UserInterface; +using Robust.Shared.Prototypes; + +namespace Content.Client.Ghost; + +public sealed class GhostRoleRadioBoundUserInterface : BoundUserInterface +{ + private GhostRoleRadioMenu? _ghostRoleRadioMenu; + + public GhostRoleRadioBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey) + { + IoCManager.InjectDependencies(this); + } + + protected override void Open() + { + base.Open(); + + _ghostRoleRadioMenu = this.CreateWindow(); + _ghostRoleRadioMenu.SetEntity(Owner); + _ghostRoleRadioMenu.SendGhostRoleRadioMessageAction += SendGhostRoleRadioMessage; + } + + public void SendGhostRoleRadioMessage(ProtoId protoId) + { + SendMessage(new GhostRoleRadioMessage(protoId)); + } +} diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml b/Content.Client/Ghost/GhostRoleRadioMenu.xaml new file mode 100644 index 000000000000..c35ee128c528 --- /dev/null +++ b/Content.Client/Ghost/GhostRoleRadioMenu.xaml @@ -0,0 +1,8 @@ + + + + diff --git a/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs b/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs new file mode 100644 index 000000000000..b05ac3fbddde --- /dev/null +++ b/Content.Client/Ghost/GhostRoleRadioMenu.xaml.cs @@ -0,0 +1,107 @@ +using Content.Client.UserInterface.Controls; +using Content.Shared.Ghost.Roles; +using Content.Shared.Ghost.Roles.Components; +using Robust.Client.GameObjects; +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using System.Numerics; + +namespace Content.Client.Ghost; + +public sealed partial class GhostRoleRadioMenu : RadialMenu +{ + [Dependency] private readonly EntityManager _entityManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + + public event Action>? SendGhostRoleRadioMessageAction; + + public EntityUid Entity { get; set; } + + public GhostRoleRadioMenu() + { + IoCManager.InjectDependencies(this); + RobustXamlLoader.Load(this); + } + + public void SetEntity(EntityUid uid) + { + Entity = uid; + RefreshUI(); + } + + private void RefreshUI() + { + // The main control that will contain all of the clickable options + var main = FindControl("Main"); + + // The purpose of this radial UI is for ghost role radios that allow you to select + // more than one potential option, such as with kobolds/lizards. + // This means that it won't show anything if SelectablePrototypes is empty. + if (!_entityManager.TryGetComponent(Entity, out var comp)) + return; + + foreach (var ghostRoleProtoString in comp.SelectablePrototypes) + { + // For each prototype we find we want to create a button that uses the name of the ghost role + // as the hover tooltip, and the icon is taken from either the ghost role entityprototype + // or the indicated icon entityprototype. + if (!_prototypeManager.TryIndex(ghostRoleProtoString, out var ghostRoleProto)) + continue; + + var button = new GhostRoleRadioMenuButton() + { + StyleClasses = { "RadialMenuButton" }, + SetSize = new Vector2(64, 64), + ToolTip = Loc.GetString(ghostRoleProto.Name), + ProtoId = ghostRoleProto.ID, + }; + + var entProtoView = new EntityPrototypeView() + { + SetSize = new Vector2(48, 48), + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Stretch = SpriteView.StretchMode.Fill + }; + + // pick the icon if it exists, otherwise fallback to the ghost role's entity + if (_prototypeManager.TryIndex(ghostRoleProto.IconPrototype, out var iconProto)) + entProtoView.SetPrototype(iconProto); + else + entProtoView.SetPrototype(comp.Prototype); + + button.AddChild(entProtoView); + main.AddChild(button); + AddGhostRoleRadioMenuButtonOnClickActions(main); + } + } + + private void AddGhostRoleRadioMenuButtonOnClickActions(Control control) + { + var mainControl = control as RadialContainer; + + if (mainControl == null) + return; + + foreach (var child in mainControl.Children) + { + var castChild = child as GhostRoleRadioMenuButton; + + if (castChild == null) + continue; + + castChild.OnButtonUp += _ => + { + SendGhostRoleRadioMessageAction?.Invoke(castChild.ProtoId); + Close(); + }; + } + } +} + +public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButton +{ + public ProtoId ProtoId { get; set; } +} diff --git a/Content.Server/Bible/BibleSystem.cs b/Content.Server/Bible/BibleSystem.cs index 2cb0ac1dd76e..76efe3290bf0 100644 --- a/Content.Server/Bible/BibleSystem.cs +++ b/Content.Server/Bible/BibleSystem.cs @@ -1,11 +1,11 @@ using Content.Server.Bible.Components; -using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Events; using Content.Server.Popups; using Content.Shared.ActionBlocker; using Content.Shared.Actions; using Content.Shared.Bible; using Content.Shared.Damage; +using Content.Shared.Ghost.Roles.Components; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Inventory; diff --git a/Content.Server/Ghost/Roles/GhostRoleSystem.cs b/Content.Server/Ghost/Roles/GhostRoleSystem.cs index b6627f11540f..8b8aa7b8c8fc 100644 --- a/Content.Server/Ghost/Roles/GhostRoleSystem.cs +++ b/Content.Server/Ghost/Roles/GhostRoleSystem.cs @@ -3,7 +3,6 @@ using Content.Server.EUI; using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Events; -using Content.Server.Ghost.Roles.Raffles; using Content.Shared.Ghost.Roles.Raffles; using Content.Server.Ghost.Roles.UI; using Content.Server.Mind.Commands; @@ -31,6 +30,7 @@ using Content.Server.Popups; using Content.Shared.Verbs; using Robust.Shared.Collections; +using Content.Shared.Ghost.Roles.Components; namespace Content.Server.Ghost.Roles { @@ -80,6 +80,7 @@ public override void Initialize() SubscribeLocalEvent(OnSpawnerTakeRole); SubscribeLocalEvent(OnTakeoverTakeRole); SubscribeLocalEvent>(OnVerb); + SubscribeLocalEvent(OnGhostRoleRadioMessage); _playerManager.PlayerStatusChanged += PlayerStatusChanged; } @@ -786,6 +787,21 @@ public void SetMode(EntityUid uid, GhostRolePrototype prototype, string verbText _popupSystem.PopupEntity(msg, uid, userUid.Value); } } + + public void OnGhostRoleRadioMessage(Entity entity, ref GhostRoleRadioMessage args) + { + if (!_prototype.TryIndex(args.ProtoId, out var ghostRoleProto)) + return; + + // if the prototype chosen isn't actually part of the selectable options, ignore it + foreach (var selectableProto in entity.Comp.SelectablePrototypes) + { + if (selectableProto == ghostRoleProto.EntityPrototype.Id) + return; + } + + SetMode(entity.Owner, ghostRoleProto, ghostRoleProto.Name, entity.Comp); + } } [AnyCommand] diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index a8952009e66e..74adea6cd697 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -9,7 +9,6 @@ using Content.Server.Mind; using Content.Server.Mind.Commands; using Content.Server.NPC; -using Content.Server.NPC.Components; using Content.Server.NPC.HTN; using Content.Server.NPC.Systems; using Content.Server.Roles; @@ -24,10 +23,8 @@ using Content.Shared.Interaction.Components; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; -using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Pulling.Components; using Content.Shared.Movement.Systems; -using Content.Shared.NPC.Components; using Content.Shared.NPC.Systems; using Content.Shared.Nutrition.AnimalHusbandry; using Content.Shared.Nutrition.Components; @@ -38,6 +35,7 @@ using Content.Shared.Prying.Components; using Content.Shared.Traits.Assorted; using Robust.Shared.Audio.Systems; +using Content.Shared.Ghost.Roles.Components; namespace Content.Server.Zombies { diff --git a/Content.Shared/Ghost/GhostRoleRadioEvents.cs b/Content.Shared/Ghost/GhostRoleRadioEvents.cs new file mode 100644 index 000000000000..8bdd6e583755 --- /dev/null +++ b/Content.Shared/Ghost/GhostRoleRadioEvents.cs @@ -0,0 +1,21 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Ghost.Roles; + +[Serializable, NetSerializable] +public sealed class GhostRoleRadioMessage : BoundUserInterfaceMessage +{ + public ProtoId ProtoId; + + public GhostRoleRadioMessage(ProtoId protoId) + { + ProtoId = protoId; + } +} + +[Serializable, NetSerializable] +public enum GhostRoleRadioUiKey : byte +{ + Key +} diff --git a/Content.Server/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs b/Content.Shared/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs similarity index 85% rename from Content.Server/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs rename to Content.Shared/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs index 6116173f904b..2e44effad965 100644 --- a/Content.Server/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs +++ b/Content.Shared/Ghost/Roles/Components/GhostRoleMobSpawnerComponent.cs @@ -1,12 +1,11 @@ -using Robust.Shared.Prototypes; +using Robust.Shared.Prototypes; -namespace Content.Server.Ghost.Roles.Components +namespace Content.Shared.Ghost.Roles.Components { /// /// Allows a ghost to take this role, spawning a new entity. /// [RegisterComponent, EntityCategory("Spawner")] - [Access(typeof(GhostRoleSystem))] public sealed partial class GhostRoleMobSpawnerComponent : Component { [DataField] diff --git a/Content.Shared/Ghost/Roles/GhostRolePrototype.cs b/Content.Shared/Ghost/Roles/GhostRolePrototype.cs index bc36774ea8ba..3e81b5e46e8e 100644 --- a/Content.Shared/Ghost/Roles/GhostRolePrototype.cs +++ b/Content.Shared/Ghost/Roles/GhostRolePrototype.cs @@ -30,6 +30,13 @@ public sealed partial class GhostRolePrototype : IPrototype [DataField(required: true)] public EntProtoId EntityPrototype; + /// + /// The entity prototype's sprite to use to represent the ghost role + /// Use this if you don't want to use the entity itself + /// + [DataField] + public EntProtoId? IconPrototype = null; + /// /// Rules of the ghostrole /// diff --git a/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl b/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl index 98f31e0ab074..b2082f772805 100644 --- a/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl +++ b/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl @@ -197,6 +197,16 @@ ghost-role-information-syndicate-reinforcement-name = Syndicate Agent ghost-role-information-syndicate-reinforcement-description = Someone needs reinforcements. You, the first person the syndicate could find, will help them. ghost-role-information-syndicate-reinforcement-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with the agent who summoned you. +ghost-role-information-syndicate-reinforcement-medic-name = Syndicate Medic +ghost-role-information-syndicate-reinforcement-medic-description = Someone needs reinforcements. Your task is to keep the agent who called you alive. + +ghost-role-information-syndicate-reinforcement-spy-name = Syndicate Spy +ghost-role-information-syndicate-reinforcement-spy-description = Someone needs reinforcements. Your speciality lies in espionage, do not be discovered. + +ghost-role-information-syndicate-reinforcement-thief-name = Syndicate Thief +ghost-role-information-syndicate-reinforcement-thief-description = Someone needs reinforcements. Your job is to break in and retrieve something valuable for your agent. + + ghost-role-information-syndicate-monkey-reinforcement-name = Syndicate Monkey Agent ghost-role-information-syndicate-monkey-reinforcement-description = Someone needs reinforcements. You, a trained monkey, will help them. ghost-role-information-syndicate-monkey-reinforcement-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with the agent who summoned you. diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl index 8edbde9bdc9d..85a64c71ae18 100644 --- a/Resources/Locale/en-US/store/uplink-catalog.ftl +++ b/Resources/Locale/en-US/store/uplink-catalog.ftl @@ -124,8 +124,10 @@ uplink-black-jetpack-desc = A black jetpack. It allows you to fly around in spac uplink-reinforcement-radio-ancestor-name = Genetic Ancestor Reinforcement Teleporter uplink-reinforcement-radio-ancestor-desc = Call in a trained ancestor of your choosing to assist you. Comes with a single syndicate cigarette. + uplink-reinforcement-radio-name = Reinforcement Teleporter -uplink-reinforcement-radio-desc = Radio in a reinforcement agent of extremely questionable quality. No off button, buy this if you're ready to party. They have a pistol with no reserve ammo, and a knife. That's it. +uplink-reinforcement-radio-traitor-desc = Radio in a reinforcement agent of extremely questionable quality. No off button, buy this if you're ready to party. Call in a medic or spy or thief to help you out. Good luck. +uplink-reinforcement-radio-nukeops-desc = Radio in a reinforcement agent of extremely questionable quality. No off button, buy this if you're ready to party. They have a pistol with no reserve ammo, and a knife. That's it. uplink-reinforcement-radio-cyborg-assault-name = Syndicate Assault Cyborg Teleporter uplink-reinforcement-radio-cyborg-assault-desc = A lean, mean killing machine with access to an Energy Sword, LMG, Cryptographic Sequencer, and a Pinpointer. diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml index 420982dcaecd..fe8b038dd924 100644 --- a/Resources/Prototypes/Catalog/uplink_catalog.yml +++ b/Resources/Prototypes/Catalog/uplink_catalog.yml @@ -902,7 +902,7 @@ - type: listing id: UplinkReinforcementRadioSyndicate name: uplink-reinforcement-radio-name - description: uplink-reinforcement-radio-desc + description: uplink-reinforcement-radio-traitor-desc productEntity: ReinforcementRadioSyndicate icon: { sprite: Objects/Devices/communication.rsi, state: old-radio-urist } cost: @@ -918,7 +918,7 @@ - type: listing id: UplinkReinforcementRadioSyndicateNukeops # Version for Nukeops that spawns an agent with the NukeOperative component. name: uplink-reinforcement-radio-name - description: uplink-reinforcement-radio-desc + description: uplink-reinforcement-radio-nukeops-desc productEntity: ReinforcementRadioSyndicateNukeops icon: { sprite: Objects/Devices/communication.rsi, state: old-radio-urist } cost: diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index cb0203ebe0e2..0b718746e13e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -33,6 +33,30 @@ giveUplink: false giveObjectives: false +- type: entity + parent: MobHumanSyndicateAgent + id: MobHumanSyndicateAgentMedic + name: syndicate medic + components: + - type: Loadout + prototypes: [SyndicateReinforcementMedic] + +- type: entity + parent: MobHumanSyndicateAgent + id: MobHumanSyndicateAgentSpy + name: syndicate spy + components: + - type: Loadout + prototypes: [SyndicateReinforcementSpy] + +- type: entity + parent: MobHumanSyndicateAgent + id: MobHumanSyndicateAgentThief + name: syndicate thief + components: + - type: Loadout + prototypes: [SyndicateReinforcementThief] + - type: entity parent: MobHumanSyndicateAgentBase id: MobHumanSyndicateAgentNukeops # Reinforcement exclusive to nukeops uplink diff --git a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml index d49c79f48b3c..6aa2686fa617 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml @@ -1,6 +1,7 @@ - type: entity parent: BaseItem - id: ReinforcementRadioSyndicate + abstract: true + id: ReinforcementRadio name: syndicate reinforcement radio description: Call in a syndicate agent of questionable quality, instantly! Only basic equipment provided. components: @@ -8,29 +9,43 @@ sprite: Objects/Devices/communication.rsi layers: - state: old-radio + - type: UserInterface + interfaces: + enum.GhostRoleRadioUiKey.Key: + type: GhostRoleRadioBoundUserInterface + - type: ActivatableUI + key: enum.GhostRoleRadioUiKey.Key + +- type: entity + parent: ReinforcementRadio + id: ReinforcementRadioSyndicate + name: syndicate reinforcement radio + description: Call in a syndicate agent of questionable quality, instantly! + components: - type: GhostRole - name: ghost-role-information-syndicate-reinforcement-name + name: ghost-role-information-syndicate-reinforcement-spy-name description: ghost-role-information-syndicate-reinforcement-description rules: ghost-role-information-syndicate-reinforcement-rules raffle: settings: default - type: GhostRoleMobSpawner - prototype: MobHumanSyndicateAgent - - type: EmitSoundOnUse - sound: /Audio/Effects/Emotes/parp1.ogg - - type: UseDelay - delay: 300 + prototype: MobHumanSyndicateAgentSpy + selectablePrototypes: ["SyndicateAgentMedic", "SyndicateAgentSpy", "SyndicateAgentThief"] - type: entity - parent: ReinforcementRadioSyndicate + parent: ReinforcementRadio id: ReinforcementRadioSyndicateNukeops # Reinforcement radio exclusive to nukeops uplink suffix: NukeOps components: + - type: GhostRole + name: ghost-role-information-syndicate-reinforcement-name + description: ghost-role-information-syndicate-reinforcement-description + rules: ghost-role-information-syndicate-reinforcement-rules - type: GhostRoleMobSpawner prototype: MobHumanSyndicateAgentNukeops - type: entity - parent: ReinforcementRadioSyndicate + parent: ReinforcementRadio id: ReinforcementRadioSyndicateAncestor name: syndicate genetic ancestor reinforcement radio description: Calls in a specially trained ancestor of your choosing to assist you. @@ -55,7 +70,7 @@ selectablePrototypes: ["SyndicateMonkeyNukeops", "SyndicateKoboldNukeops"] - type: entity - parent: ReinforcementRadioSyndicate + parent: ReinforcementRadio id: ReinforcementRadioSyndicateSyndiCat name: syndicat reinforcement radio description: Calls in a faithfully trained cat with a microbomb to assist you. @@ -72,7 +87,7 @@ sound: /Audio/Animals/cat_meow.ogg - type: entity - parent: ReinforcementRadioSyndicate + parent: ReinforcementRadio id: ReinforcementRadioSyndicateCyborgAssault # Reinforcement radio exclusive to nukeops uplink name: syndicate assault cyborg reinforcement radio description: Call in a well armed assault cyborg, instantly! diff --git a/Resources/Prototypes/Roles/Antags/traitor.yml b/Resources/Prototypes/Roles/Antags/traitor.yml index 59390d2b103c..feb810973905 100644 --- a/Resources/Prototypes/Roles/Antags/traitor.yml +++ b/Resources/Prototypes/Roles/Antags/traitor.yml @@ -36,6 +36,48 @@ - PinpointerSyndicateNuclear - DeathAcidifierImplanter +- type: startingGear + id: SyndicateOperativeClothing + equipment: + jumpsuit: ClothingUniformJumpsuitOperative + back: ClothingBackpackSyndicate + shoes: ClothingShoesBootsCombatFilled + gloves: ClothingHandsGlovesColorBlack + +- type: startingGear + id: SyndicateReinforcementMedic + parent: SyndicateOperativeClothing + equipment: + pocket1: WeaponPistolViper + inhand: + - MedkitCombatFilled + storage: + back: + - BoxSurvivalSyndicate + +- type: startingGear + id: SyndicateReinforcementSpy + parent: SyndicateOperativeClothing + equipment: + id: AgentIDCard + mask: ClothingMaskGasVoiceChameleon + pocket1: WeaponPistolViper + storage: + back: + - BoxSurvivalSyndicate + +- type: startingGear + id: SyndicateReinforcementThief + parent: SyndicateOperativeClothing + equipment: + pocket1: WeaponPistolViper + inhand: + - ToolboxSyndicateFilled + storage: + back: + - BoxSurvivalSyndicate + - SyndicateJawsOfLife + # Syndicate Reinforcement NukeOps - type: startingGear id: SyndicateOperativeGearReinforcementNukeOps @@ -43,6 +85,7 @@ equipment: id: SyndiPDA #Do not give a PDA to the normal Reinforcement - it will spawn with a 20TC uplink + #Syndicate Operative Outfit - Basic - type: startingGear id: SyndicateOperativeGearBasic diff --git a/Resources/Prototypes/Roles/Ghostroles/syndicate.yml b/Resources/Prototypes/Roles/Ghostroles/syndicate.yml index 24c0d8b3e3be..8e1827e81bf1 100644 --- a/Resources/Prototypes/Roles/Ghostroles/syndicate.yml +++ b/Resources/Prototypes/Roles/Ghostroles/syndicate.yml @@ -24,4 +24,28 @@ name: ghost-role-information-syndicate-monkey-reinforcement-name description: ghost-role-information-syndicate-monkey-reinforcement-description rules: ghost-role-information-syndicate-monkey-reinforcement-name - entityPrototype: MobMonkeySyndicateAgentNukeops \ No newline at end of file + entityPrototype: MobMonkeySyndicateAgentNukeops + +- type: ghostRole + id: SyndicateAgentMedic + name: ghost-role-information-syndicate-reinforcement-medic-name + description: ghost-role-information-syndicate-reinforcement-medic-description + rules: ghost-role-information-syndicate-monkey-reinforcement-rules + entityPrototype: MobHumanSyndicateAgentMedic + iconPrototype: MedkitCombat + +- type: ghostRole + id: SyndicateAgentSpy + name: ghost-role-information-syndicate-reinforcement-spy-name + description: ghost-role-information-syndicate-reinforcement-spy-description + rules: ghost-role-information-syndicate-monkey-reinforcement-rules + entityPrototype: MobHumanSyndicateAgentSpy + iconPrototype: ClothingMaskGasVoiceChameleon + +- type: ghostRole + id: SyndicateAgentThief + name: ghost-role-information-syndicate-reinforcement-thief-name + description: ghost-role-information-syndicate-reinforcement-thief-description + rules: ghost-role-information-syndicate-monkey-reinforcement-rules + entityPrototype: MobHumanSyndicateAgentThief + iconPrototype: SyndicateJawsOfLife