diff --git a/Content.Shared/Teleportation/Components/SwapTeleporterComponent.cs b/Content.Shared/Teleportation/Components/SwapTeleporterComponent.cs new file mode 100644 index 0000000000..7a7bac83f4 --- /dev/null +++ b/Content.Shared/Teleportation/Components/SwapTeleporterComponent.cs @@ -0,0 +1,65 @@ +using Content.Shared.Teleportation.Systems; +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Teleportation.Components; + +/// +/// This is used for an entity that, when linked to another valid entity, allows the two to swap positions, +/// additionally swapping the positions of the parents. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SwapTeleporterSystem))] +public sealed partial class SwapTeleporterComponent : Component +{ + /// + /// The other SwapTeleporterComponent that this one is linked to + /// + [DataField, AutoNetworkedField] + public EntityUid? LinkedEnt; + + /// + /// the time at which ends and the teleportation occurs + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public TimeSpan? TeleportTime; + + /// + /// Delay after starting the teleport and it occuring. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public TimeSpan TeleportDelay = TimeSpan.FromSeconds(2.5f); + + /// + /// The time at which ends and teleportation can occur again. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public TimeSpan NextTeleportUse; + + /// + /// A minimum waiting period inbetween teleports. + /// + [DataField, ViewVariables(VVAccess.ReadWrite)] + public TimeSpan Cooldown = TimeSpan.FromMinutes(5); + + /// + /// Sound played when teleportation begins + /// + [DataField] + public SoundSpecifier? TeleportSound = new SoundPathSpecifier("/Audio/Weapons/flash.ogg"); + + /// + /// A whitelist for what entities are valid for . + /// + [DataField] + public EntityWhitelist TeleporterWhitelist = new(); +} + +[Serializable, NetSerializable] +public enum SwapTeleporterVisuals : byte +{ + Linked +} diff --git a/Content.Shared/Teleportation/Systems/SwapTeleporterSystem.cs b/Content.Shared/Teleportation/Systems/SwapTeleporterSystem.cs new file mode 100644 index 0000000000..e900700e11 --- /dev/null +++ b/Content.Shared/Teleportation/Systems/SwapTeleporterSystem.cs @@ -0,0 +1,246 @@ +using Content.Shared.Examine; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Teleportation.Components; +using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map.Components; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Timing; + +namespace Content.Shared.Teleportation.Systems; + +/// +/// This handles +/// +public sealed class SwapTeleporterSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private EntityQuery _xformQuery; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnInteract); + SubscribeLocalEvent>(OnGetAltVerb); + SubscribeLocalEvent(OnActivateInWorld); + SubscribeLocalEvent(OnExamined); + + SubscribeLocalEvent(OnUnpaused); + SubscribeLocalEvent(OnShutdown); + + _xformQuery = GetEntityQuery(); + } + + private void OnInteract(Entity ent, ref AfterInteractEvent args) + { + var (uid, comp) = ent; + if (args.Target == null) + return; + + var target = args.Target.Value; + + if (!TryComp(target, out var targetComp)) + return; + + if (!comp.TeleporterWhitelist.IsValid(target, EntityManager) || + !targetComp.TeleporterWhitelist.IsValid(uid, EntityManager)) + { + return; + } + + if (comp.LinkedEnt != null) + { + _popup.PopupClient(Loc.GetString("swap-teleporter-popup-link-fail-already"), uid, args.User); + return; + } + + if (targetComp.LinkedEnt != null) + { + _popup.PopupClient(Loc.GetString("swap-teleporter-popup-link-fail-already-other"), uid, args.User); + return; + } + + comp.LinkedEnt = target; + targetComp.LinkedEnt = uid; + Dirty(uid, comp); + Dirty(target, targetComp); + _appearance.SetData(uid, SwapTeleporterVisuals.Linked, true); + _appearance.SetData(target, SwapTeleporterVisuals.Linked, true); + _popup.PopupClient(Loc.GetString("swap-teleporter-popup-link-create"), uid, args.User); + } + + private void OnGetAltVerb(Entity ent, ref GetVerbsEvent args) + { + var (uid, comp) = ent; + if (!args.CanAccess || !args.CanInteract || args.Hands == null || comp.TeleportTime != null) + return; + + if (!TryComp(comp.LinkedEnt, out var otherComp) || otherComp.TeleportTime != null) + return; + + var user = args.User; + args.Verbs.Add(new AlternativeVerb + { + Text = Loc.GetString("swap-teleporter-verb-destroy-link"), + Priority = 1, + Act = () => + { + DestroyLink((uid, comp), user); + } + }); + } + + private void OnActivateInWorld(Entity ent, ref ActivateInWorldEvent args) + { + var (uid, comp) = ent; + var user = args.User; + if (comp.TeleportTime != null) + return; + + if (comp.LinkedEnt == null) + { + _popup.PopupClient(Loc.GetString("swap-teleporter-popup-teleport-cancel-link"), ent, user); + return; + } + + // don't allow teleporting to happen if the linked one is already teleporting + if (!TryComp(comp.LinkedEnt, out var otherComp) + || otherComp.TeleportTime != null) + { + return; + } + + if (_timing.CurTime < comp.NextTeleportUse) + { + _popup.PopupClient(Loc.GetString("swap-teleporter-popup-teleport-cancel-time"), ent, user); + return; + } + + _audio.PlayPredicted(comp.TeleportSound, uid, user); + _audio.PlayPredicted(otherComp.TeleportSound, comp.LinkedEnt.Value, user); + comp.NextTeleportUse = _timing.CurTime + comp.Cooldown; + comp.TeleportTime = _timing.CurTime + comp.TeleportDelay; + Dirty(uid, comp); + } + + public void DoTeleport(Entity ent) + { + var (uid, comp, xform) = ent; + + comp.TeleportTime = null; + + Dirty(uid, comp); + if (comp.LinkedEnt is not { } linkedEnt) + { + return; + } + + var teleEnt = GetTeleportingEntity((uid, xform)); + var teleEntXform = Transform(teleEnt); + var otherTeleEnt = GetTeleportingEntity((linkedEnt, Transform(linkedEnt))); + var otherTeleEntXform = Transform(otherTeleEnt); + + _popup.PopupEntity(Loc.GetString("swap-teleporter-popup-teleport-other", + ("entity", Identity.Entity(linkedEnt, EntityManager))), + otherTeleEnt, + otherTeleEnt, + PopupType.MediumCaution); + var pos = teleEntXform.Coordinates; + var otherPos = otherTeleEntXform.Coordinates; + + _transform.SetCoordinates(teleEnt, otherPos); + _transform.SetCoordinates(otherTeleEnt, pos); + } + + /// + /// HYAH -link + /// + public void DestroyLink(Entity ent, EntityUid? user) + { + if (!Resolve(ent, ref ent.Comp, false)) + return; + var linkedNullable = ent.Comp.LinkedEnt; + + ent.Comp.LinkedEnt = null; + ent.Comp.TeleportTime = null; + _appearance.SetData(ent, SwapTeleporterVisuals.Linked, false); + Dirty(ent, ent.Comp); + + if (user != null) + _popup.PopupClient(Loc.GetString("swap-teleporter-popup-link-destroyed"), ent, user.Value); + else + _popup.PopupEntity(Loc.GetString("swap-teleporter-popup-link-destroyed"), ent); + + if (linkedNullable is {} linked) + DestroyLink(linked, user); // the linked one is shown globally + } + + private EntityUid GetTeleportingEntity(Entity ent) + { + var parent = ent.Comp.ParentUid; + + if (HasComp(parent) || HasComp(parent)) + return ent; + + if (!_xformQuery.TryGetComponent(parent, out var parentXform) || parentXform.Anchored) + return ent; + + if (!TryComp(parent, out var body) || body.BodyType == BodyType.Static) + return ent; + + return GetTeleportingEntity((parent, parentXform)); + } + + private void OnExamined(Entity ent, ref ExaminedEvent args) + { + var (_, comp) = ent; + using (args.PushGroup(nameof(SwapTeleporterComponent))) + { + var locale = comp.LinkedEnt == null + ? "swap-teleporter-examine-link-absent" + : "swap-teleporter-examine-link-present"; + args.PushMarkup(Loc.GetString(locale)); + + if (_timing.CurTime < comp.NextTeleportUse) + { + args.PushMarkup(Loc.GetString("swap-teleporter-examine-time-remaining", + ("second", (int) ((comp.NextTeleportUse - _timing.CurTime).TotalSeconds + 0.5f)))); + } + } + } + + private void OnUnpaused(Entity ent, ref EntityUnpausedEvent args) + { + ent.Comp.NextTeleportUse += args.PausedTime; + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + DestroyLink((ent, ent), null); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp, out var xform)) + { + if (comp.TeleportTime == null) + continue; + + if (_timing.CurTime < comp.TeleportTime) + continue; + + DoTeleport((uid, comp, xform)); + } + } +} diff --git a/Resources/Locale/en-US/portal/swap-teleporter.ftl b/Resources/Locale/en-US/portal/swap-teleporter.ftl new file mode 100644 index 0000000000..f13fa9be42 --- /dev/null +++ b/Resources/Locale/en-US/portal/swap-teleporter.ftl @@ -0,0 +1,17 @@ +swap-teleporter-popup-link-create = Quantum link established! +swap-teleporter-popup-link-fail-already = Quantum link failed! Link already present on device. +swap-teleporter-popup-link-fail-already-other = Quantum link failed! Link already present on secondary device. +swap-teleporter-popup-link-destroyed = Quantum link destroyed! +swap-teleporter-popup-teleport-cancel-time = It's still recharging! +swap-teleporter-popup-teleport-cancel-link = It's not linked with another device! +swap-teleporter-popup-teleport-other = {CAPITALIZE(THE($entity))} activates, and you find yourself somewhere else. + +swap-teleporter-verb-destroy-link = Destroy Quantum Link + +swap-teleporter-examine-link-present = [color=forestgreen]It is linked to another device.[/color] Alt-Click to break the quantum link. +swap-teleporter-examine-link-absent = [color=yellow]It is not currently linked.[/color] Use on another device to establish a quantum link. +swap-teleporter-examine-time-remaining = Time left to recharge: [color=purple]{$second} second{$second -> + [one]. + *[other]s. +}[/color] + diff --git a/Resources/Locale/en-US/research/technologies.ftl b/Resources/Locale/en-US/research/technologies.ftl index 99d0c20c65..973ef360a9 100644 --- a/Resources/Locale/en-US/research/technologies.ftl +++ b/Resources/Locale/en-US/research/technologies.ftl @@ -56,6 +56,7 @@ research-technology-anomaly-harnessing = Anomaly Core Harnessing research-technology-grappling = Grappling research-technology-abnormal-artifact-manipulation = Abnormal Artifact Manipulation research-technology-gravity-manipulation = Gravity Manipulation +research-technology-quantum-leaping = Quantum Leaping research-technology-advanced-anomaly-research = Advanced Anomaly Research research-technology-rped = Rapid Part Exchange research-technology-super-parts = Super Parts diff --git a/Resources/Prototypes/Entities/Objects/Devices/swapper.yml b/Resources/Prototypes/Entities/Objects/Devices/swapper.yml new file mode 100644 index 0000000000..8a743f4796 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Devices/swapper.yml @@ -0,0 +1,27 @@ +- type: entity + parent: BaseItem + id: DeviceQuantumSpinInverter + name: quantum spin inverter + description: An experimental device that is able to swap the locations of two entities by switching their particles' spin values. Must be linked to another device to function. + components: + - type: Sprite + sprite: Objects/Devices/swapper.rsi + layers: + - state: icon + map: ["base"] + - type: Item + size: Small + - type: Appearance + - type: SwapTeleporter + teleporterWhitelist: + tags: + - QuantumSpinInverter + - type: GenericVisualizer + visuals: + enum.SwapTeleporterVisuals.Linked: + base: + True: { state: linked } + False: { state: icon } + - type: Tag + tags: + - QuantumSpinInverter diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index fe9dcd1d87..0d9e3ad767 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -300,6 +300,7 @@ - FauxTileAstroGrass - FauxTileAstroIce - OreBagOfHolding + - DeviceQuantumSpinInverter - type: EmagLatheRecipes emagDynamicRecipes: - ExplosivePayload diff --git a/Resources/Prototypes/Recipes/Lathes/devices.yml b/Resources/Prototypes/Recipes/Lathes/devices.yml index d541c36353..1a1f5326e3 100644 --- a/Resources/Prototypes/Recipes/Lathes/devices.yml +++ b/Resources/Prototypes/Recipes/Lathes/devices.yml @@ -188,6 +188,15 @@ Glass: 400 Silver: 200 +- type: latheRecipe + id: DeviceQuantumSpinInverter + result: DeviceQuantumSpinInverter + completetime: 5 + materials: + Steel: 700 + Glass: 100 + Uranium: 100 + - type: latheRecipe id: WeaponProtoKineticAccelerator result: WeaponProtoKineticAccelerator diff --git a/Resources/Prototypes/Research/experimental.yml b/Resources/Prototypes/Research/experimental.yml index c6289fa9c7..bbb6ce568c 100644 --- a/Resources/Prototypes/Research/experimental.yml +++ b/Resources/Prototypes/Research/experimental.yml @@ -152,3 +152,15 @@ recipeUnlocks: - WeaponForceGun - WeaponTetherGun + +- type: technology + id: QuantumLeaping + name: research-technology-quantum-leaping + icon: + sprite: Objects/Devices/swapper.rsi + state: icon + discipline: Experimental + tier: 3 + cost: 10000 + recipeUnlocks: + - DeviceQuantumSpinInverter diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index abc5cfa483..d982cfbd5d 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -931,6 +931,9 @@ - type: Tag id: ProximitySensor +- type: Tag + id: QuantumSpinInverter + - type: Tag id: Radio diff --git a/Resources/Textures/Objects/Devices/swapper.rsi/icon.png b/Resources/Textures/Objects/Devices/swapper.rsi/icon.png new file mode 100644 index 0000000000..8aef40e4b8 Binary files /dev/null and b/Resources/Textures/Objects/Devices/swapper.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Devices/swapper.rsi/inhand-left.png b/Resources/Textures/Objects/Devices/swapper.rsi/inhand-left.png new file mode 100644 index 0000000000..08b70fe4c8 Binary files /dev/null and b/Resources/Textures/Objects/Devices/swapper.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Devices/swapper.rsi/inhand-right.png b/Resources/Textures/Objects/Devices/swapper.rsi/inhand-right.png new file mode 100644 index 0000000000..39b94bfa0a Binary files /dev/null and b/Resources/Textures/Objects/Devices/swapper.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Devices/swapper.rsi/linked.png b/Resources/Textures/Objects/Devices/swapper.rsi/linked.png new file mode 100644 index 0000000000..6c57487797 Binary files /dev/null and b/Resources/Textures/Objects/Devices/swapper.rsi/linked.png differ diff --git a/Resources/Textures/Objects/Devices/swapper.rsi/meta.json b/Resources/Textures/Objects/Devices/swapper.rsi/meta.json new file mode 100644 index 0000000000..40b7f3a037 --- /dev/null +++ b/Resources/Textures/Objects/Devices/swapper.rsi/meta.json @@ -0,0 +1,36 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "tgstation at https://github.com/tgstation/tgstation/commit/71a1fee2f13730adee5302d34bfa0f0262314d63", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "linked", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +}