Geiger counter (#12082)

This commit is contained in:
Alex Evgrashin
2022-12-07 02:56:52 +01:00
committed by GitHub
parent ae700781b4
commit 04db7d0fdd
32 changed files with 613 additions and 8 deletions

View File

@@ -0,0 +1,95 @@
using Content.Client.Items;
using Content.Client.Radiation.UI;
using Content.Shared.Radiation.Components;
using Content.Shared.Radiation.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
namespace Content.Client.Radiation.Systems;
public sealed class GeigerSystem : SharedGeigerSystem
{
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerAttachSysMessage>(OnAttachedEntityChanged);
SubscribeLocalEvent<GeigerComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<GeigerComponent, ItemStatusCollectMessage>(OnGetStatusMessage);
}
private void OnHandleState(EntityUid uid, GeigerComponent component, ref ComponentHandleState args)
{
if (args.Current is not GeigerComponentState state)
return;
UpdateGeigerSound(uid, state.IsEnabled, state.User, state.DangerLevel, false, component);
component.CurrentRadiation = state.CurrentRadiation;
component.DangerLevel = state.DangerLevel;
component.IsEnabled = state.IsEnabled;
component.User = state.User;
component.UiUpdateNeeded = true;
}
private void OnGetStatusMessage(EntityUid uid, GeigerComponent component, ItemStatusCollectMessage args)
{
if (!component.ShowControl)
return;
args.Controls.Add(new GeigerItemControl(component));
}
private void OnAttachedEntityChanged(PlayerAttachSysMessage ev)
{
// need to go for each component known to client
// and update their geiger sound
foreach (var geiger in EntityQuery<GeigerComponent>())
{
ForceUpdateGeigerSound(geiger.Owner, geiger);
}
}
private void ForceUpdateGeigerSound(EntityUid uid, GeigerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
UpdateGeigerSound(uid, component.IsEnabled, component.User, component.DangerLevel, true, component);
}
private void UpdateGeigerSound(EntityUid uid, bool isEnabled, EntityUid? user,
GeigerDangerLevel dangerLevel, bool force = false, GeigerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
// check if we even need to update sound
if (!force && isEnabled == component.IsEnabled &&
user == component.User && dangerLevel == component.DangerLevel)
{
return;
}
component.Stream?.Stop();
if (!isEnabled || user == null)
return;
if (!component.Sounds.TryGetValue(dangerLevel, out var sounds))
return;
// check that that local player controls entity that is holding geiger counter
if (_playerManager.LocalPlayer == null)
return;
var attachedEnt = _playerManager.LocalPlayer.Session.AttachedEntity;
if (attachedEnt != user)
return;
var sound = _audio.GetSound(sounds);
var param = sounds.Params.WithLoop(true).WithVolume(-4f);
component.Stream = _audio.Play(sound, Filter.Local(), uid, false, param);
}
}

View File

@@ -0,0 +1,53 @@
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Radiation.Components;
using Content.Shared.Radiation.Systems;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.Radiation.UI;
public sealed class GeigerItemControl : Control
{
private readonly GeigerComponent _component;
private readonly RichTextLabel _label;
public GeigerItemControl(GeigerComponent component)
{
_component = component;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
Update();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_component.UiUpdateNeeded)
return;
Update();
}
private void Update()
{
string msg;
if (_component.IsEnabled)
{
var color = SharedGeigerSystem.LevelToColor(_component.DangerLevel);
var currentRads = _component.CurrentRadiation;
var rads = currentRads.ToString("N1");
msg = Loc.GetString("geiger-item-control-status",
("rads", rads), ("color", color));
}
else
{
msg = Loc.GetString("geiger-item-control-disabled");
}
_label.SetMarkup(msg);
_component.UiUpdateNeeded = false;
}
}

View File

@@ -0,0 +1,9 @@
using Content.Server.Radiation.Systems;
namespace Content.Server.Radiation.Events;
/// <summary>
/// Raised when <see cref="RadiationSystem"/> updated all
/// radiation receivers and radiation sources.
/// </summary>
public record struct RadiationSystemUpdatedEvent;

View File

@@ -0,0 +1,165 @@
using Content.Server.Radiation.Components;
using Content.Server.Radiation.Events;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Inventory.Events;
using Content.Shared.Radiation.Components;
using Content.Shared.Radiation.Systems;
using Robust.Shared.GameStates;
namespace Content.Server.Radiation.Systems;
public sealed class GeigerSystem : SharedGeigerSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly RadiationSystem _radiation = default!;
private static readonly float ApproxEqual = 0.01f;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GeigerComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<GeigerComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<GeigerComponent, GotEquippedHandEvent>(OnEquippedHand);
SubscribeLocalEvent<GeigerComponent, GotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<GeigerComponent, GotUnequippedHandEvent>(OnUnequippedHand);
SubscribeLocalEvent<RadiationSystemUpdatedEvent>(OnUpdate);
SubscribeLocalEvent<GeigerComponent, ComponentGetState>(OnGetState);
}
private void OnActivate(EntityUid uid, GeigerComponent component, ActivateInWorldEvent args)
{
if (args.Handled || component.AttachedToSuit)
return;
args.Handled = true;
SetEnabled(uid, component, !component.IsEnabled);
}
private void OnEquipped(EntityUid uid, GeigerComponent component, GotEquippedEvent args)
{
if (component.AttachedToSuit)
SetEnabled(uid, component, true);
SetUser(component, args.Equipee);
}
private void OnEquippedHand(EntityUid uid, GeigerComponent component, GotEquippedHandEvent args)
{
if (component.AttachedToSuit)
return;
SetUser(component, args.User);
}
private void OnUnequipped(EntityUid uid, GeigerComponent component, GotUnequippedEvent args)
{
if (component.AttachedToSuit)
SetEnabled(uid, component, false);
SetUser(component, null);
}
private void OnUnequippedHand(EntityUid uid, GeigerComponent component, GotUnequippedHandEvent args)
{
if (component.AttachedToSuit)
return;
SetUser(component, null);
}
private void OnUpdate(RadiationSystemUpdatedEvent ev)
{
// update only active geiger counters
// deactivated shouldn't have rad receiver component
var query = EntityQuery<GeigerComponent, RadiationReceiverComponent>();
foreach (var (geiger, receiver) in query)
{
var rads = receiver.CurrentRadiation;
SetCurrentRadiation(geiger.Owner, geiger, rads);
}
}
private void OnGetState(EntityUid uid, GeigerComponent component, ref ComponentGetState args)
{
args.State = new GeigerComponentState
{
CurrentRadiation = component.CurrentRadiation,
DangerLevel = component.DangerLevel,
IsEnabled = component.IsEnabled,
User = component.User
};
}
private void SetCurrentRadiation(EntityUid uid, GeigerComponent component, float rads)
{
// check that it's approx equal
if (MathHelper.CloseTo(component.CurrentRadiation, rads, ApproxEqual))
return;
var curLevel = component.DangerLevel;
var newLevel = RadsToLevel(rads);
component.CurrentRadiation = rads;
component.DangerLevel = newLevel;
if (curLevel != newLevel)
{
UpdateAppearance(uid, component);
}
Dirty(component);
}
private void SetUser(GeigerComponent component, EntityUid? user)
{
if (component.User == user)
return;
component.User = user;
Dirty(component);
}
private void SetEnabled(EntityUid uid, GeigerComponent component, bool isEnabled)
{
if (component.IsEnabled == isEnabled)
return;
component.IsEnabled = isEnabled;
if (!isEnabled)
{
component.CurrentRadiation = 0f;
component.DangerLevel = GeigerDangerLevel.None;
}
_radiation.SetCanReceive(uid, isEnabled);
UpdateAppearance(uid, component);
Dirty(component);
}
private void UpdateAppearance(EntityUid uid, GeigerComponent? component = null,
AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref component, ref appearance, false))
return;
_appearance.SetData(uid, GeigerVisuals.IsEnabled, component.IsEnabled, appearance);
_appearance.SetData(uid, GeigerVisuals.DangerLevel, component.DangerLevel, appearance);
}
public static GeigerDangerLevel RadsToLevel(float rads)
{
return rads switch
{
< 0.2f => GeigerDangerLevel.None,
< 1f => GeigerDangerLevel.Low,
< 3f => GeigerDangerLevel.Med,
< 6f => GeigerDangerLevel.High,
_ => GeigerDangerLevel.Extreme
};
}
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Radiation.Components; using Content.Server.Radiation.Components;
using Content.Server.Radiation.Events;
using Content.Shared.Radiation.Components; using Content.Shared.Radiation.Components;
using Content.Shared.Radiation.Systems; using Content.Shared.Radiation.Systems;
using Robust.Shared.Collections; using Robust.Shared.Collections;
@@ -82,6 +83,9 @@ public partial class RadiationSystem
if (rads > 0) if (rads > 0)
IrradiateEntity(receiver.Owner, rads,GridcastUpdateRate); IrradiateEntity(receiver.Owner, rads,GridcastUpdateRate);
} }
// raise broadcast event that radiation system has updated
RaiseLocalEvent(new RadiationSystemUpdatedEvent());
} }
private RadiationRay? Irradiate(EntityUid sourceUid, TransformComponent sourceTrs, Vector2 sourceWorld, private RadiationRay? Irradiate(EntityUid sourceUid, TransformComponent sourceTrs, Vector2 sourceWorld,

View File

@@ -1,4 +1,5 @@
using Content.Shared.Radiation.Events; using Content.Server.Radiation.Components;
using Content.Shared.Radiation.Events;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Map; using Robust.Shared.Map;
@@ -43,4 +44,19 @@ public sealed partial class RadiationSystem : EntitySystem
var msg = new OnIrradiatedEvent(time, radsPerSecond); var msg = new OnIrradiatedEvent(time, radsPerSecond);
RaiseLocalEvent(uid, msg); RaiseLocalEvent(uid, msg);
} }
/// <summary>
/// Marks entity to receive/ignore radiation rays.
/// </summary>
public void SetCanReceive(EntityUid uid, bool canReceive)
{
if (canReceive)
{
EnsureComp<RadiationReceiverComponent>(uid);
}
else
{
RemComp<RadiationReceiverComponent>(uid);
}
}
} }

View File

@@ -0,0 +1,118 @@
using Content.Shared.Radiation.Systems;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Radiation.Components;
/// <summary>
/// Geiger counter that shows current radiation level.
/// Can be added as a component to clothes.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedGeigerSystem))]
public sealed class GeigerComponent : Component
{
/// <summary>
/// If true it will be active only when player equipped it.
/// </summary>
[DataField("attachedToSuit")]
public bool AttachedToSuit;
/// <summary>
/// Is geiger counter currently active?
/// If false attached entity will ignore any radiation rays.
/// </summary>
[DataField("isEnabled")]
public bool IsEnabled;
/// <summary>
/// Should it shows examine message with current radiation level?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("showExamine")]
public bool ShowExamine;
/// <summary>
/// Should it shows item control when equipped by player?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("showControl")]
public bool ShowControl;
/// <summary>
/// Map of sounds that should be play on loop for different radiation levels.
/// </summary>
[DataField("sounds")]
public Dictionary<GeigerDangerLevel, SoundSpecifier> Sounds = new()
{
{GeigerDangerLevel.Low, new SoundPathSpecifier("/Audio/Items/Geiger/low.ogg")},
{GeigerDangerLevel.Med, new SoundPathSpecifier("/Audio/Items/Geiger/med.ogg")},
{GeigerDangerLevel.High, new SoundPathSpecifier("/Audio/Items/Geiger/high.ogg")},
{GeigerDangerLevel.Extreme, new SoundPathSpecifier("/Audio/Items/Geiger/ext.ogg")}
};
/// <summary>
/// Current radiation level in rad per second.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public float CurrentRadiation;
/// <summary>
/// Estimated radiation danger level.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public GeigerDangerLevel DangerLevel = GeigerDangerLevel.None;
/// <summary>
/// Current player that equipped geiger counter.
/// Because sound is annoying, geiger counter clicks will play
/// only for player that equipped it.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public EntityUid? User;
/// <summary>
/// Marked true if control needs to update UI with latest component state.
/// </summary>
[Access(typeof(SharedGeigerSystem), Other = AccessPermissions.ReadWrite)]
public bool UiUpdateNeeded;
/// <summary>
/// Current stream of geiger counter audio.
/// Played only for current user.
/// </summary>
public IPlayingAudioStream? Stream;
}
[Serializable, NetSerializable]
public sealed class GeigerComponentState : ComponentState
{
public float CurrentRadiation;
public GeigerDangerLevel DangerLevel;
public bool IsEnabled;
public EntityUid? User;
}
[Serializable, NetSerializable]
public enum GeigerDangerLevel : byte
{
None,
Low,
Med,
High,
Extreme
}
[Serializable, NetSerializable]
public enum GeigerLayers : byte
{
Screen
}
[Serializable, NetSerializable]
public enum GeigerVisuals : byte
{
DangerLevel,
IsEnabled
}

View File

@@ -0,0 +1,44 @@
using Content.Shared.Examine;
using Content.Shared.Radiation.Components;
namespace Content.Shared.Radiation.Systems;
public abstract class SharedGeigerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GeigerComponent, ExaminedEvent>(OnExamine);
}
private void OnExamine(EntityUid uid, GeigerComponent component, ExaminedEvent args)
{
if (!component.ShowExamine || !component.IsEnabled || !args.IsInDetailsRange)
return;
var currentRads = component.CurrentRadiation;
var rads = currentRads.ToString("N1");
var color = LevelToColor(component.DangerLevel);
var msg = Loc.GetString("geiger-component-examine",
("rads", rads), ("color", color));
args.PushMarkup(msg);
}
public static Color LevelToColor(GeigerDangerLevel level)
{
switch (level)
{
case GeigerDangerLevel.None:
return Color.Green;
case GeigerDangerLevel.Low:
return Color.Yellow;
case GeigerDangerLevel.Med:
return Color.DarkOrange;
case GeigerDangerLevel.High:
case GeigerDangerLevel.Extreme:
return Color.Red;
default:
return Color.White;
}
}
}

View File

@@ -0,0 +1,8 @@
- files:
- "low.ogg"
- "med.ogg"
- "high.ogg"
- "ext.ogg"
license: "CC-BY-SA-3.0"
copyright: "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/456cd10d94084c7c2574f628cf7ac9b67087ba26"
source: "https://github.com/tgstation/tgstation/tree/456cd10d94084c7c2574f628cf7ac9b67087ba26/sound/items/geiger"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
geiger-item-control-status = Radiation: [color={$color}]{$rads} rads[/color]
geiger-item-control-disabled = Disabled
geiger-component-examine = Current radiation: [color={$color}]{$rads} rads[/color]

View File

@@ -34,7 +34,7 @@
sprite: Structures/Wallmounts/signs.rsi sprite: Structures/Wallmounts/signs.rsi
state: radiation state: radiation
product: CrateEmergencyRadiation product: CrateEmergencyRadiation
cost: 900 cost: 1000
category: Emergency category: Emergency
group: market group: market

View File

@@ -63,8 +63,8 @@
amount: 2 amount: 2
- id: ClothingHeadHatHoodRad - id: ClothingHeadHatHoodRad
amount: 2 amount: 2
# - id: GeigerCounter - id: GeigerCounter
# amount: 2 amount: 2
- id: DrinkVodkaBottleFull - id: DrinkVodkaBottleFull
amount: 1 amount: 1
- id: DrinkShotGlass - id: DrinkShotGlass

View File

@@ -115,3 +115,5 @@
amount: 2 amount: 2
- id: ClothingOuterSuitRad - id: ClothingOuterSuitRad
amount: 2 amount: 2
- id: GeigerCounter
amount: 2

View File

@@ -19,7 +19,7 @@
- Screwdriver - Screwdriver
- Flashlight - Flashlight
- Wrench - Wrench
# - GeigerCounter - GeigerCounter
- Flare - Flare
- CableCoil - CableCoil
- CigPack - CigPack
@@ -81,7 +81,7 @@
- Screwdriver - Screwdriver
- Flashlight - Flashlight
- Wrench - Wrench
# - GeigerCounter - GeigerCounter
- Flare - Flare
- CableCoil - CableCoil
- Powerdrill - Powerdrill

View File

@@ -45,7 +45,7 @@
- type: entity - type: entity
abstract: true abstract: true
parent: ClothingOuterBase parent: [ClothingOuterBase, GeigerCounterClothing]
id: ClothingOuterHardsuitBase id: ClothingOuterHardsuitBase
name: base hardsuit name: base hardsuit
components: components:

View File

@@ -61,7 +61,7 @@
coefficient: 0.01 coefficient: 0.01
- type: entity - type: entity
parent: ClothingOuterBaseLarge parent: [ClothingOuterBaseLarge, GeigerCounterClothing]
id: ClothingOuterSuitRad id: ClothingOuterSuitRad
name: radiation suit name: radiation suit
description: "A suit that protects against radiation. The label reads, 'Made with lead. Please do not consume insulation.'" description: "A suit that protects against radiation. The label reads, 'Made with lead. Please do not consume insulation.'"

View File

@@ -10,3 +10,10 @@
- WhitelistChameleon - WhitelistChameleon
- type: StaticPrice - type: StaticPrice
price: 15 price: 15
- type: entity
abstract: true
id: GeigerCounterClothing
components:
- type: Geiger
attachedToSuit: true

View File

@@ -0,0 +1,35 @@
- type: entity
parent: BaseItem
id: GeigerCounter
name: Geiger counter
description: A handheld device used for detecting and measuring radiation pulses.
components:
- type: Sprite
netsync: false
sprite: Objects/Tools/geiger.rsi
layers:
- state: geiger_base
- state: geiger_on_idle
map: ["enum.GeigerLayers.Screen"]
shader: unshaded
visible: false
- type: Item
sprite: Objects/Tools/geiger.rsi
- type: Geiger
showControl: true
showExamine: true
- type: Appearance
- type: GenericVisualizer
visuals:
enum.GeigerVisuals.IsEnabled:
GeigerLayers.Screen:
True: { visible: True }
False: { visible: False }
enum.GeigerVisuals.DangerLevel:
GeigerLayers.Screen:
None: {state: geiger_on_idle}
Low: {state: geiger_on_low}
Med: {state: geiger_on_med}
High: {state: geiger_on_high}
Extreme: {state: geiger_on_ext}

View File

@@ -249,6 +249,9 @@
- type: Tag - type: Tag
id: Gauze id: Gauze
- type: Tag
id: GeigerCounter
- type: Tag - type: Tag
id: GlassBeaker id: GlassBeaker

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 301 B

View File

@@ -0,0 +1,43 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/bfc9c6ba8126ee8c41564d68c4bfb9ce37faa8f8",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "geiger_base"
},
{
"name": "geiger_on_idle"
},
{
"name": "geiger_on_low"
},
{
"name": "geiger_on_med"
},
{
"name": "geiger_on_high"
},
{
"name": "geiger_on_ext",
"delays": [
[
0.2,
0.1
]
]
},
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
}
]
}