diff --git a/Content.Server/Physics/Controllers/RandomWalkController.cs b/Content.Server/Physics/Controllers/RandomWalkController.cs index 9cd717f811..8cffa32dba 100644 --- a/Content.Server/Physics/Controllers/RandomWalkController.cs +++ b/Content.Server/Physics/Controllers/RandomWalkController.cs @@ -5,6 +5,7 @@ using Robust.Shared.Random; using Robust.Shared.Timing; using Content.Server.Physics.Components; +using Content.Shared.Follower.Components; using Content.Shared.Throwing; namespace Content.Server.Physics.Controllers; @@ -41,7 +42,8 @@ internal sealed class RandomWalkController : VirtualController foreach(var (randomWalk, physics) in EntityManager.EntityQuery()) { if (EntityManager.HasComponent(randomWalk.Owner) - || EntityManager.HasComponent(randomWalk.Owner)) + || EntityManager.HasComponent(randomWalk.Owner) + || EntityManager.HasComponent(randomWalk.Owner)) continue; var curTime = _timing.CurTime; diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs index 968da8c269..a1a594dc31 100644 --- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs +++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs @@ -136,6 +136,7 @@ public sealed partial class RevenantSystem : EntitySystem if (component.Essence <= 0) { + Spawn(component.SpawnOnDeathPrototype, Transform(uid).Coordinates); QueueDel(uid); } return true; diff --git a/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs b/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs index 63e55e0d14..e2ea56e403 100644 --- a/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs +++ b/Content.Shared/Construction/Steps/ConstructionGraphStepTypeSerializer.cs @@ -31,6 +31,11 @@ namespace Content.Shared.Construction.Steps return typeof(TagConstructionGraphStep); } + if (node.Has("prototype")) + { + return typeof(PrototypeConstructionGraphStep); + } + if (node.Has("allTags") || node.Has("anyTags")) { return typeof(MultipleTagsConstructionGraphStep); diff --git a/Content.Shared/Construction/Steps/PrototypeConstructionGraphStep.cs b/Content.Shared/Construction/Steps/PrototypeConstructionGraphStep.cs new file mode 100644 index 0000000000..b6b975846d --- /dev/null +++ b/Content.Shared/Construction/Steps/PrototypeConstructionGraphStep.cs @@ -0,0 +1,27 @@ +using System.Linq; + +namespace Content.Shared.Construction.Steps +{ + [DataDefinition] + public sealed class PrototypeConstructionGraphStep : ArbitraryInsertConstructionGraphStep + { + [DataField("prototype")] + private string _prototype = "BaseItem"; + + [DataField("allowParents")] + private bool _allowParents; + + public override bool EntityValid(EntityUid uid, IEntityManager entityManager, IComponentFactory compFactory) + { + entityManager.TryGetComponent(uid, out MetaDataComponent? metaDataComponent); + if (metaDataComponent?.EntityPrototype == null) + return false; + + if (metaDataComponent.EntityPrototype.ID == _prototype) + return true; + if (_allowParents && metaDataComponent.EntityPrototype.Parents != null) + return metaDataComponent.EntityPrototype.Parents.Contains(_prototype); + return false; + } + } +} diff --git a/Content.Shared/Follower/FollowerSystem.cs b/Content.Shared/Follower/FollowerSystem.cs index b7dbb28cf9..d3b0747acc 100644 --- a/Content.Shared/Follower/FollowerSystem.cs +++ b/Content.Shared/Follower/FollowerSystem.cs @@ -1,18 +1,27 @@ using Content.Shared.Database; using Content.Shared.Follower.Components; using Content.Shared.Ghost; +using Content.Shared.Hands; using Content.Shared.Movement.Events; -using Content.Shared.Movement.Systems; +using Content.Shared.Physics.Pull; +using Content.Shared.Tag; using Content.Shared.Verbs; +using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Map.Events; using Robust.Shared.Utility; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Systems; namespace Content.Shared.Follower; public sealed class FollowerSystem : EntitySystem { [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly SharedContainerSystem _containerSystem = default!; + [Dependency] private readonly SharedJointSystem _jointSystem = default!; + [Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!; public override void Initialize() { @@ -20,6 +29,8 @@ public sealed class FollowerSystem : EntitySystem SubscribeLocalEvent>(OnGetAlternativeVerbs); SubscribeLocalEvent(OnFollowerMove); + SubscribeLocalEvent(OnPullStarted); + SubscribeLocalEvent(OnGotEquippedHand); SubscribeLocalEvent(OnFollowedTerminating); SubscribeLocalEvent(OnBeforeSave); } @@ -44,25 +55,38 @@ public sealed class FollowerSystem : EntitySystem private void OnGetAlternativeVerbs(GetVerbsEvent ev) { - if (!HasComp(ev.User)) - return; - if (ev.User == ev.Target || ev.Target.IsClientSide()) return; - var verb = new AlternativeVerb + if (HasComp(ev.User)) { - Priority = 10, - Act = (() => + var verb = new AlternativeVerb() { - StartFollowingEntity(ev.User, ev.Target); - }), - Impact = LogImpact.Low, - Text = Loc.GetString("verb-follow-text"), - Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/open.svg.192dpi.png")), - }; + Priority = 10, + Act = () => StartFollowingEntity(ev.User, ev.Target), + Impact = LogImpact.Low, + Text = Loc.GetString("verb-follow-text"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/open.svg.192dpi.png")) + }; + ev.Verbs.Add(verb); + } - ev.Verbs.Add(verb); + if (_tagSystem.HasTag(ev.Target, "ForceableFollow")) + { + if (!ev.CanAccess || !ev.CanInteract) + return; + + var verb = new AlternativeVerb + { + Priority = 10, + Act = () => StartFollowingEntity(ev.Target, ev.User), + Impact = LogImpact.Low, + Text = Loc.GetString("verb-follow-me-text"), + Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/close.svg.192dpi.png")), + }; + + ev.Verbs.Add(verb); + } } private void OnFollowerMove(EntityUid uid, FollowerComponent component, ref MoveInputEvent args) @@ -70,6 +94,16 @@ public sealed class FollowerSystem : EntitySystem StopFollowingEntity(uid, component.Following); } + private void OnPullStarted(EntityUid uid, FollowerComponent component, PullStartedMessage args) + { + StopFollowingEntity(uid, component.Following); + } + + private void OnGotEquippedHand(EntityUid uid, FollowerComponent component, GotEquippedHandEvent args) + { + StopFollowingEntity(uid, component.Following, deparent:false); + } + // Since we parent our observer to the followed entity, we need to detach // before they get deleted so that we don't get recursively deleted too. private void OnFollowedTerminating(EntityUid uid, FollowedComponent component, ref EntityTerminatingEvent args) @@ -102,24 +136,35 @@ public sealed class FollowerSystem : EntitySystem if (!followedComp.Following.Add(follower)) return; + if (TryComp(follower, out var joints)) + _jointSystem.ClearJoints(follower, joints); + + _physicsSystem.SetLinearVelocity(follower, Vector2.Zero); + var xform = Transform(follower); - _transform.SetCoordinates(follower, xform, new EntityCoordinates(entity, Vector2.Zero), Angle.Zero); + _containerSystem.AttachParentToContainerOrGrid(xform); + + // If we didn't get to parent's container. + if (xform.ParentUid != Transform(xform.ParentUid).ParentUid) + { + _transform.SetCoordinates(follower, xform, new EntityCoordinates(entity, Vector2.Zero), rotation: Angle.Zero); + } EnsureComp(follower); var followerEv = new StartedFollowingEntityEvent(entity, follower); var entityEv = new EntityStartedFollowingEvent(entity, follower); - RaiseLocalEvent(follower, followerEv, true); - RaiseLocalEvent(entity, entityEv, false); + RaiseLocalEvent(follower, followerEv); + RaiseLocalEvent(entity, entityEv); Dirty(followedComp); } /// /// Forces an entity to stop following another entity, if it is doing so. /// - public void StopFollowingEntity(EntityUid uid, EntityUid target, - FollowedComponent? followed=null) + /// Should the entity deparent itself + public void StopFollowingEntity(EntityUid uid, EntityUid target, FollowedComponent? followed = null, bool deparent = true) { if (!Resolve(target, ref followed, false)) return; @@ -130,24 +175,27 @@ public sealed class FollowerSystem : EntitySystem followed.Following.Remove(uid); if (followed.Following.Count == 0) RemComp(target); + RemComp(uid); - - var xform = Transform(uid); - _transform.AttachToGridOrMap(uid, xform); - if (xform.MapID == MapId.Nullspace) - { - QueueDel(uid); - return; - } - RemComp(uid); - var uidEv = new StoppedFollowingEntityEvent(target, uid); var targetEv = new EntityStoppedFollowingEvent(target, uid); RaiseLocalEvent(uid, uidEv, true); RaiseLocalEvent(target, targetEv, false); Dirty(followed); + RaiseLocalEvent(uid, uidEv); + RaiseLocalEvent(target, targetEv); + + if (!Deleted(uid) && deparent) + { + var xform = Transform(uid); + _transform.AttachToGridOrMap(uid, xform); + if (xform.MapUid == null) + { + QueueDel(uid); + } + } } /// diff --git a/Content.Shared/Revenant/Components/RevenantComponent.cs b/Content.Shared/Revenant/Components/RevenantComponent.cs index c5b513ff8a..ba93cfe77c 100644 --- a/Content.Shared/Revenant/Components/RevenantComponent.cs +++ b/Content.Shared/Revenant/Components/RevenantComponent.cs @@ -1,6 +1,7 @@ using Content.Shared.FixedPoint; using Content.Shared.Store; using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; namespace Content.Shared.Revenant.Components; @@ -18,6 +19,12 @@ public sealed class RevenantComponent : Component [DataField("stolenEssenceCurrencyPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] public string StolenEssenceCurrencyPrototype = "StolenEssence"; + /// + /// Prototype to spawn when the entity dies. + /// + [DataField("spawnOnDeathPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] + public string SpawnOnDeathPrototype = "Ectoplasm"; + /// /// The entity's current max amount of essence. Can be increased /// through harvesting player souls. diff --git a/Resources/Locale/en-US/follower/follow-verb.ftl b/Resources/Locale/en-US/follower/follow-verb.ftl index dbcac9d352..b5538b806f 100644 --- a/Resources/Locale/en-US/follower/follow-verb.ftl +++ b/Resources/Locale/en-US/follower/follow-verb.ftl @@ -1 +1,2 @@ verb-follow-text = Follow +verb-follow-me-text = Make follow diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/toy.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/toy.yml index 09c5cfbe19..b4cb46f772 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/Random/toy.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/toy.yml @@ -11,6 +11,7 @@ - type: RandomSpawner rarePrototypes: - FoamBlade + - PlushieGhost rareChance: 0.03 prototypes: - PlushieBee diff --git a/Resources/Prototypes/Entities/Objects/Decoration/present.yml b/Resources/Prototypes/Entities/Objects/Decoration/present.yml index c5a308591c..f6fa82ca47 100644 --- a/Resources/Prototypes/Entities/Objects/Decoration/present.yml +++ b/Resources/Prototypes/Entities/Objects/Decoration/present.yml @@ -55,6 +55,8 @@ orGroup: GiftPool - id: JetpackMiniFilled orGroup: GiftPool + - id: PlushieGhost + orGroup: GiftPool - id: PlushieBee orGroup: GiftPool - id: PlushieRGBee diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index f1f84f5796..1d5c20a69c 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -35,6 +35,59 @@ - type: StaticPrice price: 5 +- type: entity + parent: BasePlushie + id: PlushieGhost + name: ghost soft toy + description: The start of your personal GHOST GANG! + components: + - type: Sprite + sprite: Mobs/Ghosts/ghost_human.rsi + state: icon + noRot: true + - type: Item + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.25,-0.25,0.25,0.25" + density: 20 + mask: + - ItemMask + restitution: 0.98 + friction: 0.01 + - type: Physics + angularDamping: 0.02 + linearDamping: 0.02 + fixedRotation: true + bodyType: Dynamic + - type: TileFrictionModifier + modifier: 0.1 + - type: Tag + tags: + - ForceableFollow + - type: RandomWalk + accumulatorRatio: 0.5 + maxSpeed: 1 + minSpeed: 0.25 + +- type: entity + parent: PlushieGhost + id: PlushieGhostRevenant + name: revenant soft toy + suffix: DO NOT MAP + description: So soft it almost makes you want to take a nap... + components: + - type: Sprite + sprite: Mobs/Ghosts/revenant.rsi + state: icon + netsync: false + noRot: true + - type: Construction + graph: PlushieGhostRevenant + node: plushie + - type: entity parent: BasePlushie id: PlushieBee diff --git a/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml b/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml index 74d53e078f..18dcf699a8 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml @@ -123,3 +123,26 @@ - type: DeleteOnTrigger - type: Extractable grindableSolutionName: food + +- type: entity + parent: Ash + id: Ectoplasm + name: ectoplasm + description: Much less deadly in this form. + components: + - type: Sprite + netsync: false + sprite: Mobs/Ghosts/revenant.rsi + state: ectoplasm + - type: Tag + tags: + - Trash + - type: SolutionContainerManager + solutions: + food: + maxVol: 50 + reagents: + - ReagentId: Ash + Quantity: 5 + - ReagentId: SpaceLube + Quantity: 5 diff --git a/Resources/Prototypes/Recipes/Crafting/Graphs/toys.yml b/Resources/Prototypes/Recipes/Crafting/Graphs/toys.yml new file mode 100644 index 0000000000..b08356e5ce --- /dev/null +++ b/Resources/Prototypes/Recipes/Crafting/Graphs/toys.yml @@ -0,0 +1,15 @@ +- type: constructionGraph + id: PlushieGhostRevenant + start: start + graph: + - node: start + edges: + - to: plushie + steps: + - prototype: PlushieGhost + name: a ghost plushie + - prototype: Ectoplasm + name: ectoplasm + doAfter: 10 + - node: plushie + entity: PlushieGhostRevenant diff --git a/Resources/Prototypes/Recipes/Crafting/toys.yml b/Resources/Prototypes/Recipes/Crafting/toys.yml new file mode 100644 index 0000000000..2c59175416 --- /dev/null +++ b/Resources/Prototypes/Recipes/Crafting/toys.yml @@ -0,0 +1,12 @@ +- type: construction + name: revenant plushie + id: PlushieGhostRevenant + graph: PlushieGhostRevenant + startNode: start + targetNode: plushie + category: construction-category-misc + objectType: Item + description: A toy to scare the medbay with. + icon: + sprite: Mobs/Ghosts/revenant.rsi + state: icon diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index abf3738b35..a3c9da935e 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -284,6 +284,9 @@ - type: Tag id: FootstepSound +- type: Tag + id: ForceableFollow + - type: Tag id: ForceFixRotations # fixrotations command WILL target this