diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index 4e8ae5f97f..11f96b3c48 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -56,6 +56,7 @@ namespace Content.Client.Entry "Electrified", "Electrocution", "Paper", + "Drone", "Bloodstream", "TransformableContainer", "Mind", diff --git a/Content.Server/Actions/Actions/DisarmAction.cs b/Content.Server/Actions/Actions/DisarmAction.cs index 7e9ac324a6..07d2e69af5 100644 --- a/Content.Server/Actions/Actions/DisarmAction.cs +++ b/Content.Server/Actions/Actions/DisarmAction.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using Content.Server.Act; +using Content.Server.Actions.Events; using Content.Server.Administration.Logs; using Content.Server.Interaction; using Content.Server.Popups; @@ -44,11 +45,16 @@ namespace Content.Server.Actions.Actions [ViewVariables] [DataField("disarmSuccessSound")] private SoundSpecifier DisarmSuccessSound { get; } = new SoundPathSpecifier("/Audio/Effects/thudswoosh.ogg"); - public void DoTargetEntityAction(TargetEntityActionEventArgs args) { var entMan = IoCManager.Resolve(); var disarmedActs = entMan.GetComponents(args.Target).ToArray(); + var attemptEvent = new DisarmAttemptEvent(args.Target, args.Performer); + + entMan.EventBus.RaiseLocalEvent(args.Target, attemptEvent); + + if (attemptEvent.Cancelled) + return; if (!args.Performer.InRangeUnobstructed(args.Target)) return; diff --git a/Content.Server/Actions/Events/DisarmAttemptEvent.cs b/Content.Server/Actions/Events/DisarmAttemptEvent.cs new file mode 100644 index 0000000000..2a5d167209 --- /dev/null +++ b/Content.Server/Actions/Events/DisarmAttemptEvent.cs @@ -0,0 +1,15 @@ +using Robust.Shared.GameObjects; + +namespace Content.Server.Actions.Events +{ + public class DisarmAttemptEvent : CancellableEntityEventArgs + { + public readonly EntityUid TargetUid; + public readonly EntityUid DisarmerUid; + public DisarmAttemptEvent(EntityUid targetUid, EntityUid disarmerUid) + { + TargetUid = targetUid; + DisarmerUid = disarmerUid; + } + } +} diff --git a/Content.Server/Drone/Components/DroneComponent.cs b/Content.Server/Drone/Components/DroneComponent.cs new file mode 100644 index 0000000000..785552a4d6 --- /dev/null +++ b/Content.Server/Drone/Components/DroneComponent.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using Content.Server.Storage; +using Robust.Shared.Analyzers; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + + +namespace Content.Server.Drone.Components +{ + [RegisterComponent] + public sealed class DroneComponent : Component + { + [DataField("tools")] public List Tools = new(); + public List ToolUids = new(); + public bool AlreadyAwoken = false; + } +} diff --git a/Content.Server/Drone/DroneSystem.cs b/Content.Server/Drone/DroneSystem.cs new file mode 100644 index 0000000000..9db870f0a9 --- /dev/null +++ b/Content.Server/Drone/DroneSystem.cs @@ -0,0 +1,132 @@ +using Content.Shared.Drone; +using Content.Server.Drone.Components; +using Content.Shared.Drone.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory.Events; +using Content.Shared.MobState.Components; +using Content.Shared.MobState; +using Content.Shared.DragDrop; +using Content.Shared.Examine; +using Content.Server.Popups; +using Content.Server.Mind.Components; +using Content.Server.Ghost.Roles.Components; +using Content.Server.Hands.Components; +using Content.Shared.Body.Components; +using Content.Server.Actions.Events; +using Robust.Shared.IoC; +using Robust.Shared.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Player; +using Content.Shared.Tag; + +namespace Content.Server.Drone +{ + public class DroneSystem : SharedDroneSystem + { + [Dependency] private readonly PopupSystem _popupSystem = default!; + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnDisarmAttempt); + SubscribeLocalEvent(OnDropAttempt); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnMindAdded); + SubscribeLocalEvent(OnMindRemoved); + } + + private void OnExamined(EntityUid uid, DroneComponent component, ExaminedEvent args) + { + if (args.IsInDetailsRange) + { + if (TryComp(uid, out var mind) && mind.HasMind) + { + args.PushMarkup(Loc.GetString("drone-active")); + } + else + { + args.PushMarkup(Loc.GetString("drone-dormant")); + } + } + } + + private void OnMobStateChanged(EntityUid uid, DroneComponent drone, MobStateChangedEvent args) + { + if (args.Component.IsDead()) + { + var body = Comp(uid); //There's no way something can have a mobstate but not a body... + + foreach (var item in drone.ToolUids) + { + EntityManager.DeleteEntity(item); + } + body.Gib(); + EntityManager.DeleteEntity(uid); + } + } + + private void OnDisarmAttempt(EntityUid uid, DroneComponent drone, DisarmAttemptEvent args) + { + TryComp(args.TargetUid, out var hands); + var item = hands?.GetActiveHandItem; + if (TryComp(item?.Owner, out var itemInHand)) + { + args.Cancel(); + } + } + + private void OnMindAdded(EntityUid uid, DroneComponent drone, MindAddedMessage args) + { + TryComp(uid, out var tagComp); + UpdateDroneAppearance(uid, DroneStatus.On); + tagComp?.AddTag("DoorBumpOpener"); + _popupSystem.PopupEntity(Loc.GetString("drone-activated"), uid, Filter.Pvs(uid)); + + if (drone.AlreadyAwoken == false) + { + var spawnCoord = Transform(uid).Coordinates; + + if (drone.Tools.Count == 0) return; + + if (TryComp(uid, out var hands) && hands.Count >= drone.Tools.Count) + { + foreach (var entry in drone.Tools) + { + var item = EntityManager.SpawnEntity(entry.PrototypeId, spawnCoord); + AddComp(item); + hands.PutInHand(item); + drone.ToolUids.Add(item); + } + } + + drone.AlreadyAwoken = true; + } + } + + private void OnMindRemoved(EntityUid uid, DroneComponent drone, MindRemovedMessage args) + { + TryComp(uid, out var tagComp); + UpdateDroneAppearance(uid, DroneStatus.Off); + tagComp?.RemoveTag("DoorBumpOpener"); + EnsureComp(uid); + } + + private void OnDropAttempt(EntityUid uid, DroneComponent drone, DropAttemptEvent args) + { + TryComp(uid, out var hands); + var item = hands?.GetActiveHandItem; + if (TryComp(item?.Owner, out var itemInHand)) + { + args.Cancel(); + } + } + + private void UpdateDroneAppearance(EntityUid uid, DroneStatus status) + { + if (TryComp(uid, out var appearance)) + { + appearance.SetData(DroneVisuals.Status, status); + } + } + } +} diff --git a/Content.Shared/Drone/Components/DroneToolComponent.cs b/Content.Shared/Drone/Components/DroneToolComponent.cs new file mode 100644 index 0000000000..f25955e827 --- /dev/null +++ b/Content.Shared/Drone/Components/DroneToolComponent.cs @@ -0,0 +1,8 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Analyzers; + +namespace Content.Shared.Drone.Components +{ + [RegisterComponent] + public sealed class DroneToolComponent : Component {} +} diff --git a/Content.Shared/Drone/SharedDroneSystem.cs b/Content.Shared/Drone/SharedDroneSystem.cs new file mode 100644 index 0000000000..e29534d3f3 --- /dev/null +++ b/Content.Shared/Drone/SharedDroneSystem.cs @@ -0,0 +1,22 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.Drone +{ + public abstract class SharedDroneSystem : EntitySystem + { + [Serializable, NetSerializable] + public enum DroneVisuals : byte + { + Status + } + + [Serializable, NetSerializable] + public enum DroneStatus : byte + { + Off, + On + } + } +} diff --git a/Resources/Locale/en-US/drone/drone-system.ftl b/Resources/Locale/en-US/drone/drone-system.ftl new file mode 100644 index 0000000000..68000da268 --- /dev/null +++ b/Resources/Locale/en-US/drone/drone-system.ftl @@ -0,0 +1,3 @@ +drone-active = A maintenance drone. It seems totally unconcerned with you. +drone-dormant = A dormant maintenance drone. Who knows when it will wake up? +drone-activated = The drone whirrs to life! diff --git a/Resources/Prototypes/Body/Parts/silicon.yml b/Resources/Prototypes/Body/Parts/silicon.yml new file mode 100644 index 0000000000..1a0b6be4f4 --- /dev/null +++ b/Resources/Prototypes/Body/Parts/silicon.yml @@ -0,0 +1,50 @@ +- type: entity + id: PartSilicon + parent: BaseItem + name: "silicon body part" + abstract: true + components: + - type: Damageable + damageContainer: Inorganic + +- type: entity + id: LeftHandDrone + name: "left drone hand" + parent: PartSilicon + components: + - type: Sprite + netsync: false + sprite: Mobs/Silicon/drone.rsi + state: "l_hand" + - type: Icon + sprite: Mobs/Silicon/drone.rsi + state: "l_hand" + - type: BodyPart + partType: Hand + size: 3 + compatibility: Biological ##Does this do anything? Revisit when surgery is in + symmetry: Left + - type: Tag + tags: + - Trash + +- type: entity + id: RightHandDrone + name: "right drone hand" + parent: PartSilicon + components: + - type: Sprite + netsync: false + sprite: Mobs/Silicon/drone.rsi + state: "r_hand" + - type: Icon + sprite: Mobs/Silicon/drone.rsi + state: "r_hand" + - type: BodyPart + partType: Hand + size: 3 + compatibility: Biological + symmetry: Right + - type: Tag + tags: + - Trash diff --git a/Resources/Prototypes/Body/Presets/drone.yml b/Resources/Prototypes/Body/Presets/drone.yml new file mode 100644 index 0000000000..08eb753086 --- /dev/null +++ b/Resources/Prototypes/Body/Presets/drone.yml @@ -0,0 +1,10 @@ +- type: bodyPreset + name: "drone" + id: DronePreset + partIDs: + hand 1: LeftHandDrone + hand 2: LeftHandDrone + hand 3: LeftHandDrone + hand 4: RightHandDrone + hand 5: RightHandDrone + hand 6: RightHandDrone diff --git a/Resources/Prototypes/Body/Templates/drone.yml b/Resources/Prototypes/Body/Templates/drone.yml new file mode 100644 index 0000000000..046b79764c --- /dev/null +++ b/Resources/Prototypes/Body/Templates/drone.yml @@ -0,0 +1,11 @@ +- type: bodyTemplate + id: DroneTemplate + name: "drone" + centerSlot: "torso" + slots: + hand 1: hand + hand 2: hand + hand 3: hand + hand 4: hand + hand 5: hand + hand 6: hand diff --git a/Resources/Prototypes/Catalog/Research/technologies.yml b/Resources/Prototypes/Catalog/Research/technologies.yml index 61ab251acf..b102f5acbe 100644 --- a/Resources/Prototypes/Catalog/Research/technologies.yml +++ b/Resources/Prototypes/Catalog/Research/technologies.yml @@ -277,6 +277,7 @@ - IndustrialEngineering unlockedRecipes: - ShuttleConsoleCircuitboard + - Drone # Electromagnetic Theory Technology Tree diff --git a/Resources/Prototypes/Entities/Markers/Spawners/mobs.yml b/Resources/Prototypes/Entities/Markers/Spawners/mobs.yml index 71de4ea02b..16a6b09661 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/mobs.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/mobs.yml @@ -44,3 +44,18 @@ prototypes: - MobCorgi - MobCorgiOld + +## Player-controlled + +- type: entity + name: Drone Spawner + id: SpawnMobDrone + parent: MarkerBase + components: + - type: Sprite + layers: + - state: green + - texture: Mobs/Silicon/drone.rsi/shell.png + - type: ConditionalSpawner + prototypes: + - Drone diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml new file mode 100644 index 0000000000..7fb997aa71 --- /dev/null +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -0,0 +1,135 @@ +- type: entity + save: false + abstract: true + id: PlayerSiliconBase #for player controlled silicons + components: + - type: Reactive + groups: + Acidic: [Touch] + - type: Input + context: "human" + - type: MovedByPressure + - type: DamageOnHighSpeedImpact + damage: + types: + Blunt: 5 + soundHit: + path: /Audio/Effects/hit_kick.ogg + - type: Clickable + - type: Damageable + damageContainer: Inorganic + - type: InteractionOutline + - type: Fixtures + fixtures: + - shape: + # Circles, cuz rotation of rectangles looks very bad + !type:PhysShapeCircle + radius: 0.35 + mass: 20 + mask: + - Impassable + - MobImpassable + - VaultImpassable + - SmallImpassable + layer: + - Opaque + - type: MovementSpeedModifier + baseWalkSpeed : 4 + baseSprintSpeed : 3 + - type: Sprite + noRot: true + drawdepth: Mobs + - type: Physics + bodyType: KinematicController + - type: Hands + - type: Body + template: DroneTemplate + preset: DronePreset + - type: DoAfter + - type: Pullable + - type: Examiner + - type: Puller + - type: Recyclable + safe: false + - type: StandingState + - type: Alerts + +- type: entity + name: drone + id: Drone + parent: PlayerSiliconBase + components: + - type: Drone + tools: + - id: PowerDrill + - id: JawsOfLife + - id: WelderExperimental + - type: GhostTakeoverAvailable + makeSentient: true + name: Maintenance Drone + description: Maintain the station. Ignore organics. + rules: | + You are bound by these laws both in-game and out-of-character: + 1. You may not involve yourself in the matters of another being, even if such matters conflict with Law Two or Law Three, unless the other being is another Drone. + 2. You may not harm any being, regardless of intent or circumstance. + 3. Your goals are to build, maintain, repair, improve, and power to the best of your abilities, You must never actively work against these goals. + - type: MovementSpeedModifier + baseWalkSpeed : 6 + baseSprintSpeed : 6 + - type: MobState + thresholds: + 0: !type:NormalMobState {} + 70: !type:DeadMobState {} + - type: Sprite + drawdepth: Mobs + netsync: false + layers: + - state: shell + sprite: Mobs/Silicon/drone.rsi + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeCircle + radius: 0.25 + mass: 5 + mask: + - VaultImpassable + layer: + - Opaque + - type: Tag + - type: Access + tags: + - Maintenance + - Cargo + # - Quartermaster + - Engineering + - ChiefEngineer + - Medical + - ChiefMedicalOfficer + - Research + - ResearchDirector + - Security + - Service + - Captain + - Command + - External + - HeadOfSecurity + - HeadOfPersonnel + - Bar + - Hydroponics + - Kitchen + - Janitor + - Theatre + - type: Appearance + visuals: + - type: GenericEnumVisualizer + key: enum.DroneVisuals.Status + layer: 0 + states: + enum.DroneStatus.Off: shell + enum.DroneStatus.On: drone + - type: ReplacementAccent + accent: silicon + - type: Repairable + fuelcost: 15 + doAfterDelay: 8 diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 10b3e6f005..8641dd800c 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -160,6 +160,7 @@ - Dropper - Syringe - PillCanister + - Drone - Flash - Handcuffs - Stunbaton diff --git a/Resources/Prototypes/Recipes/Lathes/misc.yml b/Resources/Prototypes/Recipes/Lathes/misc.yml index 13616025b8..3601fb520f 100644 --- a/Resources/Prototypes/Recipes/Lathes/misc.yml +++ b/Resources/Prototypes/Recipes/Lathes/misc.yml @@ -71,9 +71,21 @@ materials: Wood: 100 +- type: latheRecipe + id: Drone + icon: + sprite: Mobs/Silicon/drone.rsi + state: shell + result: Drone + completetime: 1500 + materials: + Steel: 500 + Glass: 500 + Plastic: 500 + - type: latheRecipe id: SynthesizerInstrument - icon: + icon: sprite: Objects/Fun/Instruments/h_synthesizer.rsi state: icon result: SynthesizerInstrument diff --git a/Resources/Prototypes/accents.yml b/Resources/Prototypes/accents.yml index decc7210b7..54e9a3a7a2 100644 --- a/Resources/Prototypes/accents.yml +++ b/Resources/Prototypes/accents.yml @@ -30,3 +30,11 @@ - Mmfph! - Mmmf mrrfff! - Mmmf mnnf! + +- type: accent + id: silicon + words: + - Beep. + - Boop. + - Whirr. + - Beep-boop. diff --git a/Resources/Textures/Mobs/Silicon/drone.rsi/drone.png b/Resources/Textures/Mobs/Silicon/drone.rsi/drone.png new file mode 100644 index 0000000000..531a2aaa8e Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/drone.rsi/drone.png differ diff --git a/Resources/Textures/Mobs/Silicon/drone.rsi/l_hand.png b/Resources/Textures/Mobs/Silicon/drone.rsi/l_hand.png new file mode 100644 index 0000000000..272f4a0664 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/drone.rsi/l_hand.png differ diff --git a/Resources/Textures/Mobs/Silicon/drone.rsi/meta.json b/Resources/Textures/Mobs/Silicon/drone.rsi/meta.json new file mode 100644 index 0000000000..46a79f9ae7 --- /dev/null +++ b/Resources/Textures/Mobs/Silicon/drone.rsi/meta.json @@ -0,0 +1,53 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a", + "states": [ + { + "name": "drone", + "directions": 4, + "delays": [ + [ + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2 + ], + [ + 0.2, + 0.2, + 0.2 + ] + ] + }, + { + "name": "shell", + "delays": [ + [ + 1 + ] + ] + }, + { + "name": "l_hand", + "directions": 4 + }, + { + "name": "r_hand", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Mobs/Silicon/drone.rsi/r_hand.png b/Resources/Textures/Mobs/Silicon/drone.rsi/r_hand.png new file mode 100644 index 0000000000..4a18493138 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/drone.rsi/r_hand.png differ diff --git a/Resources/Textures/Mobs/Silicon/drone.rsi/shell.png b/Resources/Textures/Mobs/Silicon/drone.rsi/shell.png new file mode 100644 index 0000000000..0c6551c5aa Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/drone.rsi/shell.png differ