diff --git a/Content.IntegrationTests/Tests/Holosign/HolosignProjectorTest.cs b/Content.IntegrationTests/Tests/Holosign/HolosignProjectorTest.cs new file mode 100644 index 00000000000..2fc1ce6401c --- /dev/null +++ b/Content.IntegrationTests/Tests/Holosign/HolosignProjectorTest.cs @@ -0,0 +1,113 @@ +#nullable enable +using Content.IntegrationTests.Tests.Movement; +using Content.Shared.Holosign; +using Content.Shared.PowerCell; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Spawners; + +namespace Content.IntegrationTests.Tests.Holosign; + +/// +/// Tests for different devices using . +/// +[TestOf(typeof(HolosignProjectorComponent))] +public sealed class HolosignProjectorTest : MovementTest +{ + private static readonly EntProtoId HoloBarrierProjectorProtoId = "HoloprojectorSecurity"; + private static readonly EntProtoId HoloSignProjectorProtoId = "Holoprojector"; + + /// + /// Tests the janitors holosign projector. + /// + [Test] + public async Task HoloSignTest() + { + var projector = await PlaceInHands(HoloSignProjectorProtoId); + var projectorComp = Comp(projector); + var signProtoId = projectorComp.SignProto; + + // No holosigns before using the item. + await AssertEntityLookup((WallPrototype, 2)); + + var powerCellSystem = SEntMan.System(); + var initialUses = powerCellSystem.GetRemainingUses(ToServer(projector), projectorComp.ChargeUse); + Assert.That(initialUses, Is.GreaterThan(0), "Holoprojector spawned without usable charges."); + + // Click on the tile next to the player. + await Interact(null, TargetCoords); + + // We should have one charge less. + var remainingUses = powerCellSystem.GetRemainingUses(ToServer(projector), projectorComp.ChargeUse); + Assert.That(remainingUses, Is.EqualTo(initialUses - 1), "Holoprojector did not use the right amount of charge when used."); + + // We should have spawned exactly one holosign. + await AssertEntityLookup( + (signProtoId, 1), + (WallPrototype, 2)); + + // Try spawn more holosigns than we have charge. + for (var i = 0; i < initialUses; i++) + { + await Interact(null, TargetCoords); + } + + // The total should be the same as the initial charges. + await AssertEntityLookup( + (signProtoId, initialUses), + (WallPrototype, 2)); + + // We should have no charges left. + remainingUses = powerCellSystem.GetRemainingUses(ToServer(projector), projectorComp.ChargeUse); + Assert.That(remainingUses, Is.Zero, "Holoprojector did not use up all charges."); + } + + /// + /// Tests the security holo barrier projector and the barrier. + /// + [Test] + public async Task HoloBarrierTest() + { + var projector = await PlaceInHands(HoloBarrierProjectorProtoId); + var holoBarrierProtoId = Comp(projector).SignProto; + // No holobarriers before using the item. + await AssertEntityLookup((WallPrototype, 2)); + + // Click on the tile next to the player. + await Interact(null, TargetCoords); + + // We should have spawned exactly one holobarrier. + await AssertEntityLookup( + (holoBarrierProtoId, 1), + (WallPrototype, 2)); + Target = FromServer(await FindEntity(holoBarrierProtoId)); + var timeRemaining = Comp(Target).Lifetime; + + // Check that the barrier is at the location we clicked at. + AssertLocation(Target, TargetCoords); + + // Try moving past the barrier. + Assert.That(Delta(), Is.GreaterThan(0.5), "Player was not located west of the holobarrier."); + await Move(DirectionFlag.East, 0.5f); + Assert.That(Delta(), Is.GreaterThan(0.5), "Player was able to walk through a holobarrier."); + + // Try to climb the barrier. + await Interact(Target, TargetCoords, altInteract: true); + + // We should be able to move past the barrier now. + await Move(DirectionFlag.East, 0.5f); + Assert.That(Delta(), Is.LessThan(-0.5), "Player was not able to climb over a holobarrier."); + + // We should not be able to walk back without climbing again. + await Move(DirectionFlag.West, 0.5f); + Assert.That(Delta(), Is.LessThan(-0.5), "Player was able to walk through a holobarrier."); + + // Wait until the barrier despawns. + await RunSeconds(timeRemaining); + AssertDeleted(Target); + + // We should be able to walk back now. + await Move(DirectionFlag.West, 0.5f); + Assert.That(DeltaCoordinates(), Is.GreaterThan(0.5), "Player was able to walk past a deleted holobarrier."); + } +} diff --git a/Content.IntegrationTests/Tests/Movement/MovementTest.cs b/Content.IntegrationTests/Tests/Movement/MovementTest.cs index 44ef02043e8..ee9d1f77ea6 100644 --- a/Content.IntegrationTests/Tests/Movement/MovementTest.cs +++ b/Content.IntegrationTests/Tests/Movement/MovementTest.cs @@ -2,6 +2,8 @@ using System.Numerics; using Content.IntegrationTests.Tests.Interaction; using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Movement; @@ -13,6 +15,7 @@ namespace Content.IntegrationTests.Tests.Movement; public abstract class MovementTest : InteractionTest { protected override string PlayerPrototype => "MobHuman"; + protected static readonly EntProtoId WallPrototype = "WallSolid"; /// /// Number of tiles to add either side of the player. @@ -47,8 +50,8 @@ public abstract class MovementTest : InteractionTest if (AddWalls) { - var sWallLeft = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(-Tiles, 0))); - var sWallRight = await SpawnEntity("WallSolid", pCoords.Offset(new Vector2(Tiles, 0))); + var sWallLeft = await SpawnEntity(WallPrototype, pCoords.Offset(new Vector2(-Tiles, 0))); + var sWallRight = await SpawnEntity(WallPrototype, pCoords.Offset(new Vector2(Tiles, 0))); WallLeft = SEntMan.GetNetEntity(sWallLeft); WallRight = SEntMan.GetNetEntity(sWallRight); @@ -59,7 +62,7 @@ public abstract class MovementTest : InteractionTest } /// - /// Get the relative horizontal between two entities. Defaults to using the target & player entity. + /// Get the relative horizontal between two entities. Defaults to using the target & player entity. /// protected float Delta(NetEntity? target = null, NetEntity? other = null) { @@ -73,5 +76,17 @@ public abstract class MovementTest : InteractionTest var delta = Transform.GetWorldPosition(SEntMan.GetEntity(target.Value)) - Transform.GetWorldPosition(SEntMan.GetEntity(other ?? Player)); return delta.X; } + + /// + /// Get the relative horizontal between a set of coordinates and an entity. Defaults to using the target coordinates and the player entity. + /// + protected float DeltaCoordinates(NetCoordinates? coords = null, NetEntity? other = null) + { + other ??= Player; + coords ??= TargetCoords; + + var delta = Transform.ToWorldPosition(ToServer(coords.Value)) - Transform.GetWorldPosition(ToServer(other.Value)); + return delta.X; + } } diff --git a/Content.Server/Holosign/HolosignProjectorComponent.cs b/Content.Server/Holosign/HolosignProjectorComponent.cs deleted file mode 100644 index bdc826f1304..00000000000 --- a/Content.Server/Holosign/HolosignProjectorComponent.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Holosign -{ - [RegisterComponent] - public sealed partial class HolosignProjectorComponent : Component - { - [ViewVariables(VVAccess.ReadWrite)] - [DataField("signProto", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string SignProto = "HolosignWetFloor"; - - /// - /// How much charge a single use expends. - /// - [ViewVariables(VVAccess.ReadWrite), DataField("chargeUse")] - public float ChargeUse = 50f; - } -} diff --git a/Content.Shared/Holosign/HolosignProjectorComponent.cs b/Content.Shared/Holosign/HolosignProjectorComponent.cs new file mode 100644 index 00000000000..532e80fbd84 --- /dev/null +++ b/Content.Shared/Holosign/HolosignProjectorComponent.cs @@ -0,0 +1,31 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.GameStates; + +namespace Content.Shared.Holosign; + +/// +/// Added to an item and allows it to spawn a specified prototype at the location you click on, using charge from a power cell. +/// Used for holosigns, holofans and holobarriers. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class HolosignProjectorComponent : Component +{ + /// + /// The prototype to spawn on use. + /// + [DataField, AutoNetworkedField] + public EntProtoId SignProto = "HolosignWetFloor"; + + /// + /// How much charge a single use expends, in watts. + /// + [DataField, AutoNetworkedField] + public float ChargeUse = 50f; + + /// + /// Whether or not to use predictive spawning. + /// At the moment this does not support entities with animated sprites, so set this to false in that case. + /// + [DataField, AutoNetworkedField] + public bool PredictedSpawn; +} diff --git a/Content.Server/Holosign/HolosignSystem.cs b/Content.Shared/Holosign/HolosignSystem.cs similarity index 56% rename from Content.Server/Holosign/HolosignSystem.cs rename to Content.Shared/Holosign/HolosignSystem.cs index 7d01ffb9752..730ac1140a2 100644 --- a/Content.Server/Holosign/HolosignSystem.cs +++ b/Content.Shared/Holosign/HolosignSystem.cs @@ -1,29 +1,30 @@ using Content.Shared.Examine; -using Content.Shared.Coordinates.Helpers; -using Content.Shared.PowerCell; using Content.Shared.Interaction; +using Content.Shared.PowerCell; using Content.Shared.Storage; +using Robust.Shared.Network; -namespace Content.Server.Holosign; +namespace Content.Shared.Holosign; public sealed class HolosignSystem : EntitySystem { [Dependency] private readonly PowerCellSystem _powerCell = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly INetManager _net = default!; public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnBeforeInteract); SubscribeLocalEvent(OnExamine); } - private void OnExamine(EntityUid uid, HolosignProjectorComponent component, ExaminedEvent args) + private void OnExamine(Entity ent, ref ExaminedEvent args) { // TODO: This should probably be using an itemstatus // TODO: I'm too lazy to do this rn but it's literally copy-paste from emag. - var charges = _powerCell.GetRemainingUses(uid, component.ChargeUse); - var maxCharges = _powerCell.GetMaxUses(uid, component.ChargeUse); + var charges = _powerCell.GetRemainingUses(ent.Owner, ent.Comp.ChargeUse); + var maxCharges = _powerCell.GetMaxUses(ent.Owner, ent.Comp.ChargeUse); using (args.PushGroup(nameof(HolosignProjectorComponent))) { @@ -36,23 +37,18 @@ public sealed class HolosignSystem : EntitySystem } } - private void OnBeforeInteract(EntityUid uid, HolosignProjectorComponent component, BeforeRangedInteractEvent args) + private void OnBeforeInteract(Entity ent, ref BeforeRangedInteractEvent args) { - if (args.Handled || !args.CanReach // prevent placing out of range || HasComp(args.Target) // if it's a storage component like a bag, we ignore usage so it can be stored - || !_powerCell.TryUseCharge(uid, component.ChargeUse, user: args.User) // if no battery or no charge, doesn't work + || !_powerCell.TryUseCharge(ent.Owner, ent.Comp.ChargeUse, user: args.User, predicted: true) // if no battery or no charge, doesn't work ) return; - // places the holographic sign at the click location, snapped to grid. // overlapping of the same holo on one tile remains allowed to allow holofan refreshes - var holoUid = Spawn(component.SignProto, args.ClickLocation.SnapToGrid(EntityManager)); - var xform = Transform(holoUid); - // TODO: Just make the prototype anchored - if (!xform.Anchored) - _transform.AnchorEntity(holoUid, xform); // anchor to prevent any tempering with (don't know what could even interact with it) + if (ent.Comp.PredictedSpawn || _net.IsServer) + PredictedSpawnAtPosition(ent.Comp.SignProto, args.ClickLocation); args.Handled = true; } diff --git a/Resources/Prototypes/Entities/Objects/Devices/holoprojectors.yml b/Resources/Prototypes/Entities/Objects/Devices/holoprojectors.yml index 6e725ad100a..20af65296e4 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/holoprojectors.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/holoprojectors.yml @@ -31,7 +31,7 @@ state: icon - type: Tag tags: - - HolosignProjector + - HolosignProjector - type: entity @@ -107,7 +107,7 @@ state: icon - type: Tag tags: - - HolofanProjector + - HolofanProjector - type: StaticPrice price: 50 - type: GuideHelp diff --git a/Resources/Prototypes/Entities/Structures/Holographic/projections.yml b/Resources/Prototypes/Entities/Structures/Holographic/projections.yml index 06971c3a617..73ee7937cb4 100644 --- a/Resources/Prototypes/Entities/Structures/Holographic/projections.yml +++ b/Resources/Prototypes/Entities/Structures/Holographic/projections.yml @@ -21,6 +21,7 @@ lifetime: 300 - type: Holosign # DeltaV - Whitelist for holoprojector pickup # END DeltaV + #- type: Clickable #- type: Damageable # DeltaV - Moved all these commented out attributes to BaseHolosign # damageContainer: Inorganic #- type: Destructible @@ -94,12 +95,12 @@ - type: Clickable - type: Destructible thresholds: - - trigger: - !type:DamageTrigger - damage: 10 - behaviors: - - !type:DoActsBehavior - acts: [ "Destruction" ] + - trigger: + !type:DamageTrigger + damage: 10 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ] - type: entity id: HolosignSecurity @@ -135,7 +136,6 @@ color: red # END DeltaV - type: Climbable - - type: Clickable - type: entity id: HolosignForcefield @@ -175,9 +175,9 @@ throwForce: 0 - type: Destructible thresholds: - - trigger: - !type:DamageTrigger - damage: 60 - behaviors: - - !type:DoActsBehavior - acts: [ "Destruction" ] + - trigger: + !type:DamageTrigger + damage: 60 + behaviors: + - !type:DoActsBehavior + acts: [ "Destruction" ]