diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index 4d7489b383..eaf2673392 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -45,6 +45,7 @@ namespace Content.Client.Entry "DiseaseDiagnoser", "DiseaseVaccine", "DiseaseVaccineCreator", + "ImmovableRod", "DiseaseZombie", "DiseaseBuildup", "ZombieTransfer", diff --git a/Content.Server/ImmovableRod/ImmovableRodComponent.cs b/Content.Server/ImmovableRod/ImmovableRodComponent.cs new file mode 100644 index 0000000000..fec9a5b382 --- /dev/null +++ b/Content.Server/ImmovableRod/ImmovableRodComponent.cs @@ -0,0 +1,48 @@ +using Content.Shared.Sound; + +namespace Content.Server.ImmovableRod; + +[RegisterComponent] +public sealed class ImmovableRodComponent : Component +{ + public int MobCount = 0; + + [DataField("hitSound")] + public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Effects/bang.ogg"); + + [DataField("hitSoundProbability")] + public float HitSoundProbability = 0.1f; + + /// + /// The rod will be automatically cleaned up after this time. + /// + [DataField("lifetime")] + public TimeSpan Lifetime = TimeSpan.FromSeconds(30); + + [DataField("minSpeed")] + public float MinSpeed = 10f; + + [DataField("maxSpeed")] + public float MaxSpeed = 35f; + + /// + /// Stuff like wizard rods might want to set this to false, so that they can set the velocity themselves. + /// + [DataField("randomizeVelocity")] + public bool RandomizeVelocity = true; + + /// + /// Overrides the random direction for an immovable rod. + /// + [DataField("directionOverride")] + public Angle DirectionOverride = Angle.Zero; + + /// + /// With this set to true, rods will automatically set the tiles under them to space. + /// + [DataField("destroyTiles")] + public bool DestroyTiles = true; + + [DataField("accumulator")] + public float Accumulator = 0f; +} diff --git a/Content.Server/ImmovableRod/ImmovableRodSystem.cs b/Content.Server/ImmovableRod/ImmovableRodSystem.cs new file mode 100644 index 0000000000..c4a56da233 --- /dev/null +++ b/Content.Server/ImmovableRod/ImmovableRodSystem.cs @@ -0,0 +1,119 @@ +using Content.Server.Body.Components; +using Content.Server.Popups; +using Content.Shared.Examine; +using Robust.Shared.Audio; +using Robust.Shared.Map; +using Robust.Shared.Physics.Dynamics; +using Robust.Shared.Player; +using Robust.Shared.Random; + +namespace Content.Server.ImmovableRod; + +public sealed class ImmovableRodSystem : EntitySystem +{ + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly IMapManager _map = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + public override void Update(float frameTime) + { + base.Update(frameTime); + + // we are deliberately including paused entities. rod hungers for all + foreach (var (rod, trans) in EntityManager.EntityQuery(true)) + { + rod.Accumulator += frameTime; + + if (rod.Accumulator > rod.Lifetime.TotalSeconds) + { + QueueDel(rod.Owner); + return; + } + + if (!rod.DestroyTiles) + continue; + if (!_map.TryGetGrid(trans.GridID, out var grid)) + continue; + + grid.SetTile(trans.Coordinates, Tile.Empty); + } + } + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnCollide); + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnExamined); + } + + private void OnComponentInit(EntityUid uid, ImmovableRodComponent component, ComponentInit args) + { + if (EntityManager.TryGetComponent(uid, out PhysicsComponent? phys)) + { + phys.LinearDamping = 0f; + phys.Friction = 0f; + phys.BodyStatus = BodyStatus.InAir; + + if (!component.RandomizeVelocity) + return; + + var xform = Transform(uid); + var vel = component.DirectionOverride.Degrees switch + { + 0f => _random.NextVector2(component.MinSpeed, component.MaxSpeed), + _ => xform.WorldRotation.RotateVec(component.DirectionOverride.ToVec()) * _random.NextFloat(component.MinSpeed, component.MaxSpeed) + }; + + phys.ApplyLinearImpulse(vel); + xform.LocalRotation = (vel - xform.WorldPosition).ToWorldAngle() + MathHelper.PiOver2; + } + } + + private void OnCollide(EntityUid uid, ImmovableRodComponent component, StartCollideEvent args) + { + var ent = args.OtherFixture.Body.Owner; + + if (_random.Prob(component.HitSoundProbability)) + { + SoundSystem.Play(Filter.Pvs(uid), component.Sound.GetSound(), uid, component.Sound.Params); + } + + if (HasComp(ent)) + { + // oh god. + var coords = Transform(uid).Coordinates; + _popup.PopupCoordinates(Loc.GetString("immovable-rod-collided-rod-not-good"), coords, Filter.Pvs(uid)); + + Del(uid); + Del(ent); + Spawn("Singularity", coords); + + return; + } + + // gib em + if (TryComp(ent, out var body)) + { + component.MobCount++; + + _popup.PopupEntity(Loc.GetString("immovable-rod-penetrated-mob", ("rod", uid), ("mob", ent)), uid, Filter.Pvs(uid)); + body.Gib(); + } + + QueueDel(ent); + } + + private void OnExamined(EntityUid uid, ImmovableRodComponent component, ExaminedEvent args) + { + if (component.MobCount == 0) + { + args.PushText(Loc.GetString("immovable-rod-consumed-none", ("rod", uid))); + } + else + { + args.PushText(Loc.GetString("immovable-rod-consumed-souls", ("rod", uid), ("amount", component.MobCount))); + } + } +} diff --git a/Resources/Locale/en-US/immovable-rod/immovable-rod.ftl b/Resources/Locale/en-US/immovable-rod/immovable-rod.ftl new file mode 100644 index 0000000000..6843e350b0 --- /dev/null +++ b/Resources/Locale/en-US/immovable-rod/immovable-rod.ftl @@ -0,0 +1,5 @@ +immovable-rod-collided-rod-not-good = Oh fuck, that can't be good. +immovable-rod-penetrated-mob = {CAPITALIZE(THE($rod))} cleanly eviscerates {THE($mob)}! + +immovable-rod-consumed-none = {CAPITALIZE(THE($rod))} has consumed zero souls. +immovable-rod-consumed-souls = {CAPITALIZE(THE($rod))} has consumed {$amount} souls. diff --git a/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml b/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml new file mode 100644 index 0000000000..0eebc02258 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml @@ -0,0 +1,41 @@ + # Immovable rod + +- type: entity + id: ImmovableRod + name: immovable rod + description: You can sense that it's hungry. That's usually a bad sign. + components: + - type: Clickable + - type: InteractionOutline + - type: MovementIgnoreGravity + - type: Sprite + sprite: Objects/Fun/immovable_rod.rsi + state: icon + noRot: false + - type: ImmovableRod + - type: Physics + bodyType: Dynamic + linearDamping: 0 + - type: PointLight + radius: 3 + color: red + energy: 2.0 + - type: Fixtures + fixtures: + - shape: + !type:PhysShapeCircle + radius: 0.5 + mass: 1 + hard: false + layer: + - Impassable + - Opaque + +- type: entity + id: ImmovableRodSlow + suffix: Slow + parent: ImmovableRod + components: + - type: ImmovableRod + minSpeed: 1 + maxSpeed: 5 diff --git a/Resources/Textures/Objects/Fun/immovable_rod.rsi/icon.png b/Resources/Textures/Objects/Fun/immovable_rod.rsi/icon.png new file mode 100644 index 0000000000..6508b8897a Binary files /dev/null and b/Resources/Textures/Objects/Fun/immovable_rod.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Fun/immovable_rod.rsi/meta.json b/Resources/Textures/Objects/Fun/immovable_rod.rsi/meta.json new file mode 100644 index 0000000000..85047cd29b --- /dev/null +++ b/Resources/Textures/Objects/Fun/immovable_rod.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/20e4add48712b59e9bcadd187beee54c02f98e38, modified by mirrorcult to be 1-dir", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + } + ] +}