Pinpointer (#5131)

* Add pinpointer sprites

* Start working on pinpointer

* Updated pinpointer

* Working on visuals

* Working on a pinpointer and eye rotation

* Add client/server pinpointers systems

* Minor cleanup

* Add distance support

* Add nuke tag

* Remove redundant flag and add pinpointer to caps locker

* Disable rotation of pinpointer

* Fixed distance

Co-authored-by: Alexander Evgrashin <evgrashin.adl@gmail.com>
This commit is contained in:
Alex Evgrashin
2021-11-04 00:35:34 +03:00
committed by GitHub
parent b29e826102
commit c1cf22d97e
29 changed files with 686 additions and 1 deletions

View File

@@ -0,0 +1,88 @@
using Content.Shared.Pinpointer;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
namespace Content.Client.Pinpointer
{
public sealed class ClientPinpointerSystem : SharedPinpointerSystem
{
[Dependency] private readonly IEyeManager _eyeManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PinpointerComponent, ComponentHandleState>(HandleCompState);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
// we want to show pinpointers arrow direction relative
// to players eye rotation (like it was in SS13)
// because eye can change it rotation anytime
// we need to update this arrow in a update loop
foreach (var uid in ActivePinpointers)
{
UpdateEyeDir(uid);
}
}
private void HandleCompState(EntityUid uid, PinpointerComponent pinpointer, ref ComponentHandleState args)
{
if (args.Current is not PinpointerComponentState state) return;
SetActive(uid, state.IsActive, pinpointer);
SetDirection(uid, state.DirectionToTarget, pinpointer);
SetDistance(uid, state.DistanceToTarget, pinpointer);
UpdateAppearance(uid, pinpointer);
UpdateEyeDir(uid, pinpointer);
}
private void UpdateAppearance(EntityUid uid, PinpointerComponent? pinpointer = null,
AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref pinpointer, ref appearance))
return;
appearance.SetData(PinpointerVisuals.IsActive, pinpointer.IsActive);
appearance.SetData(PinpointerVisuals.TargetDistance, pinpointer.DistanceToTarget);
}
private void UpdateDirAppearance(EntityUid uid, Direction dir,PinpointerComponent? pinpointer = null,
AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref pinpointer, ref appearance))
return;
appearance.SetData(PinpointerVisuals.TargetDirection, dir);
}
/// <summary>
/// Transform pinpointer arrow from world space to eye space
/// And send it to the appearance component
/// </summary>
private void UpdateEyeDir(EntityUid uid, PinpointerComponent? pinpointer = null)
{
if (!Resolve(uid, ref pinpointer))
return;
var worldDir = pinpointer.DirectionToTarget;
if (worldDir == Direction.Invalid)
{
UpdateDirAppearance(uid, Direction.Invalid, pinpointer);
return;
}
var eye = _eyeManager.CurrentEye;
var angle = worldDir.ToAngle() + eye.Rotation;
var eyeDir = angle.GetDir();
UpdateDirAppearance(uid, eyeDir, pinpointer);
}
}
}

View File

@@ -0,0 +1,60 @@
using Content.Shared.Pinpointer;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Shared.Maths;
namespace Content.Client.Pinpointer
{
[UsedImplicitly]
public class PinpointerVisualizer : AppearanceVisualizer
{
public override void OnChangeData(AppearanceComponent component)
{
base.OnChangeData(component);
if (!component.Owner.TryGetComponent<SpriteComponent>(out var sprite))
return;
// check if pinpointer screen is active
if (!component.TryGetData(PinpointerVisuals.IsActive, out bool isActive) || !isActive)
{
sprite.LayerSetVisible(PinpointerLayers.Screen, false);
return;
}
// check if it has direction to target
sprite.LayerSetVisible(PinpointerLayers.Screen, true);
sprite.LayerSetRotation(PinpointerLayers.Screen, Angle.Zero);
if (!component.TryGetData(PinpointerVisuals.TargetDirection, out Direction dir) || dir == Direction.Invalid)
{
sprite.LayerSetState(PinpointerLayers.Screen, "pinonnull");
return;
}
// check distance to target
if (!component.TryGetData(PinpointerVisuals.TargetDistance, out Distance dis))
dis = Distance.UNKNOWN;
switch (dis)
{
case Distance.REACHED:
sprite.LayerSetState(PinpointerLayers.Screen, "pinondirect");
break;
case Distance.CLOSE:
sprite.LayerSetState(PinpointerLayers.Screen, "pinonclose");
sprite.LayerSetRotation(PinpointerLayers.Screen, dir.ToAngle());
break;
case Distance.MEDIUM:
sprite.LayerSetState(PinpointerLayers.Screen, "pinonmedium");
sprite.LayerSetRotation(PinpointerLayers.Screen, dir.ToAngle());
break;
case Distance.FAR:
case Distance.UNKNOWN:
sprite.LayerSetState(PinpointerLayers.Screen, "pinonfar");
sprite.LayerSetRotation(PinpointerLayers.Screen, dir.ToAngle());
break;
}
}
}
}

View File

@@ -0,0 +1,158 @@
using Content.Shared.Interaction;
using Content.Shared.Pinpointer;
using Content.Shared.Whitelist;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using System.Collections.Generic;
using System.Linq;
namespace Content.Server.Pinpointer
{
public sealed class ServerPinpointerSystem : SharedPinpointerSystem
{
[Dependency] private readonly IEntityLookup _entityLookup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PinpointerComponent, UseInHandEvent>(OnUseInHand);
}
private void OnUseInHand(EntityUid uid, PinpointerComponent component, UseInHandEvent args)
{
TogglePinpointer(uid, component);
// try to find target from whitelist
if (component.IsActive && component.Whitelist != null)
{
var target = FindTargetFromWhitelist(uid, component.Whitelist);
SetTarget(uid, target, component);
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// because target or pinpointer can move
// we need to update pinpointers arrow each frame
foreach (var uid in ActivePinpointers)
{
UpdateDirectionToTarget(uid);
}
}
/// <summary>
/// Try to find the closest entity from whitelist on a current map
/// Will return null if can't find anything
/// </summary>
private EntityUid? FindTargetFromWhitelist(EntityUid uid, EntityWhitelist whitelist,
ITransformComponent? transform = null)
{
if (!Resolve(uid, ref transform))
return null;
var mapId = transform.MapID;
var ents = _entityLookup.GetEntitiesInMap(mapId);
// sort all entities in distance increasing order
var l = new SortedList<float, EntityUid>();
foreach (var e in ents)
{
if (whitelist.IsValid(e))
{
var dist = (e.Transform.WorldPosition - transform.WorldPosition).LengthSquared;
l.TryAdd(dist, e.Uid);
}
}
// return uid with a smallest distacne
return l.Count > 0 ? l.First().Value : null;
}
/// <summary>
/// Set pinpointers target to track
/// </summary>
public void SetTarget(EntityUid uid, EntityUid? target, PinpointerComponent? pinpointer = null)
{
if (!Resolve(uid, ref pinpointer))
return;
if (pinpointer.Target == target)
return;
pinpointer.Target = target;
if (pinpointer.IsActive)
UpdateDirectionToTarget(uid, pinpointer);
}
/// <summary>
/// Update direction from pinpointer to selected target (if it was set)
/// </summary>
private void UpdateDirectionToTarget(EntityUid uid, PinpointerComponent? pinpointer = null)
{
if (!Resolve(uid, ref pinpointer))
return;
var target = pinpointer.Target;
if (target == null || !EntityManager.EntityExists(target.Value))
{
SetDirection(uid, Direction.Invalid, pinpointer);
SetDistance(uid, Distance.UNKNOWN, pinpointer);
return;
}
var dirVec = CalculateDirection(uid, target.Value);
if (dirVec != null)
{
var dir = dirVec.Value.GetDir();
SetDirection(uid, dir, pinpointer);
var dist = CalculateDistance(uid, dirVec.Value, pinpointer);
SetDistance(uid, dist, pinpointer);
}
else
{
SetDirection(uid, Direction.Invalid, pinpointer);
SetDistance(uid, Distance.UNKNOWN, pinpointer);
}
}
/// <summary>
/// Calculate direction from pinUid to trgUid
/// </summary>
/// <returns>Null if failed to caluclate distance between two entities</returns>
private Vector2? CalculateDirection(EntityUid pinUid, EntityUid trgUid)
{
// check if entities have transform component
if (!EntityManager.TryGetComponent(pinUid, out ITransformComponent? pin))
return null;
if (!EntityManager.TryGetComponent(trgUid, out ITransformComponent? trg))
return null;
// check if they are on same map
if (pin.MapID != trg.MapID)
return null;
// get world direction vector
var dir = (trg.WorldPosition - pin.WorldPosition);
return dir;
}
private Distance CalculateDistance(EntityUid uid, Vector2 vec, PinpointerComponent? pinpointer = null)
{
if (!Resolve(uid, ref pinpointer))
return Distance.UNKNOWN;
var dist = vec.Length;
if (dist <= pinpointer.ReachedDistance)
return Distance.REACHED;
else if (dist <= pinpointer.CloseDistance)
return Distance.CLOSE;
else if (dist <= pinpointer.MediumDistance)
return Distance.MEDIUM;
else
return Distance.FAR;
}
}
}

View File

@@ -0,0 +1,54 @@
using System;
using Content.Shared.Whitelist;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Pinpointer
{
[RegisterComponent]
[NetworkedComponent]
[Friend(typeof(SharedPinpointerSystem))]
public class PinpointerComponent : Component
{
public override string Name => "Pinpointer";
[DataField("whitelist")]
public EntityWhitelist? Whitelist;
[DataField("mediumDistance")]
public float MediumDistance = 16f;
[DataField("closeDistance")]
public float CloseDistance = 8f;
[DataField("reachedDistance")]
public float ReachedDistance = 1f;
public EntityUid? Target = null;
public bool IsActive = false;
public Direction DirectionToTarget = Direction.Invalid;
public Distance DistanceToTarget = Distance.UNKNOWN;
}
[Serializable, NetSerializable]
public sealed class PinpointerComponentState : ComponentState
{
public bool IsActive;
public Direction DirectionToTarget;
public Distance DistanceToTarget;
}
[Serializable, NetSerializable]
public enum Distance : byte
{
UNKNOWN,
REACHED,
CLOSE,
MEDIUM,
FAR
}
}

View File

@@ -0,0 +1,19 @@
using Robust.Shared.Serialization;
using System;
namespace Content.Shared.Pinpointer
{
[Serializable, NetSerializable]
public enum PinpointerVisuals : byte
{
IsActive,
TargetDirection,
TargetDistance
}
public enum PinpointerLayers : byte
{
Base,
Screen
}
}

View File

@@ -0,0 +1,93 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.Maths;
namespace Content.Shared.Pinpointer
{
public abstract class SharedPinpointerSystem : EntitySystem
{
protected readonly HashSet<EntityUid> ActivePinpointers = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PinpointerComponent, ComponentGetState>(GetCompState);
}
private void GetCompState(EntityUid uid, PinpointerComponent pinpointer, ref ComponentGetState args)
{
args.State = new PinpointerComponentState
{
IsActive = pinpointer.IsActive,
DirectionToTarget = pinpointer.DirectionToTarget,
DistanceToTarget = pinpointer.DistanceToTarget
};
}
/// <summary>
/// Manually set distance from pinpointer to target
/// </summary>
public void SetDistance(EntityUid uid, Distance distance, PinpointerComponent? pinpointer = null)
{
if (!Resolve(uid, ref pinpointer))
return;
if (distance == pinpointer.DistanceToTarget)
return;
pinpointer.DistanceToTarget = distance;
pinpointer.Dirty();
}
/// <summary>
/// Manually set pinpointer arrow direction
/// </summary>
public void SetDirection(EntityUid uid, Direction directionToTarget, PinpointerComponent? pinpointer = null)
{
if (!Resolve(uid, ref pinpointer))
return;
if (directionToTarget == pinpointer.DirectionToTarget)
return;
pinpointer.DirectionToTarget = directionToTarget;
pinpointer.Dirty();
}
/// <summary>
/// Activate/deactivate pinpointer screen. If it has target it will start tracking it.
/// </summary>
public void SetActive(EntityUid uid, bool isActive, PinpointerComponent? pinpointer = null)
{
if (!Resolve(uid, ref pinpointer))
return;
if (isActive == pinpointer.IsActive)
return;
// add-remove pinpointer from update list
if (isActive)
ActivePinpointers.Add(uid);
else
ActivePinpointers.Remove(uid);
pinpointer.IsActive = isActive;
pinpointer.Dirty();
}
/// <summary>
/// Toggle Pinpointer screen. If it has target it will start tracking it.
/// </summary>
/// <returns>True if pinpointer was activated, false otherwise</returns>
public bool TogglePinpointer(EntityUid uid, PinpointerComponent? pinpointer = null)
{
if (!Resolve(uid, ref pinpointer))
return false;
var isActive = !pinpointer.IsActive;
SetActive(uid, isActive, pinpointer);
return isActive;
}
}
}

View File

@@ -35,6 +35,8 @@
contents:
- id: NukeDisk
prob: 1
- id: PinpointerNuclear
prob: 1
- id: CaptainIDCard
prob: 1
- id: ClothingHeadHatCaptain

View File

@@ -0,0 +1,33 @@
- type: entity
name: pinpointer
description: A handheld tracking device that locks onto certain signals.
parent: BaseItem
id: PinpointerBase
abstract: true
components:
- type: Transform
noRot: True
- type: Sprite
netsync: false
sprite: Objects/Devices/pinpointer.rsi
layers:
- state: pinpointer
map: ["enum.PinpointerLayers.Base"]
- state: pinonnull
map: ["enum.PinpointerLayers.Screen"]
- type: Item
sprite: Objects/Devices/pinpointer.rsi
- type: Pinpointer
- type: Appearance
visuals:
- type: PinpointerVisualizer
- type: entity
name: pinpointer
id: PinpointerNuclear
parent: PinpointerBase
components:
- type: Pinpointer
whitelist:
tags:
- NukeDisk

View File

@@ -4,6 +4,9 @@
id: NukeDisk
description: A nuclear auth disk, capable of arming a nuke if used along with a code. Note from nanotrasen reads "THIS IS YOUR MOST IMPORTANT POSESSION, SECURE DAT FUKKEN DISK!"
components:
- type: Tag
tags:
- NukeDisk
- type: Sprite
netsync: false
sprite: Objects/Misc/nukedisk.rsi
@@ -14,6 +17,17 @@
state: icon
- type: entity
parent: NukeDisk
name: nuclear authentication disk
parent: BaseItem
id: NukeDiskFake
suffix: Fake
description: A nuclear auth disk, capable of.. WAIT THIS IS JUST PAINTED PLASTIC, FUCK-
components:
- type: Sprite
netsync: false
sprite: Objects/Misc/nukedisk.rsi
state: icon
- type: Item
size: 12
sprite: Objects/Misc/nukedisk.rsi
state: icon

View File

@@ -223,3 +223,5 @@
- type: Tag
id: HideContextMenu
- type: Tag
id: NukeDisk

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 B

View File

@@ -0,0 +1,162 @@
{
"version": 1,
"size": {
"x": 32,
"y": 32
},
"license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/59f2a4e10e5ba36033c9734ddebfbbdc6157472d",
"states": [
{
"name": "pinonalert",
"directions": 8,
"delays": [
[
0.2,
0.2
],
[
0.2,
0.2
],
[
0.2,
0.2
],
[
0.2,
0.2
],
[
0.2,
0.2
],
[
0.2,
0.2
],
[
0.2,
0.2
],
[
0.2,
0.2
]
]
},
{
"name": "pinonalertdirect",
"delays": [
[
0.2,
0.2
]
]
},
{
"name": "pinonalertnull",
"delays": [
[
0.2,
0.2
]
]
},
{
"name": "pinonclose",
"delays": [
[
0.2,
0.2
]
]
},
{
"name": "pinondirect",
"delays": [
[
0.2,
0.2
]
]
},
{
"name": "pinondirectlarge",
"delays": [
[
0.2,
0.2
]
]
},
{
"name": "pinondirectsmall",
"delays": [
[
0.2,
0.2
]
]
},
{
"name": "pinondirectxtrlarge",
"delays": [
[
0.2,
0.2
]
]
},
{
"name": "pinonfar",
"delays": [
[
0.6,
0.2
]
]
},
{
"name": "pinonmedium",
"delays": [
[
0.4,
0.2
]
]
},
{
"name": "pinonnull",
"delays": [
[
0.2,
0.2
]
]
},
{
"name": "pinpointer"
},
{
"name": "pinpointer_crew"
},
{
"name": "pinpointer_crewprox"
},
{
"name": "pinpointer_syndicate"
},
{
"name": "pinpointer_way"
},
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B