diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs index a81c2b3f34..1b9894011e 100644 --- a/Content.Client/EntryPoint.cs +++ b/Content.Client/EntryPoint.cs @@ -26,6 +26,7 @@ using System; using Content.Client.Chat; using Content.Client.GameObjects.Components; using Content.Client.GameObjects.Components.Mobs; +using Content.Client.GameObjects.Components.Movement; using Content.Client.GameObjects.Components.Research; using Content.Client.GameObjects.Components.Sound; using Content.Client.Interfaces.Chat; @@ -33,6 +34,7 @@ using Content.Client.UserInterface; using Content.Shared.GameObjects.Components.Markers; using Content.Shared.GameObjects.Components.Materials; using Content.Shared.GameObjects.Components.Mobs; +using Content.Shared.GameObjects.Components.Movement; using Content.Shared.GameObjects.Components.Research; using Robust.Client.Interfaces.State; using Robust.Client.Interfaces.UserInterface; diff --git a/Content.Client/GameObjects/Components/Movement/HandTeleporterVisualizer2D.cs b/Content.Client/GameObjects/Components/Movement/HandTeleporterVisualizer2D.cs new file mode 100644 index 0000000000..06b67654ce --- /dev/null +++ b/Content.Client/GameObjects/Components/Movement/HandTeleporterVisualizer2D.cs @@ -0,0 +1,35 @@ +using System; +using Content.Shared.GameObjects.Components.Movement; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.GameObjects.Components; + +namespace Content.Client.GameObjects.Components.Movement +{ + [UsedImplicitly] + public class HandTeleporterVisualizer2D : AppearanceVisualizer + { + + public override void OnChangeData(AppearanceComponent component) + { + var sprite = component.Owner.GetComponent(); + if (!component.TryGetData(TeleporterVisuals.VisualState, out TeleporterVisualState state)) + { + state = TeleporterVisualState.Ready; + } + + switch (state) + { + case TeleporterVisualState.Charging: + sprite.LayerSetState(0, "charging"); + break; + case TeleporterVisualState.Ready: + sprite.LayerSetState(0, "ready"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + +} diff --git a/Content.Client/GameObjects/Components/Movement/PortalVisualizer2D.cs b/Content.Client/GameObjects/Components/Movement/PortalVisualizer2D.cs new file mode 100644 index 0000000000..a2eed9a340 --- /dev/null +++ b/Content.Client/GameObjects/Components/Movement/PortalVisualizer2D.cs @@ -0,0 +1,55 @@ +using Content.Shared.GameObjects.Components.Movement; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Interfaces.GameObjects.Components; +using Robust.Shared.Interfaces.GameObjects; + +namespace Content.Client.GameObjects.Components.Movement +{ + [UsedImplicitly] + public class PortalVisualizer2D : AppearanceVisualizer + { + public override void InitializeEntity(IEntity entity) + { + base.InitializeEntity(entity); + + var sprite = entity.GetComponent(); + + sprite.LayerMapSet(Layers.Portal, sprite.AddLayerState("portal-pending")); + sprite.LayerSetShader(Layers.Portal, "unshaded"); + + } + + public override void OnChangeData(AppearanceComponent component) + { + base.OnChangeData(component); + + var sprite = component.Owner.GetComponent(); + if (component.TryGetData(PortalVisuals.State, out var state)) + { + switch (state) + { + case PortalState.Pending: + sprite.LayerSetState(Layers.Portal, "portal-pending"); + break; + // TODO: Spritework here? + case PortalState.UnableToTeleport: + sprite.LayerSetState(Layers.Portal, "portal-unconnected"); + break; + case PortalState.RecentlyTeleported: + sprite.LayerSetState(Layers.Portal, "portal-unconnected"); + break; + } + } + else + { + sprite.LayerSetState(Layers.Portal, "portal-pending"); + } + } + + enum Layers + { + Portal + } + } +} diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index 12c99c3ebd..1fa2ed4e6f 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -58,6 +58,7 @@ using Robust.Shared.Interfaces.Log; using Content.Server.GameObjects.Components.Explosive; using Content.Server.GameObjects.Components.Items; using Content.Server.GameObjects.Components.Triggers; +using Content.Shared.GameObjects.Components.Movement; namespace Content.Server { @@ -178,6 +179,9 @@ namespace Content.Server factory.RegisterReference(); factory.Register(); + factory.Register(); + factory.Register(); + factory.Register(); factory.Register(); diff --git a/Content.Server/GameObjects/Components/Movement/ServerPortalComponent.cs b/Content.Server/GameObjects/Components/Movement/ServerPortalComponent.cs new file mode 100644 index 0000000000..70eb876a8e --- /dev/null +++ b/Content.Server/GameObjects/Components/Movement/ServerPortalComponent.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using Content.Shared.GameObjects.Components.Movement; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Serialization; +using Robust.Shared.Timers; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Movement +{ + public class ServerPortalComponent : SharedPortalComponent + { +#pragma warning disable 649 + [Dependency] private readonly IServerEntityManager _serverEntityManager; +#pragma warning restore 649 + + // Potential improvements: Different sounds, + // Add Gateways + // More efficient form of GetEntitiesIntersecting, + // Put portal above most other things layer-wise + // Add telefragging (get entities on connecting portal and force brute damage) + + private AppearanceComponent _appearanceComponent; + private IEntity _connectingTeleporter; + private PortalState _state = PortalState.Pending; + [ViewVariables(VVAccess.ReadWrite)] private float _individualPortalCooldown; + [ViewVariables] private float _overallPortalCooldown; + [ViewVariables] private bool _onCooldown; + [ViewVariables] private string _departureSound; + [ViewVariables] private string _arrivalSound; + public List immuneEntities = new List(); // K + [ViewVariables(VVAccess.ReadWrite)] private float _aliveTime; + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + // How long will the portal stay up: 0 is infinite + serializer.DataField(ref _aliveTime, "alive_time", 10.0f); + // How long before a specific person can go back into it + serializer.DataField(ref _individualPortalCooldown, "individual_cooldown", 2.1f); + // How long before anyone can go in it + serializer.DataField(ref _overallPortalCooldown, "overall_cooldown", 2.0f); + serializer.DataField(ref _departureSound, "departure_sound", "/Audio/effects/teleport_departure.ogg"); + serializer.DataField(ref _arrivalSound, "arrival_sound", "/Audio/effects/teleport_arrival.ogg"); + } + + public override void Initialize() + { + base.Initialize(); + _appearanceComponent = Owner.GetComponent(); + } + + public override void OnAdd() + { + // This will blow up an entity it's attached to + base.OnAdd(); + if (Owner.TryGetComponent(out var collide)) + { + collide.IsHardCollidable = false; + } + + _state = PortalState.Pending; + if (_aliveTime > 0) + { + Timer.Spawn(TimeSpan.FromSeconds(_aliveTime), () => Owner.Delete()); + } + } + + public override void OnRemove() + { + _appearanceComponent = null; + + base.OnRemove(); + } + + public bool CanBeConnected() + { + if (_connectingTeleporter == null) + { + return true; + } + return false; + } + + public void TryConnectPortal(IEntity otherPortal) + { + if (otherPortal.TryGetComponent(out var connectedPortal) && connectedPortal.CanBeConnected()) + { + _connectingTeleporter = otherPortal; + connectedPortal._connectingTeleporter = Owner; + TryChangeState(PortalState.Pending); + } + } + + public void TryChangeState(PortalState targetState) + { + if (Owner == null) + { + return; + } + + _state = targetState; + if (_appearanceComponent != null) + { + _appearanceComponent.SetData(PortalVisuals.State, _state); + } + } + + private void releaseCooldown(IEntity entity) + { + if (immuneEntities.Contains(entity)) + { + immuneEntities.Remove(entity); + } + + if (_connectingTeleporter != null && + _connectingTeleporter.TryGetComponent(out var otherPortal)) + { + otherPortal.immuneEntities.Remove(entity); + } + } + + public IEnumerable GetPortableEntities() + { + foreach (var entity in _serverEntityManager.GetEntitiesIntersecting(Owner)) + { + if (IsEntityPortable(entity)) + { + yield return entity; + } + } + } + + private bool IsEntityPortable(IEntity entity) + { + // TODO: Check if it's slotted etc. Otherwise the slot item itself gets ported. + if (!immuneEntities.Contains(entity) && entity.HasComponent()) + { + return true; + } + return false; + } + + // TODO: Fix portal updates for performance + public void OnUpdate() + { + if (_onCooldown == false) + { + foreach (var entity in GetPortableEntities()) + { + TryPortalEntity(entity); + break; + } + } + } + + public void StartCooldown() + { + if (_overallPortalCooldown > 0 && _onCooldown == false) + { + _onCooldown = true; + TryChangeState(PortalState.RecentlyTeleported); + if (_connectingTeleporter != null) + { + _connectingTeleporter.TryGetComponent(out var otherPortal); + if (otherPortal != null) + { + otherPortal.TryChangeState(PortalState.RecentlyTeleported); + Timer.Spawn(TimeSpan.FromSeconds(_overallPortalCooldown), () => + { + _onCooldown = false; + TryChangeState(PortalState.Pending); + otherPortal.TryChangeState(PortalState.Pending); + }); + } + } + } + else + { + // Just in case? + _onCooldown = false; + } + } + + public void TryPortalEntity(IEntity entity) + { + + if (!immuneEntities.Contains(entity)) + { + var position = _connectingTeleporter.Transform.GridPosition; + var soundPlayer = IoCManager.Resolve().GetEntitySystem(); + + // Departure + // Do we need to rate-limit sounds to stop ear BLAST? + soundPlayer.Play(_departureSound, entity.Transform.GridPosition); + entity.Transform.DetachParent(); + entity.Transform.GridPosition = position; + soundPlayer.Play(_arrivalSound, entity.Transform.GridPosition); + TryChangeState(PortalState.RecentlyTeleported); + // To stop spam teleporting. Could potentially look at adding a timer to flush this from the portal + immuneEntities.Add(entity); + _connectingTeleporter.GetComponent().immuneEntities.Add(entity); + Timer.Spawn(TimeSpan.FromSeconds(_individualPortalCooldown), () => releaseCooldown(entity)); + StartCooldown(); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/Movement/ServerTeleporterComponent.cs b/Content.Server/GameObjects/Components/Movement/ServerTeleporterComponent.cs new file mode 100644 index 0000000000..78d97a2125 --- /dev/null +++ b/Content.Server/GameObjects/Components/Movement/ServerTeleporterComponent.cs @@ -0,0 +1,261 @@ +using System; +using System.Linq; +using Content.Server.GameObjects.EntitySystems; +using Content.Shared.GameObjects.Components.Movement; +using Robust.Server.GameObjects; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Map; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; +using Robust.Shared.Serialization; +using Robust.Shared.Timers; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.Movement +{ + + public class ServerTeleporterComponent : Component, IAfterAttack + { +#pragma warning disable 649 + [Dependency] private readonly IMapManager _mapManager; + [Dependency] private readonly IServerEntityManager _serverEntityManager; +#pragma warning restore 649 + // TODO: Look at MapManager.Map for Beacons to get all entities on grid + public ItemTeleporterState State => _state; + + public override string Name => "ItemTeleporter"; + + [ViewVariables] private float _chargeTime; + [ViewVariables] private float _cooldown; + [ViewVariables] private int _range; + [ViewVariables] private ItemTeleporterState _state; + [ViewVariables] private TeleporterType _teleporterType; + [ViewVariables] private string _departureSound; + [ViewVariables] private string _arrivalSound; + [ViewVariables] private string _cooldownSound; + // If the direct OR random teleport will try to avoid hitting collidables + [ViewVariables] private bool _avoidCollidable; + [ViewVariables] private float _portalAliveTime; + + private AppearanceComponent _appearanceComponent; + + private Random _spreadRandom; + + public override void ExposeData(ObjectSerializer serializer) + { + base.ExposeData(serializer); + + serializer.DataField(ref _teleporterType, "teleporter_type", TeleporterType.Random); + serializer.DataField(ref _range, "range", 15); + serializer.DataField(ref _chargeTime, "charge_time", 0.2f); + serializer.DataField(ref _cooldown, "cooldown", 2.0f); + serializer.DataField(ref _avoidCollidable, "avoid_walls", true); + serializer.DataField(ref _departureSound, "departure_sound", "/Audio/effects/teleport_departure.ogg"); + serializer.DataField(ref _arrivalSound, "arrival_sound", "/Audio/effects/teleport_arrival.ogg"); + serializer.DataField(ref _cooldownSound, "cooldown_sound", null); + serializer.DataField(ref _portalAliveTime, "portal_alive_time", 5.0f); // TODO: Change this to 0 before PR? + } + + public override void OnRemove() + { + _appearanceComponent = null; + + base.OnRemove(); + } + + private void SetState(ItemTeleporterState newState) + { + if (newState == ItemTeleporterState.Cooldown) + { + _appearanceComponent.SetData(TeleporterVisuals.VisualState, TeleporterVisualState.Charging); + } + else + { + _appearanceComponent.SetData(TeleporterVisuals.VisualState, TeleporterVisualState.Ready); + } + _state = newState; + } + + void IAfterAttack.AfterAttack(AfterAttackEventArgs eventArgs) + { + if (_teleporterType == TeleporterType.Directed) + { + var userTarget = eventArgs.ClickLocation.ToWorld(_mapManager); + TryDirectedTeleport(eventArgs.User, userTarget); + } + + if (_teleporterType == TeleporterType.Random) + { + TryRandomTeleport(eventArgs.User); + } + } + + public void TryDirectedTeleport(IEntity user, GridCoordinates grid) + { + // Checks + if (user.Transform.GridPosition.Distance(_mapManager, grid) > _range) + { + return; + } + + if (_state == ItemTeleporterState.On) + { + return; + } + if (_avoidCollidable) + { + foreach (var entity in _serverEntityManager.GetEntitiesIntersecting(grid)) + { + // Added this component to avoid stacking portals and causing shenanigans + // TODO: Doesn't do a great job of stopping stacking portals for directed + if (entity.HasComponent() || entity.HasComponent()) + { + return; + } + } + } + // Start / Continue + if (_state == ItemTeleporterState.Off) + { + SetState(ItemTeleporterState.Charging); + // Play charging sound here if you want + } + + if (_state != ItemTeleporterState.Charging) + { + return; + } + + Timer.Spawn(TimeSpan.FromSeconds(_chargeTime), () => Teleport(user, new Vector2(grid.X, grid.Y))); + StartCooldown(); + } + + public void StartCooldown() + { + SetState(ItemTeleporterState.Cooldown); + Timer.Spawn(TimeSpan.FromSeconds(_chargeTime + _cooldown), () => SetState(ItemTeleporterState.Off)); + if (_cooldownSound != null) + { + var soundPlayer = IoCManager.Resolve().GetEntitySystem(); + soundPlayer.Play(_cooldownSound, Owner); + } + } + + public override void Initialize() + { + _appearanceComponent = Owner.GetComponent(); + _spreadRandom = new Random(Owner.Uid.GetHashCode() ^ DateTime.Now.GetHashCode()); + _state = ItemTeleporterState.Off; + base.Initialize(); + } + + private bool emptySpace(IEntity user, Vector2 target) + { + // TODO: Check the user's spot? Upside is no stacking TPs but downside is they can't unstuck themselves from walls. + foreach (var entity in _serverEntityManager.GetEntitiesIntersecting(user.Transform.MapID, target)) + { + if (entity.HasComponent() || entity.HasComponent()) + { + return false; + } + } + return true; + } + + private Vector2 randomEmptySpot(IEntity user, int range) + { + Vector2 targetVector = user.Transform.GridPosition.Position; + // Definitely a better way to do this + foreach (var i in Enumerable.Range(0, 5)) + { + var randomRange = _spreadRandom.Next(0, range); + var angle = Angle.FromDegrees(_spreadRandom.Next(0, 359)); + targetVector = user.Transform.GridPosition.Position + angle.ToVec() * randomRange; + if (emptySpace(user, targetVector)) + { + return targetVector; + } + if (i == 19) + { + return targetVector; + } + } + + return targetVector; + } + + public void TryRandomTeleport(IEntity user) + { + // Checks + if (_state == ItemTeleporterState.On) + { + return; + } + + Vector2 targetVector; + if (_avoidCollidable) + { + targetVector = randomEmptySpot(user, _range); + } + else + { + var randomRange = _spreadRandom.Next(0, _range); + var angle = Angle.FromDegrees(_spreadRandom.Next(0, 359)); + targetVector = user.Transform.GridPosition.Position + angle.ToVec() * randomRange; + } + // Start / Continue + if (_state == ItemTeleporterState.Off) + { + SetState(ItemTeleporterState.Charging); + } + + if (_state != ItemTeleporterState.Charging) + { + return; + } + + // Seemed easier to just start the cd timer at the same time + Timer.Spawn(TimeSpan.FromSeconds(_chargeTime), () => Teleport(user, targetVector)); + StartCooldown(); + } + + public void Teleport(IEntity user, Vector2 vector) + { + // Messy maybe? + GridCoordinates targetGrid = new GridCoordinates(vector, user.Transform.GridID); + var soundPlayer = IoCManager.Resolve().GetEntitySystem(); + + // If portals use those, otherwise just move em over + if (_portalAliveTime > 0.0f) + { + // Call Delete here as the teleporter should have control over portal longevity + // Departure portal + var departurePortal = _serverEntityManager.SpawnEntityAt("Portal", user.Transform.GridPosition); + departurePortal.TryGetComponent(out var departureComponent); + + // Arrival portal + var arrivalPortal = _serverEntityManager.SpawnEntityAt("Portal", targetGrid); + arrivalPortal.TryGetComponent(out var arrivalComponent); + + // Connect. TODO: If the OnUpdate in ServerPortalComponent is changed this may need to change as well. + arrivalComponent.TryConnectPortal(departurePortal); + } + else + { + // Departure + soundPlayer.Play(_departureSound, user.Transform.GridPosition); + + // Arrival + user.Transform.DetachParent(); + user.Transform.WorldPosition = vector; + soundPlayer.Play(_arrivalSound, user.Transform.GridPosition); + } + + } + } +} diff --git a/Content.Server/GameObjects/Components/Movement/TeleportableComponent.cs b/Content.Server/GameObjects/Components/Movement/TeleportableComponent.cs new file mode 100644 index 0000000000..f5f0701ca4 --- /dev/null +++ b/Content.Server/GameObjects/Components/Movement/TeleportableComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameObjects; +using Robust.Shared.Log; + +namespace Content.Server.GameObjects.Components.Movement +{ + public class TeleportableComponent : Component + { + public override string Name => "Teleportable"; + } +} diff --git a/Content.Server/GameObjects/EntitySystems/PortalSystem.cs b/Content.Server/GameObjects/EntitySystems/PortalSystem.cs new file mode 100644 index 0000000000..77eaf9bdcd --- /dev/null +++ b/Content.Server/GameObjects/EntitySystems/PortalSystem.cs @@ -0,0 +1,25 @@ +using Content.Server.GameObjects.Components.Movement; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; + +namespace Content.Server.GameObjects.EntitySystems +{ + [UsedImplicitly] + public class PortalSystem : EntitySystem + { + public override void Initialize() + { + EntityQuery = new TypeEntityQuery(typeof(ServerPortalComponent)); + } + + public override void Update(float frameTime) + { + foreach (var entity in RelevantEntities) + { + var comp = entity.GetComponent(); + comp.OnUpdate(); + } + } + } +} diff --git a/Content.Shared/GameObjects/Components/Movement/SharedPortal.cs b/Content.Shared/GameObjects/Components/Movement/SharedPortal.cs new file mode 100644 index 0000000000..8f09903953 --- /dev/null +++ b/Content.Shared/GameObjects/Components/Movement/SharedPortal.cs @@ -0,0 +1,25 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Movement +{ + public abstract class SharedPortalComponent : Component { + public override string Name => "Portal"; + } + + [Serializable, NetSerializable] + public enum PortalVisuals + { + State + } + + [Serializable, NetSerializable] + public enum PortalState + { + RecentlyTeleported, + Pending, + UnableToTeleport, + } + +} diff --git a/Content.Shared/GameObjects/Components/Movement/SharedTeleporterComponent.cs b/Content.Shared/GameObjects/Components/Movement/SharedTeleporterComponent.cs new file mode 100644 index 0000000000..de607bb84f --- /dev/null +++ b/Content.Shared/GameObjects/Components/Movement/SharedTeleporterComponent.cs @@ -0,0 +1,48 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Serialization; + +namespace Content.Shared.GameObjects.Components.Movement +{ + public enum ItemTeleporterState + { + Off, + Charging, + On, + Cooldown, + } + + public enum TeleporterType + { + Directed, + Random, + Beacon, + } + + [NetSerializable] + [Serializable] + public enum TeleporterVisuals + { + VisualState, + } + + [NetSerializable] + [Serializable] + public enum TeleporterVisualState + { + Ready, + Charging, + } + + [Serializable, NetSerializable] + public class TeleportMessage : ComponentMessage + { + public readonly GridCoordinates Target; + + public TeleportMessage(GridCoordinates target) + { + Target = target; + } + } +} diff --git a/Content.Shared/GameObjects/ContentNetIDs.cs b/Content.Shared/GameObjects/ContentNetIDs.cs index fe8a7cae1e..a2b54f0dff 100644 --- a/Content.Shared/GameObjects/ContentNetIDs.cs +++ b/Content.Shared/GameObjects/ContentNetIDs.cs @@ -21,5 +21,6 @@ public const uint LATHE = 1016; public const uint LATHE_DATABASE = 1017; public const uint MATERIAL_STORAGE = 1018; + public const uint HAND_TELEPORTER = 1019; } } diff --git a/Resources/Audio/effects/teleport_arrival.ogg b/Resources/Audio/effects/teleport_arrival.ogg new file mode 100644 index 0000000000..650867782b Binary files /dev/null and b/Resources/Audio/effects/teleport_arrival.ogg differ diff --git a/Resources/Audio/effects/teleport_departure.ogg b/Resources/Audio/effects/teleport_departure.ogg new file mode 100644 index 0000000000..650867782b Binary files /dev/null and b/Resources/Audio/effects/teleport_departure.ogg differ diff --git a/Resources/Prototypes/Entities/Mobs.yml b/Resources/Prototypes/Entities/Mobs.yml index d78b70406d..7629db5035 100644 --- a/Resources/Prototypes/Entities/Mobs.yml +++ b/Resources/Prototypes/Entities/Mobs.yml @@ -65,6 +65,7 @@ - type: SpeciesVisualizer2D - type: CombatMode + - type: Teleportable - type: entity id: MobObserver diff --git a/Resources/Prototypes/Entities/Teleporters.yml b/Resources/Prototypes/Entities/Teleporters.yml new file mode 100644 index 0000000000..ef924aad20 --- /dev/null +++ b/Resources/Prototypes/Entities/Teleporters.yml @@ -0,0 +1,62 @@ +- type: entity + name: "BaseHandTele" + parent: BaseItem + id: BaseHandTele + components: + - type: Sprite + netsync: false + sprite: Objects/hand_tele.rsi + state: ready + - type: Icon + sprite: Objects/hand_tele.rsi + state: ready + - type: ItemTeleporter + teleporter_type: Random + - type: Item + Size: 12 + sprite: Objects/hand_tele.rsi + - type: Sound + - type: Appearance + visuals: + - type: HandTeleporterVisualizer2D + +- type: entity + name: "Hand Teleporter - Random" + parent: BaseHandTele + id: RandHandTele + description: "Travel to a random spot in range" + components: + - type: ItemTeleporter + teleporter_type: Random + range: 15 + cooldown: 5 + charge_time: 1 + +- type: entity + name: "Hand Teleporter - Direct" + parent: BaseHandTele + id: DirHandTele + description: "Travel to a specific spot in a short range" + components: + - type: ItemTeleporter + teleporter_type: Directed + range: 5 + cooldown: 2 + charge_time: 0.2 + +- type: entity + name: Portal + id: Portal + description: "Portal to another location" + components: + - type: Collidable + - type: Portal + - type: BoundingBox + aabb: "-0.25,-0.25,0.25,0.25" + - type: Sprite + netsync: false + sprite: "Effects/portal.rsi" + state: portal-pending + - type: Appearance + visuals: + - type: PortalVisualizer2D \ No newline at end of file diff --git a/Resources/Textures/Effects/portal.rsi/meta.json b/Resources/Textures/Effects/portal.rsi/meta.json new file mode 100644 index 0000000000..724c9230f8 --- /dev/null +++ b/Resources/Textures/Effects/portal.rsi/meta.json @@ -0,0 +1,51 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/discordia-space/CEV-Eris/raw/237d8f7894617007d75c71d5d9feb4354c78debd/icons/obj/stationobjs.dmi", + "states": [ + { + "name": "portal-pending", + "directions": 1, + "delays": [[ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ]] + }, + { + "name": "portal-unconnected", + "directions": 1, + "delays": [[ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ]] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Effects/portal.rsi/portal-pending.png b/Resources/Textures/Effects/portal.rsi/portal-pending.png new file mode 100644 index 0000000000..f09e38c608 Binary files /dev/null and b/Resources/Textures/Effects/portal.rsi/portal-pending.png differ diff --git a/Resources/Textures/Effects/portal.rsi/portal-unconnected.png b/Resources/Textures/Effects/portal.rsi/portal-unconnected.png new file mode 100644 index 0000000000..8023e01bc4 Binary files /dev/null and b/Resources/Textures/Effects/portal.rsi/portal-unconnected.png differ diff --git a/Resources/Textures/Objects/hand_tele.png b/Resources/Textures/Objects/hand_tele.png new file mode 100644 index 0000000000..68e8459c5a Binary files /dev/null and b/Resources/Textures/Objects/hand_tele.png differ diff --git a/Resources/Textures/Objects/hand_tele.rsi/charging.png b/Resources/Textures/Objects/hand_tele.rsi/charging.png new file mode 100644 index 0000000000..64fefef232 Binary files /dev/null and b/Resources/Textures/Objects/hand_tele.rsi/charging.png differ diff --git a/Resources/Textures/Objects/hand_tele.rsi/meta.json b/Resources/Textures/Objects/hand_tele.rsi/meta.json new file mode 100644 index 0000000000..64e209f934 --- /dev/null +++ b/Resources/Textures/Objects/hand_tele.rsi/meta.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "size": { + "x": 32, + "y": 32 + }, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/discordia-space/CEV-Eris/raw/237d8f7894617007d75c71d5d9feb4354c78debd/icons/obj/device.dmi", + "states": [ + { + "name": "charging", + "directions": 1, + "delays": [ + [ + 1.0, + 1.0, + 1.0, + 1.0 + ] + ] + }, + { + "name": "ready", + "directions": 1, + "delays": [ + [ + 5.0, + 5.0 + ] + ] + } + ] +} \ No newline at end of file diff --git a/Resources/Textures/Objects/hand_tele.rsi/ready.png b/Resources/Textures/Objects/hand_tele.rsi/ready.png new file mode 100644 index 0000000000..b810596d41 Binary files /dev/null and b/Resources/Textures/Objects/hand_tele.rsi/ready.png differ