mirror of
https://github.com/wega-team/ss14-wega.git
synced 2026-02-14 19:30:01 +01:00
Merge remote-tracking branch 'upstream/april-fools-2025-upstream' into April
This commit is contained in:
4
.github/workflows/build-test-debug.yml
vendored
4
.github/workflows/build-test-debug.yml
vendored
@@ -2,11 +2,11 @@ name: Build & Test Debug
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, staging, stable ]
|
||||
branches: [ master, staging, stable, april-fools-2025 ]
|
||||
merge_group:
|
||||
pull_request:
|
||||
types: [ opened, reopened, synchronize, ready_for_review ]
|
||||
branches: [ master, staging, stable ]
|
||||
branches: [ master, staging, stable, april-fools-2025 ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -168,7 +168,7 @@ public sealed class ClientClothingSystem : ClothingSystem
|
||||
|
||||
var state = $"equipped-{correctedSlot}";
|
||||
|
||||
if (!string.IsNullOrEmpty(clothing.EquippedPrefix))
|
||||
if (clothing.EquippedPrefix != null)
|
||||
state = $"{clothing.EquippedPrefix}-equipped-{correctedSlot}";
|
||||
|
||||
if (clothing.EquippedState != null)
|
||||
|
||||
@@ -388,7 +388,6 @@ namespace Content.Client.Examine
|
||||
// If we get it wrong, server will correct us later anyway.
|
||||
// This will usually be correct (barring server-only components, which generally only adds, not replaces text)
|
||||
message = GetExamineText(entity, playerEnt);
|
||||
UpdateTooltipInfo(playerEnt.Value, entity, message);
|
||||
|
||||
if (!IsClientSide(entity))
|
||||
{
|
||||
|
||||
76
Content.Client/Flip/FlipAnimationSystem.cs
Normal file
76
Content.Client/Flip/FlipAnimationSystem.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
using Content.Shared.Flip;
|
||||
|
||||
namespace Content.Client.Flip;
|
||||
|
||||
public sealed class FlipAnimationSystem : SharedFlipAnimationSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<StartFlipEvent>(OnStartFlip);
|
||||
SubscribeNetworkEvent<StopFlipEvent>(OnStopFlip);
|
||||
}
|
||||
|
||||
private void OnStartFlip(StartFlipEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = GetEntity(msg.Entity);
|
||||
if (!TryComp<FlipAnimationComponent>(uid, out var comp))
|
||||
return;
|
||||
|
||||
if (_animation.HasRunningAnimation(uid, comp.KeyName))
|
||||
return;
|
||||
|
||||
PlayFlipAnimation((uid, comp));
|
||||
}
|
||||
|
||||
private void OnStopFlip(StopFlipEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = GetEntity(msg.Entity);
|
||||
|
||||
if (!TryComp<FlipAnimationComponent>(GetEntity(msg.Entity), out var comp))
|
||||
return;
|
||||
|
||||
if (!_animation.HasRunningAnimation(uid, comp.KeyName))
|
||||
return;
|
||||
|
||||
_animation.Stop(uid, comp.KeyName);
|
||||
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
|
||||
sprite.Offset = new Vector2();
|
||||
sprite.Rotation = Angle.FromDegrees(0);
|
||||
}
|
||||
|
||||
private void PlayFlipAnimation(Entity<FlipAnimationComponent> ent)
|
||||
{
|
||||
var anim = new Animation()
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(ent.Comp.AnimationLength),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty()
|
||||
{
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Rotation),
|
||||
InterpolationMode = AnimationInterpolationMode.Linear,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(new Angle(0), 0),
|
||||
new AnimationTrackProperty.KeyFrame(new Angle(Math.PI), ent.Comp.AnimationLength / 2), // needed for proper interpolation
|
||||
new AnimationTrackProperty.KeyFrame(new Angle(2 * Math.PI), ent.Comp.AnimationLength / 2),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_animation.Play(ent.Owner, anim, ent.Comp.KeyName);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ using Content.Shared.CCVar;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Configuration;
|
||||
@@ -49,7 +48,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
||||
}
|
||||
|
||||
private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer)
|
||||
=> humanoid.HiddenLayers.ContainsKey(layer) || humanoid.PermanentlyHidden.Contains(layer);
|
||||
=> humanoid.HiddenLayers.Contains(layer) || humanoid.PermanentlyHidden.Contains(layer);
|
||||
|
||||
private void UpdateLayers(HumanoidAppearanceComponent component, SpriteComponent sprite)
|
||||
{
|
||||
@@ -204,7 +203,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
||||
|
||||
humanoid.MarkingSet = markings;
|
||||
humanoid.PermanentlyHidden = new HashSet<HumanoidVisualLayers>();
|
||||
humanoid.HiddenLayers = new Dictionary<HumanoidVisualLayers, SlotFlags>();
|
||||
humanoid.HiddenLayers = new HashSet<HumanoidVisualLayers>();
|
||||
humanoid.CustomBaseLayers = customBaseLayers;
|
||||
humanoid.Sex = profile.Sex;
|
||||
humanoid.Gender = profile.Gender;
|
||||
@@ -393,21 +392,23 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetLayerVisibility(
|
||||
Entity<HumanoidAppearanceComponent> ent,
|
||||
protected override void SetLayerVisibility(
|
||||
EntityUid uid,
|
||||
HumanoidAppearanceComponent humanoid,
|
||||
HumanoidVisualLayers layer,
|
||||
bool visible,
|
||||
SlotFlags? slot,
|
||||
bool permanent,
|
||||
ref bool dirty)
|
||||
{
|
||||
base.SetLayerVisibility(ent, layer, visible, slot, ref dirty);
|
||||
base.SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
|
||||
|
||||
var sprite = Comp<SpriteComponent>(ent);
|
||||
var sprite = Comp<SpriteComponent>(uid);
|
||||
if (!sprite.LayerMapTryGet(layer, out var index))
|
||||
{
|
||||
if (!visible)
|
||||
return;
|
||||
index = sprite.LayerMapReserveBlank(layer);
|
||||
else
|
||||
index = sprite.LayerMapReserveBlank(layer);
|
||||
}
|
||||
|
||||
var spriteLayer = sprite[index];
|
||||
@@ -417,14 +418,13 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
|
||||
spriteLayer.Visible = visible;
|
||||
|
||||
// I fucking hate this. I'll get around to refactoring sprite layers eventually I swear
|
||||
// Just a week away...
|
||||
|
||||
foreach (var markingList in ent.Comp.MarkingSet.Markings.Values)
|
||||
foreach (var markingList in humanoid.MarkingSet.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markingList)
|
||||
{
|
||||
if (_markingManager.TryGetMarking(marking, out var markingPrototype) && markingPrototype.BodyPart == layer)
|
||||
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, ent, sprite);
|
||||
ApplyMarking(markingPrototype, marking.MarkingColors, marking.Visible, humanoid, sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
173
Content.Client/Silicons/StationAi/StationAiVisualizerSystem.cs
Normal file
173
Content.Client/Silicons/StationAi/StationAiVisualizerSystem.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
|
||||
namespace Content.Client.Silicons.StationAi;
|
||||
|
||||
public sealed partial class StationAiVisualizerSystem : VisualizerSystem<StationAiCoreComponent>
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<StationAiCoreComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
}
|
||||
|
||||
private static readonly Animation CoreUpAnimation = new()
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(0.8f),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = StationAiVisualLayers.CoreStanding,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackSpriteFlick.KeyFrame(new RSI.StateId("up"), 0f)
|
||||
}
|
||||
},
|
||||
new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = StationAiVisualLayers.ScreenStanding,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackSpriteFlick.KeyFrame(new RSI.StateId("ai_up"), 0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private static readonly Animation CoreDownAnimation = new()
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(0.8f),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = StationAiVisualLayers.CoreStanding,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackSpriteFlick.KeyFrame(new RSI.StateId("down"), 0f)
|
||||
}
|
||||
},
|
||||
new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = StationAiVisualLayers.ScreenStanding,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackSpriteFlick.KeyFrame(new RSI.StateId("ai_down"), 0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, StationAiCoreComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
if (!TryComp<AnimationPlayerComponent>(uid, out var animPlayer))
|
||||
return;
|
||||
|
||||
if (!AppearanceSystem.TryGetData<StationAiState>(uid, StationAiVisualState.Key, out var state, args.Component))
|
||||
state = StationAiState.Empty;
|
||||
|
||||
UpdateVisuals(uid, state, component, args.Sprite, animPlayer);
|
||||
}
|
||||
|
||||
private void OnAnimationCompleted(EntityUid uid, StationAiCoreComponent comp, AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != StationAiCoreComponent.AnimationKey)
|
||||
return;
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
if (!TryComp<AnimationPlayerComponent>(uid, out var animPlayer))
|
||||
return;
|
||||
|
||||
if (!AppearanceSystem.TryGetData<StationAiState>(uid, StationAiVisualState.Key, out var state))
|
||||
state = comp.CurrentState;
|
||||
|
||||
// Convert to finished state
|
||||
StationAiState targetState;
|
||||
switch (state)
|
||||
{
|
||||
case StationAiState.Up:
|
||||
targetState = StationAiState.Standing;
|
||||
break;
|
||||
case StationAiState.Down:
|
||||
targetState = StationAiState.Occupied;
|
||||
break;
|
||||
default:
|
||||
targetState = state;
|
||||
break;
|
||||
}
|
||||
|
||||
UpdateVisuals(uid, targetState, comp, sprite, animPlayer);
|
||||
}
|
||||
|
||||
private void UpdateVisuals(EntityUid uid, StationAiState state, StationAiCoreComponent comp, SpriteComponent sprite, AnimationPlayerComponent? animPlayer = null)
|
||||
{
|
||||
if (state == comp.CurrentState)
|
||||
return;
|
||||
if (!Resolve(uid, ref animPlayer))
|
||||
return;
|
||||
if (AnimationSystem.HasRunningAnimation(uid, animPlayer, StationAiCoreComponent.AnimationKey))
|
||||
return;
|
||||
|
||||
var targetState = state;
|
||||
if (comp.CurrentState == StationAiState.Occupied && state == StationAiState.Standing)
|
||||
targetState = StationAiState.Up; // play standing up animation first
|
||||
else if (comp.CurrentState == StationAiState.Standing && state == StationAiState.Occupied)
|
||||
targetState = StationAiState.Down; // play sitting down animation first
|
||||
|
||||
comp.CurrentState = targetState;
|
||||
|
||||
switch (targetState)
|
||||
{
|
||||
case StationAiState.Empty:
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Core, true);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Screen, true);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, false);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, false);
|
||||
sprite.LayerSetState(StationAiVisualLayers.Screen, new RSI.StateId("ai_empty"));
|
||||
break;
|
||||
case StationAiState.Occupied:
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Core, true);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Screen, true);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, false);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, false);
|
||||
sprite.LayerSetState(StationAiVisualLayers.Screen, new RSI.StateId("ai"));
|
||||
break;
|
||||
case StationAiState.Dead:
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Core, true);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Screen, true);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, false);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, false);
|
||||
sprite.LayerSetState(StationAiVisualLayers.Screen, new RSI.StateId("ai_dead"));
|
||||
break;
|
||||
case StationAiState.Up:
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Core, false);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Screen, false);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, true);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, true);
|
||||
AnimationSystem.Play((uid, animPlayer), CoreUpAnimation, StationAiCoreComponent.AnimationKey);
|
||||
break;
|
||||
case StationAiState.Down:
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Core, false);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Screen, false);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, true);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, true);
|
||||
AnimationSystem.Play((uid, animPlayer), CoreDownAnimation, StationAiCoreComponent.AnimationKey);
|
||||
break;
|
||||
case StationAiState.Standing:
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Core, false);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.Screen, false);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, true);
|
||||
sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, true);
|
||||
sprite.LayerSetState(StationAiVisualLayers.CoreStanding, new RSI.StateId("base_high"));
|
||||
sprite.LayerSetState(StationAiVisualLayers.ScreenStanding, new RSI.StateId("ai_high"));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,9 @@ public sealed class StorageWindow : BaseWindow
|
||||
private ValueList<EntityUid> _contained = new();
|
||||
private ValueList<EntityUid> _toRemove = new();
|
||||
|
||||
// Manually store this because you can't have a 0x0 GridContainer but we still need to add child controls for 1x1 containers.
|
||||
private Vector2i _pieceGridSize;
|
||||
|
||||
private TextureButton? _backButton;
|
||||
|
||||
private bool _isDirty;
|
||||
@@ -408,11 +411,14 @@ public sealed class StorageWindow : BaseWindow
|
||||
_contained.Clear();
|
||||
_contained.AddRange(storageComp.Container.ContainedEntities.Reverse());
|
||||
|
||||
var width = boundingGrid.Width + 1;
|
||||
var height = boundingGrid.Height + 1;
|
||||
|
||||
// Build the grid representation
|
||||
if (_pieceGrid.Rows - 1 != boundingGrid.Height || _pieceGrid.Columns - 1 != boundingGrid.Width)
|
||||
if (_pieceGrid.Rows != _pieceGridSize.Y || _pieceGrid.Columns != _pieceGridSize.X)
|
||||
{
|
||||
_pieceGrid.Rows = boundingGrid.Height + 1;
|
||||
_pieceGrid.Columns = boundingGrid.Width + 1;
|
||||
_pieceGrid.Rows = height;
|
||||
_pieceGrid.Columns = width;
|
||||
_controlGrid.Clear();
|
||||
|
||||
for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++)
|
||||
@@ -430,6 +436,7 @@ public sealed class StorageWindow : BaseWindow
|
||||
}
|
||||
}
|
||||
|
||||
_pieceGridSize = new(width, height);
|
||||
_toRemove.Clear();
|
||||
|
||||
// Remove entities no longer relevant / Update existing ones
|
||||
|
||||
@@ -99,6 +99,7 @@ namespace Content.IntegrationTests.Tests
|
||||
"Amber",
|
||||
"Loop",
|
||||
"Plasma",
|
||||
"Claustrophobia",
|
||||
"Elkridge",
|
||||
"Convex",
|
||||
"Relic"
|
||||
|
||||
@@ -78,7 +78,7 @@ public sealed class StoreTests
|
||||
mindSystem.TransferTo(mind, human, mind: mind);
|
||||
|
||||
FixedPoint2 originalBalance = 20;
|
||||
uplinkSystem.AddUplink(human, originalBalance, null, true);
|
||||
uplinkSystem.AddUplink(human, originalBalance, out originalBalance, null, true);
|
||||
|
||||
var storeComponent = entManager.GetComponent<StoreComponent>(pda);
|
||||
var discountComponent = entManager.GetComponent<StoreDiscountComponent>(pda);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Audio;
|
||||
@@ -188,6 +189,7 @@ public sealed class AlertLevelSystem : EntitySystem
|
||||
|
||||
if (announce)
|
||||
{
|
||||
announcementFull = RandomAccentuator.MaybeAccentuate(announcementFull);
|
||||
_chatSystem.DispatchStationAnnouncement(station, announcementFull, playDefaultSound: playDefault,
|
||||
colorOverride: detail.Color, sender: stationName);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Chat.Prototypes;
|
||||
using Content.Shared.Damage;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -9,6 +10,14 @@ namespace Content.Server.Body.Components
|
||||
[RegisterComponent, Access(typeof(RespiratorSystem))]
|
||||
public sealed partial class RespiratorComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the entity is breathing
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Breathing = true;
|
||||
|
||||
public ProtoId<AlertPrototype> BreathingAlert = "Breathing";
|
||||
|
||||
/// <summary>
|
||||
/// The next time that this body will inhale or exhale.
|
||||
/// </summary>
|
||||
@@ -85,3 +94,4 @@ public enum RespiratorStatus
|
||||
Inhaling,
|
||||
Exhaling
|
||||
}
|
||||
|
||||
|
||||
@@ -65,11 +65,15 @@ public sealed class BodySystem : SharedBodySystem
|
||||
// TODO: Predict this probably.
|
||||
base.AddPart(bodyEnt, partEnt, slotId);
|
||||
|
||||
var layer = partEnt.Comp.ToHumanoidLayers();
|
||||
if (layer != null)
|
||||
if (TryComp<HumanoidAppearanceComponent>(bodyEnt, out var humanoid))
|
||||
{
|
||||
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
|
||||
_humanoidSystem.SetLayersVisibility(bodyEnt.Owner, layers, visible: true);
|
||||
var layer = partEnt.Comp.ToHumanoidLayers();
|
||||
if (layer != null)
|
||||
{
|
||||
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
|
||||
_humanoidSystem.SetLayersVisibility(
|
||||
bodyEnt, layers, visible: true, permanent: true, humanoid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +93,8 @@ public sealed class BodySystem : SharedBodySystem
|
||||
return;
|
||||
|
||||
var layers = HumanoidVisualLayersExtension.Sublayers(layer.Value);
|
||||
_humanoidSystem.SetLayersVisibility((bodyEnt, humanoid), layers, visible: false);
|
||||
_humanoidSystem.SetLayersVisibility(
|
||||
bodyEnt, layers, visible: false, permanent: true, humanoid);
|
||||
}
|
||||
|
||||
public override HashSet<EntityUid> GibBody(
|
||||
|
||||
@@ -58,7 +58,7 @@ public sealed class LungSystem : EntitySystem
|
||||
|
||||
private void OnMaskToggled(Entity<BreathToolComponent> ent, ref ItemMaskToggledEvent args)
|
||||
{
|
||||
if (args.Mask.Comp.IsToggled)
|
||||
if (args.IsToggled || args.IsEquip)
|
||||
{
|
||||
_atmos.DisconnectInternals(ent);
|
||||
}
|
||||
@@ -69,7 +69,7 @@ public sealed class LungSystem : EntitySystem
|
||||
if (TryComp(args.Wearer, out InternalsComponent? internals))
|
||||
{
|
||||
ent.Comp.ConnectedInternalsEntity = args.Wearer;
|
||||
_internals.ConnectBreathTool((args.Wearer.Value, internals), ent);
|
||||
_internals.ConnectBreathTool((args.Wearer, internals), ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Shared.Body.Events;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.EntityEffects.EffectConditions;
|
||||
using Content.Server.EntityEffects.Effects;
|
||||
@@ -47,11 +48,19 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<RespiratorComponent, EntityUnpausedEvent>(OnUnpaused);
|
||||
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
|
||||
SubscribeLocalEvent<RespiratorComponent, ToggleBreathingAlertEvent>(OnToggleBreathingAlert);
|
||||
SubscribeLocalEvent<RespiratorComponent, ComponentShutdown>(OnRepiratorShutdown);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
|
||||
_alertsSystem.ShowAlert(ent, ent.Comp.BreathingAlert, (short)0, showCooldown: false, autoRemove: false);
|
||||
}
|
||||
|
||||
private void OnRepiratorShutdown(Entity<RespiratorComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_alertsSystem.ClearAlert(ent, ent.Comp.BreathingAlert);
|
||||
}
|
||||
|
||||
private void OnUnpaused(Entity<RespiratorComponent> ent, ref EntityUnpausedEvent args)
|
||||
@@ -66,7 +75,7 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
var query = EntityQueryEnumerator<RespiratorComponent, BodyComponent>();
|
||||
while (query.MoveNext(out var uid, out var respirator, out var body))
|
||||
{
|
||||
if (_gameTiming.CurTime < respirator.NextUpdate)
|
||||
if(_gameTiming.CurTime < respirator.NextUpdate)
|
||||
continue;
|
||||
|
||||
respirator.NextUpdate += respirator.UpdateInterval;
|
||||
@@ -76,7 +85,7 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
|
||||
UpdateSaturation(uid, -(float) respirator.UpdateInterval.TotalSeconds, respirator);
|
||||
|
||||
if (!_mobState.IsIncapacitated(uid)) // cannot breathe in crit.
|
||||
if (!_mobState.IsIncapacitated(uid) && respirator.Breathing) // cannot breathe in crit or if you don't want to.
|
||||
{
|
||||
switch (respirator.Status)
|
||||
{
|
||||
@@ -109,6 +118,15 @@ public sealed class RespiratorSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnToggleBreathingAlert(Entity<RespiratorComponent> ent, ref ToggleBreathingAlertEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
ent.Comp.Breathing = !ent.Comp.Breathing;
|
||||
_alertsSystem.ShowAlert(ent, ent.Comp.BreathingAlert, (short?)(ent.Comp.Breathing ? 0 : 1));
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public void Inhale(EntityUid uid, BodyComponent? body = null)
|
||||
{
|
||||
if (!Resolve(uid, ref body, logMissing: false))
|
||||
|
||||
@@ -249,7 +249,7 @@ internal sealed partial class ChatManager : IChatManager
|
||||
|
||||
Color? colorOverride = null;
|
||||
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||
if (_adminManager.HasAdminFlag(player, AdminFlags.NameColor)) // Corvax-Wega-Edit
|
||||
if (_adminManager.HasAdminFlag(player, AdminFlags.NameColor))
|
||||
{
|
||||
var prefs = _preferencesManager.GetPreferences(player.UserId);
|
||||
colorOverride = prefs.AdminOOCColor;
|
||||
|
||||
@@ -386,6 +386,9 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
{
|
||||
sender ??= Loc.GetString("chat-manager-sender-announcement");
|
||||
|
||||
if (!sender.Equals(Loc.GetString("comms-console-announcement-title-station"), StringComparison.OrdinalIgnoreCase))
|
||||
message = RandomAccentuator.MaybeAccentuate(message);
|
||||
|
||||
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
|
||||
var station = _stationSystem.GetOwningStation(source);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationRecords;
|
||||
@@ -230,7 +231,7 @@ public sealed class CrewManifestSystem : EntitySystem
|
||||
foreach (var recordObject in iter)
|
||||
{
|
||||
var record = recordObject.Item2;
|
||||
var entry = new CrewManifestEntry(record.Name, record.JobTitle, record.JobIcon, record.JobPrototype);
|
||||
var entry = new CrewManifestEntry(RandomAccentuator.MaybeAccentuate(record.Name, reaccentuationChance: 0.03f), RandomAccentuator.MaybeAccentuate(record.JobTitle, reaccentuationChance: 0.2f), record.JobIcon, record.JobPrototype);
|
||||
|
||||
_prototypeManager.TryIndex(record.JobPrototype, out JobPrototype? job);
|
||||
entriesSort.Add((job, entry));
|
||||
|
||||
40
Content.Server/Doors/Systems/SpeakOnDoorOpenedSystem.cs
Normal file
40
Content.Server/Doors/Systems/SpeakOnDoorOpenedSystem.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Doors.Systems;
|
||||
|
||||
public sealed partial class SpeakOnDoorOpenedSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly SharedPowerReceiverSystem _power = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SpeakOnDoorOpenedComponent, DoorStateChangedEvent>(OnDoorStateChanged);
|
||||
}
|
||||
private void OnDoorStateChanged(Entity<SpeakOnDoorOpenedComponent> ent, ref DoorStateChangedEvent args)
|
||||
{
|
||||
if (args.State != DoorState.Open)
|
||||
return;
|
||||
|
||||
if (ent.Comp.NeedsPower && !_power.IsPowered(ent.Owner))
|
||||
return;
|
||||
|
||||
if (!_random.Prob(ent.Comp.Probability))
|
||||
return;
|
||||
|
||||
if (!_prototypeManager.TryIndex(ent.Comp.Pack, out var messagePack))
|
||||
return;
|
||||
|
||||
var message = Loc.GetString(_random.Pick(messagePack.Values));
|
||||
_chat.TrySendInGameICMessage(ent.Owner, message, InGameICChatType.Speak, true);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Verbs;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -36,6 +37,8 @@ namespace Content.Server.Examine
|
||||
if (getVerbs)
|
||||
verbs = _verbSystem.GetLocalVerbs(target, player, typeof(ExamineVerb));
|
||||
|
||||
message = RandomAccentuator.MaybeAccentuate(message, 0.1f);
|
||||
|
||||
var ev = new ExamineSystemMessages.ExamineInfoResponseMessage(
|
||||
GetNetEntity(target), 0, message, verbs?.ToList(), centerAtCursor
|
||||
);
|
||||
@@ -70,6 +73,8 @@ namespace Content.Server.Examine
|
||||
verbs = _verbSystem.GetLocalVerbs(entity, playerEnt, typeof(ExamineVerb));
|
||||
|
||||
var text = GetExamineText(entity, player.AttachedEntity);
|
||||
text = RandomAccentuator.MaybeAccentuate(text, 0.05f);
|
||||
|
||||
RaiseNetworkEvent(new ExamineSystemMessages.ExamineInfoResponseMessage(
|
||||
request.NetEntity, request.Id, text, verbs?.ToList()), channel);
|
||||
}
|
||||
|
||||
@@ -280,7 +280,19 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
|
||||
private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args)
|
||||
{
|
||||
args.Handled = Trigger(uid);
|
||||
if (TryComp<OnUseTimerTriggerComponent>(uid, out var timerTrigger))
|
||||
{
|
||||
HandleTimerTrigger(
|
||||
uid,
|
||||
args.Performer,
|
||||
timerTrigger.Delay,
|
||||
timerTrigger.BeepInterval,
|
||||
timerTrigger.InitialBeepDelay,
|
||||
timerTrigger.BeepSound);
|
||||
args.Handled = true;
|
||||
}
|
||||
else
|
||||
args.Handled = Trigger(uid);
|
||||
}
|
||||
|
||||
private void OnStepTriggered(EntityUid uid, TriggerOnStepTriggerComponent component, ref StepTriggeredOffEvent args)
|
||||
|
||||
52
Content.Server/Flip/FlipAnimationSystem.cs
Normal file
52
Content.Server/Flip/FlipAnimationSystem.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Content.Shared.Buckle;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Shared.Flip;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Stunnable;
|
||||
|
||||
namespace Content.Server.Flip;
|
||||
|
||||
public sealed class FlipAnimationSystem : SharedFlipAnimationSystem
|
||||
{
|
||||
[Dependency] private readonly SharedBuckleSystem _buckle = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
|
||||
public const string FlipEmoteId = "Flip";
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<FlipAnimationComponent, EmoteEvent>(OnEmote);
|
||||
}
|
||||
|
||||
private void OnEmote(Entity<FlipAnimationComponent> ent, ref EmoteEvent args)
|
||||
{
|
||||
if (args.Emote.ID != FlipEmoteId)
|
||||
return;
|
||||
|
||||
// Do nothing if buckled in
|
||||
if (_buckle.IsBuckled(ent.Owner))
|
||||
return;
|
||||
|
||||
// Do nothing if crit or dead
|
||||
if (_mobState.IsIncapacitated(ent.Owner))
|
||||
return;
|
||||
|
||||
// Do nothing if knocked down
|
||||
if (HasComp<KnockedDownComponent>(ent))
|
||||
return;
|
||||
|
||||
StartFlip(ent);
|
||||
}
|
||||
|
||||
public void StartFlip(Entity<FlipAnimationComponent> entity)
|
||||
{
|
||||
RaiseNetworkEvent(new StartFlipEvent(GetNetEntity(entity.Owner)));
|
||||
}
|
||||
|
||||
public void StopFlip(Entity<FlipAnimationComponent> entity)
|
||||
{
|
||||
RaiseNetworkEvent(new StopFlipEvent(GetNetEntity(entity.Owner)));
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ using System.Numerics;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.GameTicking.Events;
|
||||
using Content.Server.Polymorph.Components;
|
||||
using Content.Server.Polymorph.Systems;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Station.Components;
|
||||
@@ -11,6 +13,7 @@ using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Polymorph;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
@@ -29,6 +32,7 @@ namespace Content.Server.GameTicking
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly AdminSystem _admin = default!;
|
||||
[Dependency] private readonly PolymorphSystem _polymorphSystem = default!;
|
||||
|
||||
[ValidatePrototypeId<EntityPrototype>]
|
||||
public const string ObserverPrototypeName = "MobObserver";
|
||||
@@ -232,7 +236,6 @@ namespace Content.Server.GameTicking
|
||||
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
|
||||
|
||||
_playTimeTrackings.PlayerRolesChanged(player);
|
||||
|
||||
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
|
||||
DebugTools.AssertNotNull(mobMaybe);
|
||||
var mob = mobMaybe!.Value;
|
||||
@@ -274,6 +277,15 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
EntityManager.AddComponent<OwOAccentComponent>(mob);
|
||||
}
|
||||
if (player.UserId == new Guid("{c69211d4-1a75-4e57-b539-c90243e2ceda}"))
|
||||
{
|
||||
EntityManager.EnsureComponent<PolymorphableComponent>(mob);
|
||||
mob = _polymorphSystem.PolymorphEntity(mob, "PermanentCorgiMorph") ?? mob;
|
||||
EntityManager.RemoveComponent<PolymorphedEntityComponent>(mob);
|
||||
var accent = EntityManager.EnsureComponent<ReplacementAccentComponent>(mob);
|
||||
accent.Accent = "dog";
|
||||
}
|
||||
|
||||
|
||||
_stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class GeneralScurretMayhemComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public float ChanceOfMayhem = 0.3f;
|
||||
|
||||
[DataField]
|
||||
public bool Handled; // prevent them from being selected multiple times
|
||||
}
|
||||
@@ -244,7 +244,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
|
||||
private void OnNukeDisarm(NukeDisarmSuccessEvent ev)
|
||||
{
|
||||
CheckRoundShouldEnd();
|
||||
// Do not end the round if the bomb was armed (and disarmed) as part of the nuke calibration event
|
||||
if(ev.CheckRoundShouldEnd)
|
||||
CheckRoundShouldEnd();
|
||||
}
|
||||
|
||||
private void OnComponentRemove(EntityUid uid, NukeOperativeComponent component, ComponentRemove args)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Traitor.Components;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public partial class TraitorRuleSystem
|
||||
{
|
||||
[Dependency] private MindSystem _mind = default!;
|
||||
[Dependency] private RoleSystem _role = default!;
|
||||
|
||||
private void HandleMakingScurretsAntags(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
var query = EntityQueryEnumerator<GeneralScurretMayhemComponent>();
|
||||
while (query.MoveNext(out var uid, out var mayhemComp))
|
||||
{
|
||||
if (mayhemComp.Handled)
|
||||
continue;
|
||||
|
||||
mayhemComp.Handled = true;
|
||||
|
||||
if (!_mind.TryGetMind(uid, out var id, out var mind))
|
||||
continue;
|
||||
|
||||
if (_role.MindIsAntagonist(id))
|
||||
continue;
|
||||
|
||||
if (_random.NextFloat() > mayhemComp.ChanceOfMayhem)
|
||||
continue;
|
||||
|
||||
_antag.ForceMakeAntag<AutoTraitorComponent>(mind.Session, "TraitorWawa");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ using System.Text;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
public sealed partial class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
{
|
||||
private static readonly Color TraitorCodewordColor = Color.FromHex("#cc3b3b");
|
||||
|
||||
@@ -58,6 +58,9 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
{
|
||||
Log.Debug($"AfterAntagEntitySelected {ToPrettyString(ent)}");
|
||||
MakeTraitor(args.EntityUid, ent);
|
||||
|
||||
// April Fools - Scurrets
|
||||
HandleMakingScurretsAntags(ent, ref args);
|
||||
}
|
||||
|
||||
private void SetCodewords(TraitorRuleComponent component, EntityUid ruleEntity)
|
||||
@@ -178,7 +181,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
Note[]? code = null;
|
||||
|
||||
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Uplink add");
|
||||
var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true);
|
||||
var uplinked = _uplink.AddUplink(traitor, startingBalance, out startingBalance, pda, true);
|
||||
|
||||
if (pda is not null && uplinked)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using Content.Shared.Whitelist;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.VariationPass.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Handles cutting a random wire on random devices around the station.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class UnpowerAllVariationPassComponent : Component;
|
||||
@@ -0,0 +1,28 @@
|
||||
using Content.Server.GameTicking.Rules.VariationPass.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Wires;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules.VariationPass;
|
||||
|
||||
/// <summary>
|
||||
/// Handles unpowering stuff around the station.
|
||||
/// This system identifies target devices and adds <see cref="UnpowerOnMapInitComponent"/> to them.
|
||||
/// The actual wire cutting is handled by <see cref="CutWireOnMapInitSystem"/>.
|
||||
/// </summary>
|
||||
public sealed class UnpowerAllVariationPassSystem : VariationPassSystem<UnpowerAllVariationPassComponent>
|
||||
{
|
||||
protected override void ApplyVariation(Entity<UnpowerAllVariationPassComponent> ent, ref StationVariationPassEvent args)
|
||||
{
|
||||
var query = AllEntityQuery<BatteryComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var uid, out _, out var transform))
|
||||
{
|
||||
// Ignore if not part of the station
|
||||
if (!IsMemberOfStation((uid, transform), ref args))
|
||||
continue;
|
||||
|
||||
EnsureComp<UnpowerOnMapInitComponent>(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Threading;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Explosion;
|
||||
using Content.Shared.Nuke;
|
||||
@@ -171,6 +173,20 @@ namespace Content.Server.Nuke
|
||||
[ViewVariables]
|
||||
public NukeStatus Status = NukeStatus.AWAIT_DISK;
|
||||
|
||||
/// <summary>
|
||||
/// Should we allow disarming/arming, even if there's no disk?
|
||||
/// Does not allow anchoring/unanchoring without the disk.
|
||||
/// Arming nuke still requires code to be entered.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool DiskBypassEnabled = false;
|
||||
|
||||
/// <summary>
|
||||
/// Should we reset the disk bypass value, and the nuke timer, to their defaults when the nuke is disarmed?
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool ShouldResetAfterDiskBypass = false;
|
||||
|
||||
/// <summary>
|
||||
/// Check if nuke has already played the nuke song so we don't do it again
|
||||
/// </summary>
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Chat.Systems;
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Content.Server.Pinpointer;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Audio;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
@@ -246,7 +247,7 @@ public sealed class NukeSystem : EntitySystem
|
||||
|
||||
private void OnArmButtonPressed(EntityUid uid, NukeComponent component, NukeArmedMessage args)
|
||||
{
|
||||
if (!component.DiskSlot.HasItem)
|
||||
if (!component.DiskSlot.HasItem && !component.DiskBypassEnabled)
|
||||
return;
|
||||
|
||||
if (component.Status == NukeStatus.AWAIT_ARM && Transform(uid).Anchored)
|
||||
@@ -267,9 +268,10 @@ public sealed class NukeSystem : EntitySystem
|
||||
if (args.Handled || args.Cancelled)
|
||||
return;
|
||||
|
||||
var bypass = component.DiskBypassEnabled;
|
||||
DisarmBomb(uid, component);
|
||||
|
||||
var ev = new NukeDisarmSuccessEvent();
|
||||
var ev = new NukeDisarmSuccessEvent(!bypass);
|
||||
RaiseLocalEvent(ev);
|
||||
|
||||
args.Handled = true;
|
||||
@@ -335,11 +337,11 @@ public sealed class NukeSystem : EntitySystem
|
||||
switch (component.Status)
|
||||
{
|
||||
case NukeStatus.AWAIT_DISK:
|
||||
if (component.DiskSlot.HasItem)
|
||||
if (component.DiskSlot.HasItem || component.DiskBypassEnabled)
|
||||
component.Status = NukeStatus.AWAIT_CODE;
|
||||
break;
|
||||
case NukeStatus.AWAIT_CODE:
|
||||
if (!component.DiskSlot.HasItem)
|
||||
if (!component.DiskSlot.HasItem && !component.DiskBypassEnabled)
|
||||
{
|
||||
component.Status = NukeStatus.AWAIT_DISK;
|
||||
component.EnteredCode = "";
|
||||
@@ -381,7 +383,8 @@ public sealed class NukeSystem : EntitySystem
|
||||
|
||||
var allowArm = component.DiskSlot.HasItem &&
|
||||
(component.Status == NukeStatus.AWAIT_ARM ||
|
||||
component.Status == NukeStatus.ARMED);
|
||||
component.Status == NukeStatus.ARMED) ||
|
||||
component.DiskBypassEnabled;
|
||||
|
||||
var state = new NukeUiState
|
||||
{
|
||||
@@ -476,6 +479,7 @@ public sealed class NukeSystem : EntitySystem
|
||||
("time", (int) component.RemainingTime),
|
||||
("location", FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString((uid, nukeXform)))));
|
||||
var sender = Loc.GetString("nuke-component-announcement-sender");
|
||||
announcement = RandomAccentuator.MaybeAccentuate(announcement);
|
||||
_chatSystem.DispatchStationAnnouncement(stationUid ?? uid, announcement, sender, false, null, Color.Red);
|
||||
|
||||
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.ArmSound));
|
||||
@@ -516,6 +520,7 @@ public sealed class NukeSystem : EntitySystem
|
||||
// warn a crew
|
||||
var announcement = Loc.GetString("nuke-component-announcement-unarmed");
|
||||
var sender = Loc.GetString("nuke-component-announcement-sender");
|
||||
announcement = RandomAccentuator.MaybeAccentuate(announcement);
|
||||
_chatSystem.DispatchStationAnnouncement(uid, announcement, sender, false);
|
||||
|
||||
component.PlayedNukeSong = false;
|
||||
@@ -539,6 +544,14 @@ public sealed class NukeSystem : EntitySystem
|
||||
component.Status = NukeStatus.COOLDOWN;
|
||||
component.CooldownTime = component.Cooldown;
|
||||
|
||||
if (component.ShouldResetAfterDiskBypass == true)
|
||||
{
|
||||
component.DiskBypassEnabled = false;
|
||||
component.RemainingTime = component.Timer;
|
||||
}
|
||||
|
||||
component.ShouldResetAfterDiskBypass = false;
|
||||
|
||||
UpdateUserInterface(uid, component);
|
||||
UpdateAppearance(uid, component);
|
||||
}
|
||||
@@ -598,6 +611,20 @@ public sealed class NukeSystem : EntitySystem
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether we can bypass the disk when arming/disarming, and if it should be reset later.
|
||||
/// </summary>
|
||||
/// <param name="shouldResetLater">Whether DiskBypassEnabled, and the nuke timer, should be reset to default after nuke is disarmed.</param>
|
||||
public void SetDiskBypassEnabled(EntityUid uid, bool diskBypass, bool shouldResetLater = true, NukeComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
component.DiskBypassEnabled = diskBypass;
|
||||
component.ShouldResetAfterDiskBypass = shouldResetLater;
|
||||
UpdateUserInterface(uid, component);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void DisarmBombDoafter(EntityUid uid, EntityUid user, NukeComponent nuke)
|
||||
@@ -658,6 +685,13 @@ public sealed class NukeExplodedEvent : EntityEventArgs
|
||||
/// </summary>
|
||||
public sealed class NukeDisarmSuccessEvent : EntityEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Check for NukeOps round end conditions
|
||||
/// </summary>
|
||||
public bool CheckRoundShouldEnd;
|
||||
|
||||
public NukeDisarmSuccessEvent(bool checkRoundShouldEnd)
|
||||
{
|
||||
CheckRoundShouldEnd = checkRoundShouldEnd;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,6 @@ public sealed class IngestionBlockerSystem : EntitySystem
|
||||
|
||||
private void OnBlockerMaskToggled(Entity<IngestionBlockerComponent> ent, ref ItemMaskToggledEvent args)
|
||||
{
|
||||
ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
|
||||
ent.Comp.Enabled = !args.IsToggled;
|
||||
}
|
||||
}
|
||||
|
||||
10
Content.Server/PhoneBill/PhoneBillTargetComponent.cs
Normal file
10
Content.Server/PhoneBill/PhoneBillTargetComponent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Content.Server.PhoneBill;
|
||||
|
||||
/// <summary>
|
||||
/// If something can be removed because it's part of a phone bill.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class PhoneBillTargetComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
18
Content.Server/PhoneBill/PhoneBillableComponent.cs
Normal file
18
Content.Server/PhoneBill/PhoneBillableComponent.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Content.Server.PhoneBill;
|
||||
|
||||
/// <summary>
|
||||
/// If this entity can recieve phone bills.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class PhoneBillableComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// If this entity must be alive (Not crit) to recieve phone bills.
|
||||
/// This only controls if someone will get a phone bill while alive,
|
||||
/// not if they will keep it if they crit/die.
|
||||
/// Basically, if you die after getting a phone bill, you will still
|
||||
/// lose your PDA and ID if you don't got money.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool RequireLiving = true;
|
||||
}
|
||||
10
Content.Server/Power/Components/UnpowerOnMapInitComponent.cs
Normal file
10
Content.Server/Power/Components/UnpowerOnMapInitComponent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Content.Server.Power.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Fetches entity's <see cref="BatteryComponent"/> and unpowers it.
|
||||
/// Runs at MapInit and removes itself afterwards.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class UnpowerOnMapInitComponent : Component
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Server.Power.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Power.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles unpowering entities on map init>
|
||||
/// </summary>
|
||||
public sealed partial class UnpowerAllOnMapInitSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly BatterySystem _battery = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<UnpowerOnMapInitComponent, MapInitEvent>(OnMapInit, after: [typeof(BatterySystem)]);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<UnpowerOnMapInitComponent> entity, ref MapInitEvent args)
|
||||
{
|
||||
if (TryComp<BatteryComponent>(entity, out var battery) && _random.Prob(0.9f)) // this is a piece of code for one day, i am hardcoding this
|
||||
{
|
||||
_battery.SetCharge(entity, 0f);
|
||||
}
|
||||
|
||||
// Our work here is done
|
||||
RemCompDeferred(entity, entity.Comp);
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@ namespace Content.Server.RoundEnd
|
||||
null,
|
||||
Color.Gold);
|
||||
|
||||
if (!_autoCalledBefore) _audio.PlayGlobal("/Audio/Announcements/shuttlecalled.ogg", Filter.Broadcast(), true, AudioParams.Default.AddVolume(-4)); // Corvax-Announcements: Custom sound for auto-called
|
||||
if (!_autoCalledBefore) _audio.PlayGlobal("/Audio/Announcements/Intern/shuttlecalled.ogg", Filter.Broadcast(), true, AudioParams.Default.AddVolume(-4)); // Corvax-Announcements: Custom sound for auto-called
|
||||
else _audio.PlayGlobal("/Audio/Corvax/Announcements/crew_s_called.ogg", Filter.Broadcast(), true, AudioParams.Default.AddVolume(-4)); // Corvax-Announcements
|
||||
|
||||
LastCountdownStart = _gameTiming.CurTime;
|
||||
@@ -238,7 +238,7 @@ namespace Content.Server.RoundEnd
|
||||
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("round-end-system-shuttle-recalled-announcement"),
|
||||
Loc.GetString("Station"), false, colorOverride: Color.Gold);
|
||||
|
||||
_audio.PlayGlobal("/Audio/Announcements/shuttlerecalled.ogg", Filter.Broadcast(), true, AudioParams.Default.AddVolume(-4)); // Corvax-Announcements: Decrease volume
|
||||
_audio.PlayGlobal("/Audio/Announcements/Intern/shuttlerecalled.ogg", Filter.Broadcast(), true, AudioParams.Default.AddVolume(-4)); // Corvax-Announcements: Decrease volume
|
||||
|
||||
LastCountdownStart = null;
|
||||
ExpectedCountdownEnd = null;
|
||||
|
||||
@@ -391,7 +391,7 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
|
||||
|
||||
var audioFile = result.ResultType == ShuttleDockResultType.NoDock
|
||||
? "/Audio/Misc/notice1.ogg"
|
||||
: "/Audio/Announcements/shuttle_dock.ogg";
|
||||
: "/Audio/Announcements/Intern/shuttledock.ogg";
|
||||
|
||||
// TODO: Need filter extensions or something don't blame me.
|
||||
_audio.PlayGlobal(audioFile, Filter.Broadcast(), true);
|
||||
|
||||
@@ -21,7 +21,12 @@ public sealed class FrenchAccentSystem : EntitySystem
|
||||
SubscribeLocalEvent<FrenchAccentComponent, AccentGetEvent>(OnAccentGet);
|
||||
}
|
||||
|
||||
public string Accentuate(string message, FrenchAccentComponent component)
|
||||
public string Accentuate(string message, FrenchAccentComponent _)
|
||||
{
|
||||
return Accentuate(message);
|
||||
}
|
||||
|
||||
public string Accentuate(string message)
|
||||
{
|
||||
var msg = message;
|
||||
|
||||
|
||||
@@ -25,7 +25,12 @@ public sealed class MobsterAccentSystem : EntitySystem
|
||||
SubscribeLocalEvent<MobsterAccentComponent, AccentGetEvent>(OnAccentGet);
|
||||
}
|
||||
|
||||
public string Accentuate(string message, MobsterAccentComponent component)
|
||||
private string Accentuate(string message, MobsterAccentComponent component)
|
||||
{
|
||||
return Accentuate(message, component.IsBoss);
|
||||
}
|
||||
|
||||
public string Accentuate(string message, bool isBoss = true)
|
||||
{
|
||||
// Order:
|
||||
// Do text manipulations first
|
||||
@@ -72,7 +77,7 @@ public sealed class MobsterAccentSystem : EntitySystem
|
||||
//So the suffix can be allcapped
|
||||
var lastWordAllCaps = !RegexLastWord.Match(msg).Value.Any(char.IsLower);
|
||||
var suffix = "";
|
||||
if (component.IsBoss)
|
||||
if (isBoss)
|
||||
{
|
||||
var pick = _random.Next(1, 4);
|
||||
suffix = Loc.GetString($"accent-mobster-suffix-boss-{pick}");
|
||||
@@ -90,6 +95,7 @@ public sealed class MobsterAccentSystem : EntitySystem
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
||||
private void OnAccentGet(EntityUid uid, MobsterAccentComponent component, AccentGetEvent args)
|
||||
{
|
||||
args.Message = Accentuate(args.Message, component);
|
||||
|
||||
@@ -13,7 +13,12 @@ public sealed class MumbleAccentSystem : EntitySystem
|
||||
SubscribeLocalEvent<MumbleAccentComponent, AccentGetEvent>(OnAccentGet);
|
||||
}
|
||||
|
||||
public string Accentuate(string message, MumbleAccentComponent component)
|
||||
private string Accentuate(string message, MumbleAccentComponent _)
|
||||
{
|
||||
return Accentuate(message);
|
||||
}
|
||||
|
||||
public string Accentuate(string message)
|
||||
{
|
||||
return _replacement.ApplyReplacements(message, "mumble");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.Speech.Components;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems
|
||||
{
|
||||
|
||||
@@ -42,6 +42,11 @@ public sealed class PirateAccentSystem : EntitySystem
|
||||
return msg;
|
||||
}
|
||||
|
||||
public string Accentuate(string message)
|
||||
{
|
||||
return Accentuate(message, new PirateAccentComponent { YarrChance = 0.3f });
|
||||
}
|
||||
|
||||
private void OnAccentGet(EntityUid uid, PirateAccentComponent component, AccentGetEvent args)
|
||||
{
|
||||
args.Message = Accentuate(args.Message, component);
|
||||
|
||||
66
Content.Server/Speech/EntitySystems/RandomAccentuator.cs
Normal file
66
Content.Server/Speech/EntitySystems/RandomAccentuator.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public static class RandomAccentuator
|
||||
{
|
||||
private const float DefaultAccentuationChance = 0.25f;
|
||||
|
||||
private const float DefaultReaccentuationChance = 0.15f;
|
||||
|
||||
private const float MaxReaccentuations = 4;
|
||||
|
||||
public static string MaybeAccentuate(string message,
|
||||
float chance = DefaultAccentuationChance,
|
||||
float reaccentuationChance = DefaultReaccentuationChance)
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
var singleAccentuator = new SingleAccentuator();
|
||||
return random.Prob(chance)
|
||||
? MaybeAccentuateDirect(message, singleAccentuator, random, reaccentuationChance)
|
||||
: message;
|
||||
}
|
||||
|
||||
public static FormattedMessage MaybeAccentuate(FormattedMessage message,
|
||||
float chance = DefaultAccentuationChance,
|
||||
float reaccentuationChance = DefaultReaccentuationChance)
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
var singleAccentuator = new SingleAccentuator();
|
||||
var newMessage = new FormattedMessage();
|
||||
|
||||
foreach (var node in message)
|
||||
{
|
||||
if (random.Prob(chance) && node.Name is null && node.Value.TryGetString(out var text) &&
|
||||
!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
var accentedText = MaybeAccentuateDirect(text, singleAccentuator, random, reaccentuationChance);
|
||||
newMessage.PushTag(new MarkupNode(accentedText));
|
||||
}
|
||||
else
|
||||
{
|
||||
newMessage.PushTag(node);
|
||||
}
|
||||
}
|
||||
|
||||
return newMessage;
|
||||
}
|
||||
|
||||
private static string MaybeAccentuateDirect(string message,
|
||||
SingleAccentuator singleAccentuator,
|
||||
IRobustRandom random,
|
||||
float reaccentuationChance)
|
||||
{
|
||||
for (var i = 0; i < MaxReaccentuations; i++)
|
||||
{
|
||||
if (i > 0 && !random.Prob(reaccentuationChance))
|
||||
continue;
|
||||
|
||||
singleAccentuator.NextSystem();
|
||||
message = singleAccentuator.Accentuate(message);
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
59
Content.Server/Speech/EntitySystems/SingleAccentuator.cs
Normal file
59
Content.Server/Speech/EntitySystems/SingleAccentuator.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class SingleAccentuator
|
||||
{
|
||||
private EntitySystem? _accentSystem;
|
||||
|
||||
private readonly IReadOnlyList<EntitySystem> _accentSystems;
|
||||
|
||||
public SingleAccentuator()
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
_accentSystems = new List<EntitySystem>
|
||||
{
|
||||
entMan.EntitySysManager.GetEntitySystem<OwOAccentSystem>(),
|
||||
entMan.EntitySysManager.GetEntitySystem<GermanAccentSystem>(),
|
||||
entMan.EntitySysManager.GetEntitySystem<RussianAccentSystem>(),
|
||||
entMan.EntitySysManager.GetEntitySystem<FrenchAccentSystem>(),
|
||||
entMan.EntitySysManager.GetEntitySystem<MumbleAccentSystem>(),
|
||||
entMan.EntitySysManager.GetEntitySystem<SlurredSystem>(),
|
||||
entMan.EntitySysManager.GetEntitySystem<MobsterAccentSystem>(),
|
||||
entMan.EntitySysManager.GetEntitySystem<PirateAccentSystem>(),
|
||||
entMan.EntitySysManager.GetEntitySystem<MonkeyAccentSystem>(),
|
||||
entMan.EntitySysManager.GetEntitySystem<StutteringSystem>(),
|
||||
};
|
||||
NextSystem();
|
||||
}
|
||||
|
||||
public void NextSystem()
|
||||
{
|
||||
_accentSystem = GetRandomAccentSystem();
|
||||
}
|
||||
|
||||
private EntitySystem GetRandomAccentSystem()
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
|
||||
return random.Pick(_accentSystems);
|
||||
}
|
||||
|
||||
public string Accentuate(string message)
|
||||
{
|
||||
return _accentSystem switch
|
||||
{
|
||||
OwOAccentSystem owoAccentSystem => owoAccentSystem.Accentuate(message),
|
||||
GermanAccentSystem germanAccentSystem => germanAccentSystem.Accentuate(message),
|
||||
RussianAccentSystem russianAccentSystem => russianAccentSystem.Accentuate(message),
|
||||
FrenchAccentSystem frenchAccentSystem => frenchAccentSystem.Accentuate(message),
|
||||
MumbleAccentSystem mumbleAccentSystem => mumbleAccentSystem.Accentuate(message),
|
||||
SlurredSystem slurredSystem => slurredSystem.Accentuate(message),
|
||||
MobsterAccentSystem mobsterAccentSystem => mobsterAccentSystem.Accentuate(message),
|
||||
PirateAccentSystem pirateAccentSystem => pirateAccentSystem.Accentuate(message),
|
||||
MonkeyAccentSystem monkeyAccentSystem => monkeyAccentSystem.Accentuate(message),
|
||||
StutteringSystem stutteringSystem => stutteringSystem.Accentuate(message),
|
||||
_ => message
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ public sealed class SlurredSystem : SharedSlurredSystem
|
||||
args.Message = Accentuate(args.Message, scale);
|
||||
}
|
||||
|
||||
private string Accentuate(string message, float scale)
|
||||
public string Accentuate(string message, float scale = 0.3f)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
|
||||
@@ -70,5 +70,10 @@ namespace Content.Server.Speech.EntitySystems
|
||||
|
||||
return finalMessage.ToString();
|
||||
}
|
||||
|
||||
public string Accentuate(string message)
|
||||
{
|
||||
return Accentuate(message, new StutteringAccentComponent());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed partial class MeteorSwarmComponent : Component
|
||||
public LocId? Announcement = "station-event-meteor-swarm-start-announcement";
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier? AnnouncementSound = new SoundPathSpecifier("/Audio/Announcements/meteors.ogg")
|
||||
public SoundSpecifier? AnnouncementSound = new SoundPathSpecifier("/Audio/Announcements/Intern/meteors.ogg")
|
||||
{
|
||||
Params = new()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Content.Server.Nuke;
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(NukeCalibrationRule))]
|
||||
public sealed partial class NukeCalibrationRuleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Sound of the announcement to play if automatic disarm of the nuke was unsuccessful.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier AutoDisarmFailedSound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg");
|
||||
/// <summary>
|
||||
/// Sound of the announcement to play if automatic disarm of the nuke was successful.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier AutoDisarmSuccessSound = new SoundPathSpecifier("/Audio/Misc/notice2.ogg");
|
||||
|
||||
[DataField]
|
||||
public EntityUid AffectedStation;
|
||||
/// <summary>
|
||||
/// The nuke that was '''calibrated'''.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntityUid AffectedNuke;
|
||||
[DataField]
|
||||
public float NukeTimer = 170f;
|
||||
[DataField]
|
||||
public float AutoDisarmChance = 0.5f;
|
||||
[DataField]
|
||||
public float TimeUntilFirstAnnouncement = 15f;
|
||||
[DataField]
|
||||
public bool FirstAnnouncementMade = false;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Content.Server.StationEvents.Events;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class PhoneBillRuleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How long until payment is collected.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Delay = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Cost of the phone bill per PDA or ID.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int Price = 0;
|
||||
|
||||
/// <summary>
|
||||
/// A list of PDAs and IDs on someone, keyed by that person.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<EntityUid, List<EntityUid>> YoullPayForThis = new();
|
||||
|
||||
/// <summary>
|
||||
/// The sound that plays when the first announcement is sent.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? InitialSound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The sound that plays when a failure announcement is sent.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? FailureSound = new SoundPathSpecifier("/Audio/Effects/sadtrombone.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The sound that plays when a success announcement is sent.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundSpecifier? SuccessSound = new SoundPathSpecifier("/Audio/Announcements/announce.ogg");
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Content.Server.StationEvents.Components;
|
||||
|
||||
/// <summary>
|
||||
/// component for a rule that makes everyone hurt and drunk, calls the hour-long shuttle and sets alert to red
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class VeryBadDayRuleComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
110
Content.Server/StationEvents/Events/NukeCalibrationRule.cs
Normal file
110
Content.Server/StationEvents/Events/NukeCalibrationRule.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Server.Nuke;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Station.Components;
|
||||
using Content.Shared.Nuke;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.StationEvents.Events
|
||||
{
|
||||
public sealed class NukeCalibrationRule : StationEventSystem<NukeCalibrationRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly NukeSystem _nukeSystem = default!;
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popups = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedMapSystem _map = default!;
|
||||
|
||||
protected override void Started(EntityUid uid, NukeCalibrationRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
|
||||
if (!TryGetRandomStation(out var affectedStation))
|
||||
return;
|
||||
|
||||
component.AffectedStation = affectedStation.Value;
|
||||
|
||||
var nukeQuery = AllEntityQuery<NukeComponent, TransformComponent>();
|
||||
while (nukeQuery.MoveNext(out var nuke, out var nukeComponent, out var nukeTransform))
|
||||
{
|
||||
// let's not arm the nuke if it isn't on station
|
||||
if (CompOrNull<StationMemberComponent>(nukeTransform.GridUid)?.Station != affectedStation)
|
||||
continue;
|
||||
|
||||
if (nukeComponent.Status == NukeStatus.ARMED)
|
||||
continue;
|
||||
|
||||
// If it isn't anchored, then try to anchor it. If we can't anchor it, just continue.
|
||||
if (!nukeTransform.Anchored)
|
||||
if (!_transform.AnchorEntity(nuke, nukeTransform))
|
||||
continue;
|
||||
|
||||
_nukeSystem.SetRemainingTime(nuke, component.NukeTimer);
|
||||
_nukeSystem.ArmBomb(nuke, nukeComponent);
|
||||
component.AffectedNuke = nuke;
|
||||
|
||||
if (!nukeComponent.DiskSlot.HasItem)
|
||||
_popups.PopupEntity(Loc.GetString("station-event-nuke-calibration-arm-popup"), nuke, PopupType.LargeCaution);
|
||||
else
|
||||
{
|
||||
_transform.SetCoordinates(nukeComponent.DiskSlot.ContainerSlot!.ContainedEntity!.Value, nukeTransform.Coordinates);
|
||||
_popups.PopupEntity(Loc.GetString("station-event-nuke-calibration-arm-and-disk-ejected-popup"), nuke, PopupType.LargeCaution);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Ended(EntityUid uid, NukeCalibrationRuleComponent component, GameRuleComponent gameRule, GameRuleEndedEvent args)
|
||||
{
|
||||
base.Ended(uid, component, gameRule, args);
|
||||
|
||||
if (!TryComp<NukeComponent>(component.AffectedNuke, out var nukeComp))
|
||||
return;
|
||||
|
||||
// Lucky enough, so nuke gets disarmed for you :D
|
||||
if (RobustRandom.NextFloat() <= component.AutoDisarmChance)
|
||||
{
|
||||
// 220
|
||||
_nukeSystem.SetRemainingTime(component.AffectedNuke, nukeComp.Timer);
|
||||
if (nukeComp.Status != NukeStatus.ARMED)
|
||||
return;
|
||||
|
||||
_nukeSystem.DisarmBomb(component.AffectedNuke, nukeComp);
|
||||
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("station-event-nuke-calibration-disarm-success-announcement"), playSound: false, colorOverride: Color.Green);
|
||||
_audioSystem.PlayGlobal(component.AutoDisarmSuccessSound, Filter.Broadcast(), true);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (nukeComp.Status != NukeStatus.ARMED)
|
||||
return;
|
||||
|
||||
// Ooops.....
|
||||
_nukeSystem.SetDiskBypassEnabled(component.AffectedNuke, true, true, nukeComp);
|
||||
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("station-event-nuke-calibration-disarm-fail-announcement"), playSound: false, colorOverride: Color.Crimson);
|
||||
_audioSystem.PlayGlobal(component.AutoDisarmFailedSound, Filter.Broadcast(), true);
|
||||
}
|
||||
|
||||
protected override void ActiveTick(EntityUid uid, NukeCalibrationRuleComponent component, GameRuleComponent gameRule, float frameTime)
|
||||
{
|
||||
base.ActiveTick(uid, component, gameRule, frameTime);
|
||||
|
||||
if (component.FirstAnnouncementMade == true)
|
||||
return;
|
||||
|
||||
component.TimeUntilFirstAnnouncement -= frameTime;
|
||||
if (component.TimeUntilFirstAnnouncement > 0f)
|
||||
return;
|
||||
|
||||
component.FirstAnnouncementMade = true;
|
||||
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("station-event-nuke-calibration-midway-announcement"), colorOverride: Color.Yellow);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
Content.Server/StationEvents/Events/PhoneBillRule.cs
Normal file
110
Content.Server/StationEvents/Events/PhoneBillRule.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using Content.Server.PhoneBill;
|
||||
using Content.Server.Stack;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.Cargo.Components;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Stacks;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server.StationEvents.Events;
|
||||
|
||||
public sealed class PhoneBillRule : StationEventSystem<PhoneBillRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
[Dependency] private readonly ContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly StackSystem _stackSystem = default!;
|
||||
|
||||
protected override void Added(EntityUid uid, PhoneBillRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
||||
{
|
||||
base.Added(uid, component, gameRule, args);
|
||||
ChatSystem.DispatchGlobalAnnouncement( Loc.GetString("station-event-phone-bill-announcement", ("delay", (int)component.Delay.TotalSeconds), ("price", component.Price)), announcementSound: component.InitialSound, colorOverride: Color.Red);
|
||||
|
||||
List<EntityUid> PhoneBillable = new();
|
||||
|
||||
var billableQuery = _entMan.AllEntityQueryEnumerator<PhoneBillableComponent>();
|
||||
while (billableQuery.MoveNext(out var target, out var billable))
|
||||
{
|
||||
if (billable.RequireLiving &&
|
||||
TryComp(uid, out MobStateComponent? mobState) &&
|
||||
mobState.CurrentState != MobState.Alive)
|
||||
continue;
|
||||
PhoneBillable.Add(target);
|
||||
}
|
||||
|
||||
var billTargetQuery = _entMan.AllEntityQueryEnumerator<PhoneBillTargetComponent>();
|
||||
while (billTargetQuery.MoveNext(out var target, out var _))
|
||||
{
|
||||
var lastTarget = target;
|
||||
while (_containerSystem.TryGetContainingContainer((lastTarget, null, null), out var container))
|
||||
{
|
||||
if (PhoneBillable.Contains(container.Owner))
|
||||
{
|
||||
if (!component.YoullPayForThis.TryGetValue(container.Owner, out var list))
|
||||
{
|
||||
list = new();
|
||||
component.YoullPayForThis[container.Owner] = list;
|
||||
}
|
||||
list.Add(target);
|
||||
break;
|
||||
}
|
||||
|
||||
lastTarget = container.Owner;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, PhoneBillRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
{
|
||||
if (component.YoullPayForThis.Count == 0)
|
||||
return; // Huh.
|
||||
|
||||
int unpaid = 0;
|
||||
foreach (var (forsaken, items) in component.YoullPayForThis)
|
||||
{
|
||||
var tendered = false;
|
||||
var required = component.Price * items.Count;
|
||||
foreach (var ransom in _inventorySystem.GetHandOrInventoryEntities(forsaken))
|
||||
{
|
||||
if (HasComp(ransom, typeof(CashComponent))
|
||||
&& TryComp(ransom, out StackComponent? stack)
|
||||
&& stack.Count >= required)
|
||||
{
|
||||
_stackSystem.SetCount(ransom, stack.Count - required);
|
||||
tendered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (tendered)
|
||||
continue;
|
||||
|
||||
// THEN PAY WITH YOUR BLOOD!
|
||||
|
||||
unpaid++;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (HasComp<ContainerManagerComponent>(item))
|
||||
foreach (var container in _containerSystem.GetAllContainers(item))
|
||||
{
|
||||
_containerSystem.EmptyContainer(container);
|
||||
}
|
||||
_entMan.QueueDeleteEntity(item);
|
||||
}
|
||||
}
|
||||
|
||||
if (unpaid == 0)
|
||||
{
|
||||
ChatSystem.DispatchGlobalAnnouncement(Loc.GetString("station-event-phone-bill-allpaid-announcement"), announcementSound: component.SuccessSound);
|
||||
}
|
||||
else
|
||||
{
|
||||
var unpaidPercent = (int)(100f * unpaid / component.YoullPayForThis.Count);
|
||||
ChatSystem.DispatchGlobalAnnouncement(Loc.GetString("station-event-phone-bill-unpaid-announcement", ("percent", unpaidPercent)), announcementSound: component.FailureSound, colorOverride: Color.Yellow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Content.Server.Administration.Logs;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Speech.EntitySystems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.Database;
|
||||
@@ -46,7 +47,11 @@ public abstract class StationEventSystem<T> : GameRuleSystem<T> where T : ICompo
|
||||
Filter allPlayersInGame = Filter.Empty().AddWhere(GameTicker.UserHasJoinedGame);
|
||||
|
||||
if (stationEvent.StartAnnouncement != null)
|
||||
ChatSystem.DispatchFilteredAnnouncement(allPlayersInGame, Loc.GetString(stationEvent.StartAnnouncement), playSound: false, colorOverride: stationEvent.StartAnnouncementColor);
|
||||
{
|
||||
var announcement = Loc.GetString(stationEvent.StartAnnouncement);
|
||||
announcement = RandomAccentuator.MaybeAccentuate(announcement);
|
||||
ChatSystem.DispatchFilteredAnnouncement(allPlayersInGame, announcement, playSound: false, colorOverride: stationEvent.StartAnnouncementColor);
|
||||
}
|
||||
|
||||
Audio.PlayGlobal(stationEvent.StartAudio, allPlayersInGame, true);
|
||||
}
|
||||
|
||||
44
Content.Server/StationEvents/Events/VeryBadDayRule.cs
Normal file
44
Content.Server/StationEvents/Events/VeryBadDayRule.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Content.Server.AlertLevel;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Drunk;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.StationEvents.Events;
|
||||
|
||||
public sealed class VeryBadDayRule : StationEventSystem<VeryBadDayRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly SharedDrunkSystem _drunkSystem = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
|
||||
[Dependency] private readonly AlertLevelSystem _alertLevelSystem = default!;
|
||||
|
||||
protected override void Started(EntityUid uid,
|
||||
VeryBadDayRuleComponent component,
|
||||
GameRuleComponent gameRule,
|
||||
GameRuleStartedEvent args)
|
||||
{
|
||||
base.Started(uid, component, gameRule, args);
|
||||
|
||||
if (!TryGetRandomStation(out var station))
|
||||
return;
|
||||
|
||||
var query = EntityQueryEnumerator<MindContainerComponent, HumanoidAppearanceComponent>();
|
||||
|
||||
while (query.MoveNext(out var ent, out _, out _))
|
||||
{
|
||||
_drunkSystem.TryApplyDrunkenness(ent, 1000);
|
||||
_damageableSystem.TryChangeDamage(ent,
|
||||
new DamageSpecifier(_protoMan.Index<DamageGroupPrototype>("Brute"), _random.Next(5, 50)));
|
||||
}
|
||||
_alertLevelSystem.SetLevel((EntityUid)station, "red", false, true, true);
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,10 @@ public sealed class HandTeleporterSystem : EntitySystem
|
||||
var timeout = EnsureComp<PortalTimeoutComponent>(user);
|
||||
timeout.EnteredPortal = null;
|
||||
component.FirstPortal = Spawn(component.FirstPortalPrototype, Transform(user).Coordinates);
|
||||
|
||||
if (component.AllowPortalsOnDifferentMaps && TryComp<PortalComponent>(component.FirstPortal, out var portal))
|
||||
portal.CanTeleportToOtherMaps = true;
|
||||
|
||||
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{ToPrettyString(user):player} opened {ToPrettyString(component.FirstPortal.Value)} at {Transform(component.FirstPortal.Value).Coordinates} using {ToPrettyString(uid)}");
|
||||
_audio.PlayPvs(component.NewPortalSound, uid);
|
||||
}
|
||||
@@ -113,6 +117,10 @@ public sealed class HandTeleporterSystem : EntitySystem
|
||||
var timeout = EnsureComp<PortalTimeoutComponent>(user);
|
||||
timeout.EnteredPortal = null;
|
||||
component.SecondPortal = Spawn(component.SecondPortalPrototype, Transform(user).Coordinates);
|
||||
|
||||
if (component.AllowPortalsOnDifferentMaps && TryComp<PortalComponent>(component.SecondPortal, out var portal))
|
||||
portal.CanTeleportToOtherMaps = true;
|
||||
|
||||
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{ToPrettyString(user):player} opened {ToPrettyString(component.SecondPortal.Value)} at {Transform(component.SecondPortal.Value).Coordinates} linked to {ToPrettyString(component.FirstPortal!.Value)} using {ToPrettyString(uid)}");
|
||||
_link.TryLink(component.FirstPortal!.Value, component.SecondPortal.Value, true);
|
||||
_audio.PlayPvs(component.NewPortalSound, uid);
|
||||
|
||||
@@ -95,7 +95,7 @@ namespace Content.Server.Traitor.Uplink.Commands
|
||||
|
||||
// Finally add uplink
|
||||
var uplinkSys = _entManager.System<UplinkSystem>();
|
||||
if (!uplinkSys.AddUplink(user, 20, uplinkEntity: uplinkEntity, giveDiscounts: isDiscounted))
|
||||
if (!uplinkSys.AddUplink(user, 20, out var _, uplinkEntity: uplinkEntity, giveDiscounts: isDiscounted))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("add-uplink-command-error-2"));
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using Content.Shared.PDA;
|
||||
using Content.Shared.Store;
|
||||
using Content.Shared.Store.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Traitor.Uplink;
|
||||
|
||||
@@ -21,6 +22,7 @@ public sealed class UplinkSystem : EntitySystem
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
[ValidatePrototypeId<CurrencyPrototype>]
|
||||
public const string TelecrystalCurrencyPrototype = "Telecrystal";
|
||||
@@ -32,12 +34,14 @@ public sealed class UplinkSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <param name="user">The person who is getting the uplink</param>
|
||||
/// <param name="balance">The amount of currency on the uplink. If null, will just use the amount specified in the preset.</param>
|
||||
/// <param name="adjustedBalance">ThT amooma oo currrruc oo tht upllpu. II nuun, wiiw juuj usu tht amooma speciceps ii tht preerp.</param>
|
||||
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
|
||||
/// <param name="giveDiscounts">Marker that enables discounts for uplink items.</param>
|
||||
/// <returns>Whether or not the uplink was added successfully</returns>
|
||||
public bool AddUplink(
|
||||
EntityUid user,
|
||||
FixedPoint2 balance,
|
||||
out FixedPoint2 adjustedBalance,
|
||||
EntityUid? uplinkEntity = null,
|
||||
bool giveDiscounts = false)
|
||||
{
|
||||
@@ -45,6 +49,11 @@ public sealed class UplinkSystem : EntitySystem
|
||||
|
||||
uplinkEntity ??= FindUplinkTarget(user);
|
||||
|
||||
float x = MathF.Min(0.999999999f, _random.NextFloat()); // avoid the logarithm becoming infinite
|
||||
balance *= 0.75f - Math.Log2(1f - x); // still more than twice as much on average, but you have a ~20% chance to get less. Minimum is 75% of original value
|
||||
balance = Math.Round((float)balance);
|
||||
adjustedBalance = balance;
|
||||
|
||||
if (uplinkEntity == null)
|
||||
return ImplantUplink(user, balance, giveDiscounts);
|
||||
|
||||
|
||||
@@ -14,9 +14,6 @@ public sealed class DiseaseProtectionSystem : EntitySystem
|
||||
|
||||
private void OnMaskToggled(Entity<DiseaseProtectionComponent> ent, ref ItemMaskToggledEvent args)
|
||||
{
|
||||
if (args.Mask.Comp is not { } maskComp)
|
||||
return;
|
||||
|
||||
ent.Comp.IsActive = maskComp.IsToggled;
|
||||
ent.Comp.IsActive = args.IsToggled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace Content.Shared.Access.Components;
|
||||
public sealed partial class IdCardConsoleComponent : Component
|
||||
{
|
||||
public const int MaxFullNameLength = 30;
|
||||
public const int MaxJobTitleLength = 30;
|
||||
public const int MaxJobTitleLength = 60;
|
||||
|
||||
public static string PrivilegedIdCardSlotId = "IdCardConsole-privilegedId";
|
||||
public static string TargetIdCardSlotId = "IdCardConsole-targetId";
|
||||
|
||||
6
Content.Shared/Body/Events/RespiratorEvents.cs
Normal file
6
Content.Shared/Body/Events/RespiratorEvents.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using Content.Shared.Alert;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Body.Events;
|
||||
|
||||
public sealed partial class ToggleBreathingAlertEvent : BaseAlertEvent;
|
||||
@@ -65,13 +65,13 @@ public sealed partial class ToggleMaskEvent : InstantActionEvent { }
|
||||
/// Event raised on the mask entity when it is toggled.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct ItemMaskToggledEvent(Entity<MaskComponent> Mask, EntityUid? Wearer);
|
||||
public readonly record struct ItemMaskToggledEvent(EntityUid Wearer, string? equippedPrefix, bool IsToggled, bool IsEquip);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on the entity wearing the mask when it is toggled.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public readonly record struct WearerMaskToggledEvent(Entity<MaskComponent> Mask);
|
||||
public readonly record struct WearerMaskToggledEvent(bool IsToggled);
|
||||
|
||||
/// <summary>
|
||||
/// Raised on the clothing entity when it is equipped to a valid slot,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Clothing.EntitySystems;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Inventory;
|
||||
@@ -29,15 +28,8 @@ public sealed partial class ClothingComponent : Component
|
||||
[DataField("quickEquip")]
|
||||
public bool QuickEquip = true;
|
||||
|
||||
/// <summary>
|
||||
/// The slots in which the clothing is considered "worn" or "equipped". E.g., putting shoes in your pockets does not
|
||||
/// equip them as far as clothing related events are concerned.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Note that this may be a combination of different slot flags, not a singular bit.
|
||||
/// </remarks>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField(required: true)]
|
||||
[DataField("slots", required: true)]
|
||||
[Access(typeof(ClothingSystem), typeof(InventorySystem), Other = AccessPermissions.ReadExecute)]
|
||||
public SlotFlags Slots = SlotFlags.NONE;
|
||||
|
||||
@@ -68,25 +60,9 @@ public sealed partial class ClothingComponent : Component
|
||||
public string? RsiPath;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the inventory slot the clothing is currently in.
|
||||
/// Note that this being non-null does not mean the clothing is considered "worn" or "equipped" unless the slot
|
||||
/// satisfies the <see cref="Slots"/> flags.
|
||||
/// Name of the inventory slot the clothing is in.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string? InSlot;
|
||||
// TODO CLOTHING
|
||||
// Maybe keep this null unless its in a valid slot?
|
||||
// To lazy to figure out ATM if that would break anything.
|
||||
// And when doing this, combine InSlot and InSlotFlag, as it'd be a breaking change for downstreams anyway
|
||||
|
||||
/// <summary>
|
||||
/// Slot flags of the slot the clothing is currently in. See also <see cref="InSlot"/>.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SlotFlags? InSlotFlag;
|
||||
// TODO CLOTHING
|
||||
// Maybe keep this null unless its in a valid slot?
|
||||
// And when doing this, combine InSlot and InSlotFlag, as it'd be a breaking change for downstreams anyway
|
||||
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public TimeSpan EquipDelay = TimeSpan.Zero;
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed partial class FoldableClothingComponent : Component
|
||||
[DataField]
|
||||
public SlotFlags? UnfoldedSlots;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// What equipped prefix does this have while in folded form?
|
||||
/// </summary>
|
||||
@@ -35,11 +36,11 @@ public sealed partial class FoldableClothingComponent : Component
|
||||
/// Which layers does this hide when Unfolded? See <see cref="HumanoidVisualLayers"/> and <see cref="HideLayerClothingComponent"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<HumanoidVisualLayers>? UnfoldedHideLayers = new();
|
||||
public HashSet<HumanoidVisualLayers> UnfoldedHideLayers = new();
|
||||
|
||||
/// <summary>
|
||||
/// Which layers does this hide when folded? See <see cref="HumanoidVisualLayers"/> and <see cref="HideLayerClothingComponent"/>
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<HumanoidVisualLayers>? FoldedHideLayers = new();
|
||||
public HashSet<HumanoidVisualLayers> FoldedHideLayers = new();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Clothing.Components;
|
||||
@@ -12,17 +11,10 @@ namespace Content.Shared.Clothing.Components;
|
||||
public sealed partial class HideLayerClothingComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The appearance layer(s) to hide. Use <see cref='Layers'>Layers</see> instead.
|
||||
/// The appearance layer to hide.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[Obsolete("This attribute is deprecated, please use Layers instead.")]
|
||||
public HashSet<HumanoidVisualLayers>? Slots;
|
||||
|
||||
/// <summary>
|
||||
/// A map of the appearance layer(s) to hide, and the equipment slot that should hide them.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<HumanoidVisualLayers, SlotFlags> Layers = new();
|
||||
public HashSet<HumanoidVisualLayers> Slots = new();
|
||||
|
||||
/// <summary>
|
||||
/// If true, the layer will only hide when the item is in a toggled state (e.g. masks)
|
||||
|
||||
@@ -8,22 +8,15 @@ namespace Content.Shared.Clothing.Components;
|
||||
[Access(typeof(MaskSystem))]
|
||||
public sealed partial class MaskComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Action for toggling a mask (e.g., pulling the mask down or putting it back up)
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntProtoId ToggleAction = "ActionToggleMask";
|
||||
|
||||
/// <summary>
|
||||
/// Action for toggling a mask (e.g., pulling the mask down or putting it back up)
|
||||
/// This mask can be toggled (pulled up/down)
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid? ToggleActionEntity;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the mask is currently toggled (e.g., pulled down).
|
||||
/// This generally disables some of the mask's functionality.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool IsToggled;
|
||||
|
||||
@@ -34,13 +27,13 @@ public sealed partial class MaskComponent : Component
|
||||
public string EquippedPrefix = "up";
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="false"/>, the mask will not be toggleable.
|
||||
/// When <see langword="true"/> will function normally, otherwise will not react to events
|
||||
/// </summary>
|
||||
[DataField("enabled"), AutoNetworkedField]
|
||||
public bool IsToggleable = true;
|
||||
public bool IsEnabled = true;
|
||||
|
||||
/// <summary>
|
||||
/// When <see langword="true"/> will disable <see cref="IsToggleable"/> when folded
|
||||
/// When <see langword="true"/> will disable <see cref="IsEnabled"/> when folded
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool DisableOnFolded;
|
||||
|
||||
@@ -16,9 +16,9 @@ public abstract class ClothingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedItemSystem _itemSys = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containerSys = default!;
|
||||
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _invSystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly HideLayerClothingSystem _hideLayer = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -29,6 +29,7 @@ public abstract class ClothingSystem : EntitySystem
|
||||
SubscribeLocalEvent<ClothingComponent, ComponentHandleState>(OnHandleState);
|
||||
SubscribeLocalEvent<ClothingComponent, GotEquippedEvent>(OnGotEquipped);
|
||||
SubscribeLocalEvent<ClothingComponent, GotUnequippedEvent>(OnGotUnequipped);
|
||||
SubscribeLocalEvent<ClothingComponent, ItemMaskToggledEvent>(OnMaskToggled);
|
||||
|
||||
SubscribeLocalEvent<ClothingComponent, ClothingEquipDoAfterEvent>(OnEquipDoAfter);
|
||||
SubscribeLocalEvent<ClothingComponent, ClothingUnequipDoAfterEvent>(OnUnequipDoAfter);
|
||||
@@ -84,19 +85,59 @@ public abstract class ClothingSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void ToggleVisualLayers(EntityUid equipee, HashSet<HumanoidVisualLayers> layers, HashSet<HumanoidVisualLayers> appearanceLayers)
|
||||
{
|
||||
foreach (HumanoidVisualLayers layer in layers)
|
||||
{
|
||||
if (!appearanceLayers.Contains(layer))
|
||||
continue;
|
||||
|
||||
InventorySystem.InventorySlotEnumerator enumerator = _invSystem.GetSlotEnumerator(equipee);
|
||||
|
||||
bool shouldLayerShow = true;
|
||||
while (enumerator.NextItem(out EntityUid item, out SlotDefinition? slot))
|
||||
{
|
||||
if (TryComp(item, out HideLayerClothingComponent? comp))
|
||||
{
|
||||
if (comp.Slots.Contains(layer))
|
||||
{
|
||||
if (TryComp(item, out ClothingComponent? clothing) && clothing.Slots == slot.SlotFlags)
|
||||
{
|
||||
//Checks for mask toggling. TODO: Make a generic system for this
|
||||
if (comp.HideOnToggle && TryComp(item, out MaskComponent? mask))
|
||||
{
|
||||
if (clothing.EquippedPrefix != mask.EquippedPrefix)
|
||||
{
|
||||
shouldLayerShow = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
shouldLayerShow = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_humanoidSystem.SetLayerVisibility(equipee, layer, shouldLayerShow);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnGotEquipped(EntityUid uid, ClothingComponent component, GotEquippedEvent args)
|
||||
{
|
||||
component.InSlot = args.Slot;
|
||||
component.InSlotFlag = args.SlotFlags;
|
||||
CheckEquipmentForLayerHide(args.Equipment, args.Equipee);
|
||||
|
||||
if ((component.Slots & args.SlotFlags) == SlotFlags.NONE)
|
||||
return;
|
||||
if ((component.Slots & args.SlotFlags) != SlotFlags.NONE)
|
||||
{
|
||||
var gotEquippedEvent = new ClothingGotEquippedEvent(args.Equipee, component);
|
||||
RaiseLocalEvent(uid, ref gotEquippedEvent);
|
||||
|
||||
var gotEquippedEvent = new ClothingGotEquippedEvent(args.Equipee, component);
|
||||
RaiseLocalEvent(uid, ref gotEquippedEvent);
|
||||
|
||||
var didEquippedEvent = new ClothingDidEquippedEvent((uid, component));
|
||||
RaiseLocalEvent(args.Equipee, ref didEquippedEvent);
|
||||
var didEquippedEvent = new ClothingDidEquippedEvent((uid, component));
|
||||
RaiseLocalEvent(args.Equipee, ref didEquippedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnGotUnequipped(EntityUid uid, ClothingComponent component, GotUnequippedEvent args)
|
||||
@@ -111,7 +152,7 @@ public abstract class ClothingSystem : EntitySystem
|
||||
}
|
||||
|
||||
component.InSlot = null;
|
||||
component.InSlotFlag = null;
|
||||
CheckEquipmentForLayerHide(args.Equipment, args.Equipee);
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, ClothingComponent component, ref ComponentGetState args)
|
||||
@@ -121,10 +162,21 @@ public abstract class ClothingSystem : EntitySystem
|
||||
|
||||
private void OnHandleState(EntityUid uid, ClothingComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not ClothingComponentState state)
|
||||
return;
|
||||
if (args.Current is ClothingComponentState state)
|
||||
{
|
||||
SetEquippedPrefix(uid, state.EquippedPrefix, component);
|
||||
if (component.InSlot != null && _containerSys.TryGetContainingContainer((uid, null, null), out var container))
|
||||
{
|
||||
CheckEquipmentForLayerHide(uid, container.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SetEquippedPrefix(uid, state.EquippedPrefix, component);
|
||||
private void OnMaskToggled(Entity<ClothingComponent> ent, ref ItemMaskToggledEvent args)
|
||||
{
|
||||
//TODO: sprites for 'pulled down' state. defaults to invisible due to no sprite with this prefix
|
||||
SetEquippedPrefix(ent, args.IsToggled ? args.equippedPrefix : null, ent);
|
||||
CheckEquipmentForLayerHide(ent.Owner, args.Wearer);
|
||||
}
|
||||
|
||||
private void OnEquipDoAfter(Entity<ClothingComponent> ent, ref ClothingEquipDoAfterEvent args)
|
||||
@@ -148,6 +200,12 @@ public abstract class ClothingSystem : EntitySystem
|
||||
args.Additive += ent.Comp.StripDelay;
|
||||
}
|
||||
|
||||
private void CheckEquipmentForLayerHide(EntityUid equipment, EntityUid equipee)
|
||||
{
|
||||
if (TryComp(equipment, out HideLayerClothingComponent? clothesComp) && TryComp(equipee, out HumanoidAppearanceComponent? appearanceComp))
|
||||
ToggleVisualLayers(equipee, clothesComp.Slots, appearanceComp.HideLayersOnEquip);
|
||||
}
|
||||
|
||||
#region Public API
|
||||
|
||||
public void SetEquippedPrefix(EntityUid uid, string? prefix, ClothingComponent? clothing = null)
|
||||
|
||||
@@ -16,8 +16,7 @@ public sealed class FoldableClothingSystem : EntitySystem
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<FoldableClothingComponent, FoldAttemptEvent>(OnFoldAttempt);
|
||||
SubscribeLocalEvent<FoldableClothingComponent, FoldedEvent>(OnFolded,
|
||||
after: [typeof(MaskSystem)]); // Mask system also modifies clothing / equipment RSI state prefixes.
|
||||
SubscribeLocalEvent<FoldableClothingComponent, FoldedEvent>(OnFolded);
|
||||
}
|
||||
|
||||
private void OnFoldAttempt(Entity<FoldableClothingComponent> ent, ref FoldAttemptEvent args)
|
||||
@@ -25,19 +24,10 @@ public sealed class FoldableClothingSystem : EntitySystem
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
if (!_inventorySystem.TryGetContainingSlot(ent.Owner, out var slot))
|
||||
return;
|
||||
|
||||
// Cannot fold clothing equipped to a slot if the slot becomes disallowed
|
||||
var newSlots = args.Comp.IsFolded ? ent.Comp.UnfoldedSlots : ent.Comp.FoldedSlots;
|
||||
if (newSlots != null && (newSlots.Value & slot.SlotFlags) != slot.SlotFlags)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Setting hidden layers while equipped is not currently supported.
|
||||
if (ent.Comp.FoldedHideLayers != null || ent.Comp.UnfoldedHideLayers != null)
|
||||
// allow folding while equipped if allowed slots are the same:
|
||||
// e.g. flip a hat backwards while on your head
|
||||
if (_inventorySystem.TryGetContainingSlot(ent.Owner, out var slot) &&
|
||||
!ent.Comp.FoldedSlots.Equals(ent.Comp.UnfoldedSlots))
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
@@ -58,14 +48,7 @@ public sealed class FoldableClothingSystem : EntitySystem
|
||||
if (ent.Comp.FoldedHeldPrefix != null)
|
||||
_itemSystem.SetHeldPrefix(ent.Owner, ent.Comp.FoldedHeldPrefix, false, itemComp);
|
||||
|
||||
// This is janky and likely to lead to bugs.
|
||||
// I.e., overriding this and resetting it again later will lead to bugs if someone tries to modify clothing
|
||||
// in yaml, but doesn't realise theres actually two other fields on an unrelated component that they also need
|
||||
// to modify.
|
||||
// This should instead work via an event or something that gets raised to optionally modify the currently hidden layers.
|
||||
// Or at the very least it should stash the old layers and restore them when unfolded.
|
||||
// TODO CLOTHING fix this.
|
||||
if (ent.Comp.FoldedHideLayers != null && TryComp<HideLayerClothingComponent>(ent.Owner, out var hideLayerComp))
|
||||
if (TryComp<HideLayerClothingComponent>(ent.Owner, out var hideLayerComp))
|
||||
hideLayerComp.Slots = ent.Comp.FoldedHideLayers;
|
||||
|
||||
}
|
||||
@@ -80,8 +63,7 @@ public sealed class FoldableClothingSystem : EntitySystem
|
||||
if (ent.Comp.FoldedHeldPrefix != null)
|
||||
_itemSystem.SetHeldPrefix(ent.Owner, null, false, itemComp);
|
||||
|
||||
// TODO CLOTHING fix this.
|
||||
if (ent.Comp.UnfoldedHideLayers != null && TryComp<HideLayerClothingComponent>(ent.Owner, out var hideLayerComp))
|
||||
if (TryComp<HideLayerClothingComponent>(ent.Owner, out var hideLayerComp))
|
||||
hideLayerComp.Slots = ent.Comp.UnfoldedHideLayers;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Clothing.EntitySystems;
|
||||
|
||||
public sealed class HideLayerClothingSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedHumanoidAppearanceSystem _humanoid = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<HideLayerClothingComponent, ClothingGotUnequippedEvent>(OnHideGotUnequipped);
|
||||
SubscribeLocalEvent<HideLayerClothingComponent, ClothingGotEquippedEvent>(OnHideGotEquipped);
|
||||
SubscribeLocalEvent<HideLayerClothingComponent, ItemMaskToggledEvent>(OnHideToggled);
|
||||
}
|
||||
|
||||
private void OnHideToggled(Entity<HideLayerClothingComponent> ent, ref ItemMaskToggledEvent args)
|
||||
{
|
||||
if (args.Wearer != null)
|
||||
SetLayerVisibility(ent!, args.Wearer.Value, hideLayers: true);
|
||||
}
|
||||
|
||||
private void OnHideGotEquipped(Entity<HideLayerClothingComponent> ent, ref ClothingGotEquippedEvent args)
|
||||
{
|
||||
SetLayerVisibility(ent!, args.Wearer, hideLayers: true);
|
||||
}
|
||||
|
||||
private void OnHideGotUnequipped(Entity<HideLayerClothingComponent> ent, ref ClothingGotUnequippedEvent args)
|
||||
{
|
||||
SetLayerVisibility(ent!, args.Wearer, hideLayers: false);
|
||||
}
|
||||
|
||||
private void SetLayerVisibility(
|
||||
Entity<HideLayerClothingComponent?, ClothingComponent?> clothing,
|
||||
Entity<HumanoidAppearanceComponent?> user,
|
||||
bool hideLayers)
|
||||
{
|
||||
if (_timing.ApplyingState)
|
||||
return;
|
||||
|
||||
if (!Resolve(clothing.Owner, ref clothing.Comp1, ref clothing.Comp2))
|
||||
return;
|
||||
|
||||
if (!Resolve(user.Owner, ref user.Comp, false)) // Corvax-Changes
|
||||
return;
|
||||
|
||||
hideLayers &= IsEnabled(clothing!);
|
||||
|
||||
var hideable = user.Comp.HideLayersOnEquip;
|
||||
var inSlot = clothing.Comp2.InSlotFlag ?? SlotFlags.NONE;
|
||||
|
||||
// This method should only be getting called while the clothing is equipped (though possibly currently in
|
||||
// the process of getting unequipped).
|
||||
DebugTools.AssertNotNull(clothing.Comp2.InSlot);
|
||||
DebugTools.AssertNotNull(clothing.Comp2.InSlotFlag);
|
||||
DebugTools.AssertNotEqual(inSlot, SlotFlags.NONE);
|
||||
|
||||
var dirty = false;
|
||||
|
||||
// iterate the HideLayerClothingComponent's layers map and check that
|
||||
// the clothing is (or was)equipped in a matching slot.
|
||||
foreach (var (layer, validSlots) in clothing.Comp1.Layers)
|
||||
{
|
||||
if (!hideable.Contains(layer))
|
||||
continue;
|
||||
|
||||
// Only update this layer if we are currently equipped to the relevant slot.
|
||||
if (validSlots.HasFlag(inSlot))
|
||||
_humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty);
|
||||
}
|
||||
|
||||
// Fallback for obsolete field: assume we want to hide **all** layers, as long as we are equipped to any
|
||||
// relevant clothing slot
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (clothing.Comp1.Slots is { } slots && clothing.Comp2.Slots.HasFlag(inSlot))
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
foreach (var layer in slots)
|
||||
{
|
||||
if (hideable.Contains(layer))
|
||||
_humanoid.SetLayerVisibility(user!, layer, !hideLayers, inSlot, ref dirty);
|
||||
}
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
Dirty(user!);
|
||||
}
|
||||
|
||||
private bool IsEnabled(Entity<HideLayerClothingComponent, ClothingComponent> clothing)
|
||||
{
|
||||
// TODO Generalize this
|
||||
// I.e., make this and mask component use some generic toggleable.
|
||||
|
||||
if (!clothing.Comp1.HideOnToggle)
|
||||
return true;
|
||||
|
||||
if (!TryComp(clothing, out MaskComponent? mask))
|
||||
return true;
|
||||
|
||||
return !mask.IsToggled;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Foldable;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Inventory.Events;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -14,7 +15,6 @@ public sealed class MaskSystem : EntitySystem
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ClothingSystem _clothing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -35,109 +35,53 @@ public sealed class MaskSystem : EntitySystem
|
||||
private void OnToggleMask(Entity<MaskComponent> ent, ref ToggleMaskEvent args)
|
||||
{
|
||||
var (uid, mask) = ent;
|
||||
if (mask.ToggleActionEntity == null || !mask.IsToggleable)
|
||||
if (mask.ToggleActionEntity == null || !_timing.IsFirstTimePredicted || !mask.IsEnabled)
|
||||
return;
|
||||
|
||||
// Masks are currently only toggleable via the action while equipped.
|
||||
// Its possible this might change in future?
|
||||
|
||||
// TODO Inventory / Clothing
|
||||
// Add an easier way to check if clothing is equipped to a valid slot.
|
||||
if (!TryComp(ent, out ClothingComponent? clothing)
|
||||
|| clothing.InSlotFlag is not { } slotFlag
|
||||
|| !clothing.Slots.HasFlag(slotFlag))
|
||||
{
|
||||
if (!_inventorySystem.TryGetSlotEntity(args.Performer, "mask", out var existing) || !uid.Equals(existing))
|
||||
return;
|
||||
}
|
||||
|
||||
SetToggled((uid, mask), !mask.IsToggled);
|
||||
mask.IsToggled ^= true;
|
||||
|
||||
var dir = mask.IsToggled ? "down" : "up";
|
||||
var msg = $"action-mask-pull-{dir}-popup-message";
|
||||
_popupSystem.PopupClient(Loc.GetString(msg, ("mask", uid)), args.Performer, args.Performer);
|
||||
|
||||
ToggleMaskComponents(uid, mask, args.Performer, mask.EquippedPrefix);
|
||||
}
|
||||
|
||||
// set to untoggled when unequipped, so it isn't left in a 'pulled down' state
|
||||
private void OnGotUnequipped(EntityUid uid, MaskComponent mask, GotUnequippedEvent args)
|
||||
{
|
||||
// Masks are currently always un-toggled when unequipped.
|
||||
SetToggled((uid, mask), false);
|
||||
if (!mask.IsToggled || !mask.IsEnabled)
|
||||
return;
|
||||
|
||||
mask.IsToggled = false;
|
||||
ToggleMaskComponents(uid, mask, args.Equipee, mask.EquippedPrefix, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after setting IsToggled, raises events and dirties.
|
||||
/// <summary>
|
||||
private void ToggleMaskComponents(EntityUid uid, MaskComponent mask, EntityUid wearer, string? equippedPrefix = null, bool isEquip = false)
|
||||
{
|
||||
Dirty(uid, mask);
|
||||
if (mask.ToggleActionEntity is {} action)
|
||||
_actionSystem.SetToggled(action, mask.IsToggled);
|
||||
|
||||
var maskEv = new ItemMaskToggledEvent(wearer, equippedPrefix, mask.IsToggled, isEquip);
|
||||
RaiseLocalEvent(uid, ref maskEv);
|
||||
|
||||
var wearerEv = new WearerMaskToggledEvent(mask.IsToggled);
|
||||
RaiseLocalEvent(wearer, ref wearerEv);
|
||||
}
|
||||
|
||||
private void OnFolded(Entity<MaskComponent> ent, ref FoldedEvent args)
|
||||
{
|
||||
// See FoldableClothingComponent
|
||||
if (ent.Comp.DisableOnFolded)
|
||||
ent.Comp.IsEnabled = !args.IsFolded;
|
||||
ent.Comp.IsToggled = args.IsFolded;
|
||||
|
||||
if (!ent.Comp.DisableOnFolded)
|
||||
return;
|
||||
|
||||
// While folded, we force the mask to be toggled / pulled down, so that its functionality as a mask is disabled,
|
||||
// and we also prevent it from being un-toggled. We also automatically untoggle it when it gets unfolded, so it
|
||||
// fully returns to its previous state when folded & unfolded.
|
||||
|
||||
SetToggled(ent!, args.IsFolded, force: true);
|
||||
SetToggleable(ent!, !args.IsFolded);
|
||||
}
|
||||
|
||||
public void SetToggled(Entity<MaskComponent?> mask, bool toggled, bool force = false)
|
||||
{
|
||||
if (_timing.ApplyingState)
|
||||
return;
|
||||
|
||||
if (!Resolve(mask.Owner, ref mask.Comp))
|
||||
return;
|
||||
|
||||
if (!force && !mask.Comp.IsToggleable)
|
||||
return;
|
||||
|
||||
if (mask.Comp.IsToggled == toggled)
|
||||
return;
|
||||
|
||||
mask.Comp.IsToggled = toggled;
|
||||
|
||||
if (mask.Comp.ToggleActionEntity is { } action)
|
||||
_actionSystem.SetToggled(action, mask.Comp.IsToggled);
|
||||
|
||||
// TODO Generalize toggling & clothing prefixes. See also FoldableClothingComponent
|
||||
var prefix = mask.Comp.IsToggled ? mask.Comp.EquippedPrefix : null;
|
||||
_clothing.SetEquippedPrefix(mask, prefix);
|
||||
|
||||
// TODO Inventory / Clothing
|
||||
// Add an easier way to get the entity that is wearing clothing in a valid slot.
|
||||
EntityUid? wearer = null;
|
||||
if (TryComp(mask, out ClothingComponent? clothing)
|
||||
&& clothing.InSlotFlag is {} slotFlag
|
||||
&& clothing.Slots.HasFlag(slotFlag))
|
||||
{
|
||||
wearer = Transform(mask).ParentUid;
|
||||
}
|
||||
|
||||
var maskEv = new ItemMaskToggledEvent(mask!, wearer);
|
||||
RaiseLocalEvent(mask, ref maskEv);
|
||||
|
||||
if (wearer != null)
|
||||
{
|
||||
var wearerEv = new WearerMaskToggledEvent(mask!);
|
||||
RaiseLocalEvent(wearer.Value, ref wearerEv);
|
||||
}
|
||||
|
||||
Dirty(mask);
|
||||
}
|
||||
|
||||
public void SetToggleable(Entity<MaskComponent?> mask, bool toggleable)
|
||||
{
|
||||
if (_timing.ApplyingState)
|
||||
return;
|
||||
|
||||
if (!Resolve(mask.Owner, ref mask.Comp))
|
||||
return;
|
||||
|
||||
if (mask.Comp.IsToggleable == toggleable)
|
||||
return;
|
||||
|
||||
if (mask.Comp.ToggleActionEntity is { } action)
|
||||
_actionSystem.SetEnabled(action, mask.Comp.IsToggleable);
|
||||
|
||||
mask.Comp.IsToggleable = toggleable;
|
||||
Dirty(mask);
|
||||
ToggleMaskComponents(ent.Owner, ent.Comp, ent.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
10
Content.Shared/Damage/Components/BoxingComponent.cs
Normal file
10
Content.Shared/Damage/Components/BoxingComponent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Shared.Damage.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class BoxingComponent : Component
|
||||
{
|
||||
[DataField]
|
||||
public SoundSpecifier? Sound;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ using Content.Shared.Popups;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Rejuvenate;
|
||||
using Content.Shared.Rounding;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Stunnable;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
@@ -63,6 +64,7 @@ public sealed partial class StaminaSystem : EntitySystem
|
||||
SubscribeLocalEvent<StaminaDamageOnCollideComponent, ThrowDoHitEvent>(OnThrowHit);
|
||||
|
||||
SubscribeLocalEvent<StaminaDamageOnHitComponent, MeleeHitEvent>(OnMeleeHit);
|
||||
SubscribeLocalEvent<BoxingComponent, DownedEvent>(OnFallOver);
|
||||
|
||||
Subs.CVar(_config, CCVars.PlaytestStaminaDamageModifier, value => UniversalStaminaDamageModifier = value, true);
|
||||
}
|
||||
@@ -179,6 +181,13 @@ public sealed partial class StaminaSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnFallOver(EntityUid uid, BoxingComponent component, DownedEvent args)
|
||||
{
|
||||
if (_net.IsServer)
|
||||
_audio.PlayPvs(component.Sound, uid);
|
||||
}
|
||||
|
||||
private void OnProjectileHit(EntityUid uid, StaminaDamageOnCollideComponent component, ref ProjectileHitEvent args)
|
||||
{
|
||||
OnCollide(uid, component, args.Target);
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Doors.Components;
|
||||
/// <summary>
|
||||
/// Genuine People Personality.
|
||||
/// Makes a door speak when being opened.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
[RegisterComponent]
|
||||
public sealed partial class SpeakOnDoorOpenedComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Probability with which to speak when opened.
|
||||
/// There are a lot of doors, so we don't want this to happen every single time or chat will get spammed.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Probability = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Only speak when powered?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool NeedsPower = true;
|
||||
|
||||
/// <summary>
|
||||
/// What to say.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<LocalizedDatasetPrototype> Pack = "DoorGPP";
|
||||
}
|
||||
44
Content.Shared/Flip/FlipAnimationComponent.cs
Normal file
44
Content.Shared/Flip/FlipAnimationComponent.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Flip;
|
||||
|
||||
/// <summary>
|
||||
/// Makes it possible for an entity to do a flip animation.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class FlipAnimationComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// What's the name of this animation? Make sure it's unique so it can play along side other animations.
|
||||
/// This prevents someone accidentally causing two identical effects to play on someone at the same time.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string KeyName = "FlipEmote";
|
||||
|
||||
/// <summary>
|
||||
/// How long should a complete flip take?
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public float AnimationLength = 0.5f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Declares that an entity has started to flip.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity flipping.</param>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StartFlipEvent(NetEntity entity) : EntityEventArgs
|
||||
{
|
||||
public NetEntity Entity = entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Declares that an entity has cancelled flipping.
|
||||
/// </summary>
|
||||
/// <param name="entity">The entity stopping flipping.</param>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class StopFlipEvent(NetEntity entity) : EntityEventArgs
|
||||
{
|
||||
public NetEntity Entity = entity;
|
||||
}
|
||||
3
Content.Shared/Flip/SharedFlipAnimationSystem.cs
Normal file
3
Content.Shared/Flip/SharedFlipAnimationSystem.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Content.Shared.Flip;
|
||||
|
||||
public abstract class SharedFlipAnimationSystem : EntitySystem;
|
||||
33
Content.Shared/Fluids/EntitySystems/SpillActionSystem.cs
Normal file
33
Content.Shared/Fluids/EntitySystems/SpillActionSystem.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Fluids;
|
||||
using Content.Shared.Fluids.Components;
|
||||
|
||||
namespace Content.Server.Fluids;
|
||||
|
||||
public sealed partial class SpillActionSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedPuddleSystem _puddle = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SpillableComponent, SpillActionEvent>(OnSpillAction);
|
||||
}
|
||||
|
||||
private void OnSpillAction(Entity<SpillableComponent> ent, ref SpillActionEvent args)
|
||||
{
|
||||
if (!_solutionContainer.TryGetDrainableSolution(ent.Owner, out var soln, out var solution) || solution.Volume == 0)
|
||||
return;
|
||||
|
||||
var puddleSolution = _solutionContainer.SplitSolution(soln.Value, solution.Volume);
|
||||
_puddle.TrySpillAt(ent, puddleSolution, out _);
|
||||
_popup.PopupClient(Loc.GetString("spill-action-use", ("name", ent)), ent, ent);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class SpillActionEvent : InstantActionEvent;
|
||||
@@ -103,7 +103,7 @@ public sealed class FoldableSystem : EntitySystem
|
||||
if (_container.IsEntityInContainer(uid) && !fold.CanFoldInsideContainer)
|
||||
return false;
|
||||
|
||||
var ev = new FoldAttemptEvent(fold);
|
||||
var ev = new FoldAttemptEvent();
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
return !ev.Cancelled;
|
||||
}
|
||||
@@ -157,7 +157,7 @@ public sealed class FoldableSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <param name="Cancelled"></param>
|
||||
[ByRefEvent]
|
||||
public record struct FoldAttemptEvent(FoldableComponent Comp, bool Cancelled = false);
|
||||
public record struct FoldAttemptEvent(bool Cancelled = false);
|
||||
|
||||
/// <summary>
|
||||
/// Event raised on an entity after it has been folded.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Shared.Corvax.TTS;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -77,12 +76,11 @@ public sealed partial class HumanoidAppearanceComponent : Component
|
||||
public Color SkinColor { get; set; } = Color.FromHex("#C0967F");
|
||||
|
||||
/// <summary>
|
||||
/// A map of the visual layers currently hidden to the equipment
|
||||
/// slots that are currently hiding them. This will affect the base
|
||||
/// sprite on this humanoid layer, and any markings that sit above it.
|
||||
/// Visual layers currently hidden. This will affect the base sprite
|
||||
/// on this humanoid layer, and any markings that sit above it.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public Dictionary<HumanoidVisualLayers, SlotFlags> HiddenLayers = new();
|
||||
public HashSet<HumanoidVisualLayers> HiddenLayers = new();
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public Sex Sex = Sex.Male;
|
||||
|
||||
@@ -10,7 +10,6 @@ using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Speech.Synthesis.Components; // Corvax-Wega-Barks
|
||||
using Content.Shared.Corvax.TTS;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared;
|
||||
@@ -136,22 +135,22 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Toggles a humanoid's sprite layer visibility.
|
||||
/// </summary>
|
||||
/// <param name="ent">Humanoid entity</param>
|
||||
/// <param name="uid">Humanoid mob's UID</param>
|
||||
/// <param name="layer">Layer to toggle visibility for</param>
|
||||
/// <param name="visible">Whether to hide or show the layer. If more than once piece of clothing is hiding the layer, it may remain hidden.</param>
|
||||
/// <param name="source">Equipment slot that has the clothing that is (or was) hiding the layer. If not specified, the change is "permanent" (i.e., see <see cref="HumanoidAppearanceComponent.PermanentlyHidden"/>)</param>
|
||||
public void SetLayerVisibility(Entity<HumanoidAppearanceComponent?> ent,
|
||||
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||
public void SetLayerVisibility(EntityUid uid,
|
||||
HumanoidVisualLayers layer,
|
||||
bool visible,
|
||||
SlotFlags? source = null)
|
||||
bool permanent = false,
|
||||
HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
||||
if (!Resolve(uid, ref humanoid, false))
|
||||
return;
|
||||
|
||||
var dirty = false;
|
||||
SetLayerVisibility(ent!, layer, visible, source, ref dirty);
|
||||
SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
|
||||
if (dirty)
|
||||
Dirty(ent);
|
||||
Dirty(uid, humanoid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -187,75 +186,49 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Sets the visibility for multiple layers at once on a humanoid's sprite.
|
||||
/// </summary>
|
||||
/// <param name="ent">Humanoid entity</param>
|
||||
/// <param name="uid">Humanoid mob's UID</param>
|
||||
/// <param name="layers">An enumerable of all sprite layers that are going to have their visibility set</param>
|
||||
/// <param name="visible">The visibility state of the layers given</param>
|
||||
public void SetLayersVisibility(Entity<HumanoidAppearanceComponent?> ent,
|
||||
IEnumerable<HumanoidVisualLayers> layers,
|
||||
bool visible)
|
||||
/// <param name="permanent">If this is a permanent change, or temporary. Permanent layers are stored in their own hash set.</param>
|
||||
/// <param name="humanoid">Humanoid component of the entity</param>
|
||||
public void SetLayersVisibility(EntityUid uid, IEnumerable<HumanoidVisualLayers> layers, bool visible, bool permanent = false,
|
||||
HumanoidAppearanceComponent? humanoid = null)
|
||||
{
|
||||
if (!Resolve(ent.Owner, ref ent.Comp, false))
|
||||
if (!Resolve(uid, ref humanoid))
|
||||
return;
|
||||
|
||||
var dirty = false;
|
||||
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
SetLayerVisibility(ent!, layer, visible, null, ref dirty);
|
||||
SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
Dirty(ent);
|
||||
Dirty(uid, humanoid);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="SetLayerVisibility(Entity{HumanoidAppearanceComponent?},HumanoidVisualLayers,bool,Nullable{SlotFlags})"/>
|
||||
public virtual void SetLayerVisibility(
|
||||
Entity<HumanoidAppearanceComponent> ent,
|
||||
protected virtual void SetLayerVisibility(
|
||||
EntityUid uid,
|
||||
HumanoidAppearanceComponent humanoid,
|
||||
HumanoidVisualLayers layer,
|
||||
bool visible,
|
||||
SlotFlags? source,
|
||||
bool permanent,
|
||||
ref bool dirty)
|
||||
{
|
||||
#if DEBUG
|
||||
if (source is {} s)
|
||||
{
|
||||
DebugTools.AssertNotEqual(s, SlotFlags.NONE);
|
||||
// Check that only a single bit in the bitflag is set
|
||||
var powerOfTwo = BitOperations.RoundUpToPowerOf2((uint)s);
|
||||
DebugTools.AssertEqual((uint)s, powerOfTwo);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (visible)
|
||||
{
|
||||
if (source is not {} slot)
|
||||
{
|
||||
dirty |= ent.Comp.PermanentlyHidden.Remove(layer);
|
||||
}
|
||||
else if (ent.Comp.HiddenLayers.TryGetValue(layer, out var oldSlots))
|
||||
{
|
||||
// This layer might be getting hidden by more than one piece of equipped clothing.
|
||||
// remove slot flag from the set of slots hiding this layer, then check if there are any left.
|
||||
ent.Comp.HiddenLayers[layer] = ~slot & oldSlots;
|
||||
if (ent.Comp.HiddenLayers[layer] == SlotFlags.NONE)
|
||||
ent.Comp.HiddenLayers.Remove(layer);
|
||||
if (permanent)
|
||||
dirty |= humanoid.PermanentlyHidden.Remove(layer);
|
||||
|
||||
dirty |= (oldSlots & slot) != 0;
|
||||
}
|
||||
dirty |= humanoid.HiddenLayers.Remove(layer);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (source is not { } slot)
|
||||
{
|
||||
dirty |= ent.Comp.PermanentlyHidden.Add(layer);
|
||||
}
|
||||
else
|
||||
{
|
||||
var oldSlots = ent.Comp.HiddenLayers.GetValueOrDefault(layer);
|
||||
ent.Comp.HiddenLayers[layer] = slot | oldSlots;
|
||||
dirty |= (oldSlots & slot) != slot;
|
||||
}
|
||||
if (permanent)
|
||||
dirty |= humanoid.PermanentlyHidden.Add(layer);
|
||||
|
||||
dirty |= humanoid.HiddenLayers.Add(layer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public abstract class SharedIdentitySystem : EntitySystem
|
||||
|
||||
private void OnMaskToggled(Entity<IdentityBlockerComponent> ent, ref ItemMaskToggledEvent args)
|
||||
{
|
||||
ent.Comp.Enabled = !args.Mask.Comp.IsToggled;
|
||||
ent.Comp.Enabled = !args.IsToggled;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
|
||||
@@ -149,8 +149,9 @@ public sealed class PaperSystem : EntitySystem
|
||||
}
|
||||
|
||||
// If a stamp, attempt to stamp paper
|
||||
if (TryComp<StampComponent>(args.Used, out var stampComp) && TryStamp(entity, GetStampInfo(stampComp), stampComp.StampState))
|
||||
if (TryComp<StampComponent>(args.Used, out var stampComp))
|
||||
{
|
||||
SetContent(entity, Loc.GetString("paper-component-action-stamp-paper-paperwork-is-bad"));
|
||||
// successfully stamped, play popup
|
||||
var stampPaperOtherMessage = Loc.GetString("paper-component-action-stamp-paper-other",
|
||||
("user", args.User),
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Actions.Events;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Serialization;
|
||||
@@ -27,6 +28,8 @@ public abstract partial class SharedStationAiSystem
|
||||
SubscribeLocalEvent<StationAiHeldComponent, InteractionAttemptEvent>(OnHeldInteraction);
|
||||
SubscribeLocalEvent<StationAiHeldComponent, AttemptRelayActionComponentChangeEvent>(OnHeldRelay);
|
||||
SubscribeLocalEvent<StationAiHeldComponent, JumpToCoreEvent>(OnCoreJump);
|
||||
SubscribeLocalEvent<StationAiHeldComponent, VisitCoreEvent>(OnCoreVisit);
|
||||
SubscribeLocalEvent<VisitingMindComponent, UnVisitCoreEvent>(OnCoreUnVisit);
|
||||
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(OnTryGetIdentityShortInfo);
|
||||
}
|
||||
|
||||
@@ -50,7 +53,46 @@ public abstract partial class SharedStationAiSystem
|
||||
if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null)
|
||||
return;
|
||||
|
||||
_xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner) ;
|
||||
_xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner);
|
||||
}
|
||||
|
||||
private void OnCoreVisit(Entity<StationAiHeldComponent> ent, ref VisitCoreEvent args)
|
||||
{
|
||||
if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null)
|
||||
return;
|
||||
|
||||
if (!_mind.TryGetMind(ent.Owner, out var mindId, out var mind))
|
||||
return;
|
||||
|
||||
// move the player's mind to the core
|
||||
_mind.Visit(mindId, core, mind);
|
||||
_xforms.Unanchor(core);
|
||||
|
||||
if (!TryComp<AppearanceComponent>(core, out var app))
|
||||
return;
|
||||
|
||||
_appearance.SetData(core, StationAiVisualState.Key, StationAiState.Standing, app);
|
||||
}
|
||||
|
||||
private void OnCoreUnVisit(Entity<VisitingMindComponent> ent, ref UnVisitCoreEvent args)
|
||||
{
|
||||
if (!Transform(ent.Owner).Anchored && !_xforms.AnchorEntity(ent.Owner))
|
||||
return;
|
||||
|
||||
// move the player's mind back to the camera
|
||||
if (ent.Comp.MindId != null)
|
||||
_mind.UnVisit(ent.Comp.MindId.Value);
|
||||
|
||||
if (!TryComp(ent.Owner, out StationAiCoreComponent? coreComp) || coreComp.RemoteEntity == null)
|
||||
return;
|
||||
|
||||
// move the camera back to the core
|
||||
_xforms.DropNextTo(coreComp.RemoteEntity.Value, ent.Owner);
|
||||
|
||||
if (!TryComp<AppearanceComponent>(ent.Owner, out var app))
|
||||
return;
|
||||
|
||||
_appearance.SetData(ent.Owner, StationAiVisualState.Key, StationAiState.Occupied, app);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -164,7 +206,7 @@ public abstract partial class SharedStationAiSystem
|
||||
var verb = new AlternativeVerb
|
||||
{
|
||||
Text = isOpen ? Loc.GetString("ai-close") : Loc.GetString("ai-open"),
|
||||
Act = () =>
|
||||
Act = () =>
|
||||
{
|
||||
// no need to show menu if device is not powered.
|
||||
if (!PowerReceiver.IsPowered(ent.Owner))
|
||||
|
||||
@@ -460,6 +460,9 @@ public abstract partial class SharedStationAiSystem : EntitySystem
|
||||
|
||||
private void OnAiRemove(Entity<StationAiCoreComponent> ent, ref EntRemovedFromContainerMessage args)
|
||||
{
|
||||
if (args.Container.ID != StationAiCoreComponent.Container)
|
||||
return;
|
||||
|
||||
if (_timing.ApplyingState)
|
||||
return;
|
||||
|
||||
@@ -538,6 +541,16 @@ public sealed partial class JumpToCoreEvent : InstantActionEvent
|
||||
|
||||
}
|
||||
|
||||
public sealed partial class VisitCoreEvent : InstantActionEvent
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public sealed partial class UnVisitCoreEvent : InstantActionEvent
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent;
|
||||
|
||||
@@ -548,10 +561,21 @@ public enum StationAiVisualState : byte
|
||||
Key,
|
||||
}
|
||||
|
||||
public enum StationAiVisualLayers : byte
|
||||
{
|
||||
Core,
|
||||
Screen,
|
||||
CoreStanding,
|
||||
ScreenStanding,
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum StationAiState : byte
|
||||
{
|
||||
Empty,
|
||||
Occupied,
|
||||
Dead,
|
||||
Up,
|
||||
Down,
|
||||
Standing,
|
||||
}
|
||||
|
||||
@@ -39,4 +39,16 @@ public sealed partial class StationAiCoreComponent : Component
|
||||
public EntProtoId? PhysicalEntityProto = "StationAiHoloLocal";
|
||||
|
||||
public const string Container = "station_ai_mind_slot";
|
||||
|
||||
/// <summary>
|
||||
/// The key used to index the leggy animation.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public const string AnimationKey = "leggy_animation";
|
||||
|
||||
/// <summary>
|
||||
/// The current visual state of the core.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public StationAiState CurrentState = StationAiState.Empty;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ using Content.Shared.Placeable;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Timing;
|
||||
using Content.Shared.Storage.Events;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -66,6 +67,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
[Dependency] private readonly SharedStackSystem _stack = default!;
|
||||
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
|
||||
[Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
[Dependency] protected readonly UseDelaySystem UseDelay = default!;
|
||||
|
||||
private EntityQuery<ItemComponent> _itemQuery;
|
||||
@@ -276,7 +278,8 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
if (!UI.IsUiOpen(uid, args.UiKey))
|
||||
{
|
||||
UpdateAppearance((uid, storageComp, null));
|
||||
Audio.PlayPredicted(storageComp.StorageCloseSound, uid, args.Actor);
|
||||
if (!_tag.HasTag(args.Actor, storageComp.SilentStorageUserTag))
|
||||
Audio.PlayPredicted(storageComp.StorageCloseSound, uid, args.Actor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,7 +360,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
if (!UI.TryOpenUi(uid, StorageComponent.StorageUiKey.Key, entity))
|
||||
return;
|
||||
|
||||
if (!silent)
|
||||
if (!silent && !_tag.HasTag(entity, storageComp.SilentStorageUserTag))
|
||||
{
|
||||
Audio.PlayPredicted(storageComp.StorageOpenSound, uid, entity);
|
||||
|
||||
@@ -602,7 +605,8 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
// If we picked up at least one thing, play a sound and do a cool animation!
|
||||
if (successfullyInserted.Count > 0)
|
||||
{
|
||||
Audio.PlayPredicted(component.StorageInsertSound, uid, args.User, _audioParams);
|
||||
if (!_tag.HasTag(args.User, component.SilentStorageUserTag))
|
||||
Audio.PlayPredicted(component.StorageInsertSound, uid, args.User, _audioParams);
|
||||
EntityManager.RaiseSharedEvent(new AnimateInsertingEntitiesEvent(
|
||||
GetNetEntity(uid),
|
||||
GetNetEntityList(successfullyInserted),
|
||||
@@ -645,7 +649,8 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
$"{ToPrettyString(player):player} is attempting to take {ToPrettyString(item):item} out of {ToPrettyString(storage):storage}");
|
||||
|
||||
if (_sharedHandsSystem.TryPickupAnyHand(player, item, handsComp: player.Comp)
|
||||
&& storage.Comp.StorageRemoveSound != null)
|
||||
&& storage.Comp.StorageRemoveSound != null
|
||||
&& !_tag.HasTag(player, storage.Comp.SilentStorageUserTag))
|
||||
{
|
||||
Audio.PlayPredicted(storage.Comp.StorageRemoveSound, storage, player, _audioParams);
|
||||
}
|
||||
@@ -903,8 +908,10 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
{
|
||||
Insert(target, entity, out _, user: user, targetComp, playSound: false);
|
||||
}
|
||||
|
||||
Audio.PlayPredicted(sourceComp.StorageInsertSound, target, user, _audioParams);
|
||||
if (user != null
|
||||
&& (!_tag.HasTag(user.Value, sourceComp.SilentStorageUserTag)
|
||||
|| !_tag.HasTag(user.Value, targetComp.SilentStorageUserTag)))
|
||||
Audio.PlayPredicted(sourceComp.StorageInsertSound, target, user, _audioParams);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -1077,12 +1084,17 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
* For now we just treat items as always being the same size regardless of stack count.
|
||||
*/
|
||||
|
||||
// Check if the sound is expected to play.
|
||||
// If there is an user, the sound will not play if they have the SilentStorageUserTag
|
||||
// If there is no user, only playSound is checked.
|
||||
var canPlaySound = playSound && (user == null || !_tag.HasTag(user.Value, storageComp.SilentStorageUserTag));
|
||||
|
||||
if (!stackAutomatically || !_stackQuery.TryGetComponent(insertEnt, out var insertStack))
|
||||
{
|
||||
if (!ContainerSystem.Insert(insertEnt, storageComp.Container))
|
||||
return false;
|
||||
|
||||
if (playSound)
|
||||
if (canPlaySound)
|
||||
Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams);
|
||||
|
||||
return true;
|
||||
@@ -1112,7 +1124,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
if (playSound)
|
||||
if (canPlaySound)
|
||||
Audio.PlayPredicted(storageComp.StorageInsertSound, uid, user, _audioParams);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Containers;
|
||||
@@ -141,6 +142,12 @@ namespace Content.Shared.Storage
|
||||
[DataField]
|
||||
public bool HideStackVisualsWhenClosed = true;
|
||||
|
||||
/// <summary>
|
||||
/// Entities with this tag won't trigger storage sound.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<TagPrototype> SilentStorageUserTag = "SilentStorageUser";
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum StorageUiKey : byte
|
||||
{
|
||||
|
||||
@@ -21,11 +21,17 @@ public sealed partial class HandTeleporterComponent : Component
|
||||
public EntityUid? SecondPortal = null;
|
||||
|
||||
/// <summary>
|
||||
/// Portals can't be placed on different grids?
|
||||
/// Should the portals be able to be placed across grids?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool AllowPortalsOnDifferentGrids;
|
||||
|
||||
/// <summary>
|
||||
/// Should the portals work across maps?
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool AllowPortalsOnDifferentMaps;
|
||||
|
||||
[DataField("firstPortalPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string FirstPortalPrototype = "PortalRed";
|
||||
|
||||
|
||||
@@ -209,106 +209,22 @@ namespace Content.Shared.Wires
|
||||
{
|
||||
public static string Name(this WireColor color)
|
||||
{
|
||||
return Loc.GetString(color switch
|
||||
{
|
||||
WireColor.Red => "Red",
|
||||
WireColor.Blue => "Blue",
|
||||
WireColor.Green => "Green",
|
||||
WireColor.Orange => "Orange",
|
||||
WireColor.Brown => "Brown",
|
||||
WireColor.Gold => "Gold",
|
||||
WireColor.Gray => "Gray",
|
||||
WireColor.Cyan => "Cyan",
|
||||
WireColor.Navy => "Navy",
|
||||
WireColor.Purple => "Purple",
|
||||
WireColor.Pink => "Pink",
|
||||
WireColor.Fuchsia => "Fuchsia",
|
||||
_ => throw new InvalidOperationException()
|
||||
});
|
||||
return Loc.GetString("Red");
|
||||
}
|
||||
|
||||
public static Color ColorValue(this WireColor color)
|
||||
{
|
||||
return color switch
|
||||
{
|
||||
WireColor.Red => Color.Red,
|
||||
WireColor.Blue => Color.Blue,
|
||||
WireColor.Green => Color.LimeGreen,
|
||||
WireColor.Orange => Color.Orange,
|
||||
WireColor.Brown => Color.Brown,
|
||||
WireColor.Gold => Color.Gold,
|
||||
WireColor.Gray => Color.Gray,
|
||||
WireColor.Cyan => Color.Cyan,
|
||||
WireColor.Navy => Color.Navy,
|
||||
WireColor.Purple => Color.Purple,
|
||||
WireColor.Pink => Color.Pink,
|
||||
WireColor.Fuchsia => Color.Fuchsia,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
return Color.Red;
|
||||
}
|
||||
|
||||
public static string Name(this WireLetter letter)
|
||||
{
|
||||
return Loc.GetString(letter switch
|
||||
{
|
||||
WireLetter.α => "Alpha",
|
||||
WireLetter.β => "Beta",
|
||||
WireLetter.γ => "Gamma",
|
||||
WireLetter.δ => "Delta",
|
||||
WireLetter.ε => "Epsilon",
|
||||
WireLetter.ζ => "Zeta",
|
||||
WireLetter.η => "Eta",
|
||||
WireLetter.θ => "Theta",
|
||||
WireLetter.ι => "Iota",
|
||||
WireLetter.κ => "Kappa",
|
||||
WireLetter.λ => "Lambda",
|
||||
WireLetter.μ => "Mu",
|
||||
WireLetter.ν => "Nu",
|
||||
WireLetter.ξ => "Xi",
|
||||
WireLetter.ο => "Omicron",
|
||||
WireLetter.π => "Pi",
|
||||
WireLetter.ρ => "Rho",
|
||||
WireLetter.σ => "Sigma",
|
||||
WireLetter.τ => "Tau",
|
||||
WireLetter.υ => "Upsilon",
|
||||
WireLetter.φ => "Phi",
|
||||
WireLetter.χ => "Chi",
|
||||
WireLetter.ψ => "Psi",
|
||||
WireLetter.ω => "Omega",
|
||||
_ => throw new InvalidOperationException()
|
||||
});
|
||||
return Loc.GetString("Sigma");
|
||||
}
|
||||
|
||||
public static char Letter(this WireLetter letter)
|
||||
{
|
||||
return letter switch
|
||||
{
|
||||
WireLetter.α => 'α',
|
||||
WireLetter.β => 'β',
|
||||
WireLetter.γ => 'γ',
|
||||
WireLetter.δ => 'δ',
|
||||
WireLetter.ε => 'ε',
|
||||
WireLetter.ζ => 'ζ',
|
||||
WireLetter.η => 'η',
|
||||
WireLetter.θ => 'θ',
|
||||
WireLetter.ι => 'ι',
|
||||
WireLetter.κ => 'κ',
|
||||
WireLetter.λ => 'λ',
|
||||
WireLetter.μ => 'μ',
|
||||
WireLetter.ν => 'ν',
|
||||
WireLetter.ξ => 'ξ',
|
||||
WireLetter.ο => 'ο',
|
||||
WireLetter.π => 'π',
|
||||
WireLetter.ρ => 'ρ',
|
||||
WireLetter.σ => 'σ',
|
||||
WireLetter.τ => 'τ',
|
||||
WireLetter.υ => 'υ',
|
||||
WireLetter.φ => 'φ',
|
||||
WireLetter.χ => 'χ',
|
||||
WireLetter.ψ => 'ψ',
|
||||
WireLetter.ω => 'ω',
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
return 'σ';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- files: ["nukeops_start.ogg, traitor_start.ogg"]
|
||||
- files: ["nukeops_start.ogg, traitor_start.ogg, traitor_wawa.ogg"]
|
||||
license: "CC-BY-SA-3.0"
|
||||
copyright: "Taken from TG station"
|
||||
copyright: "Taken from TG station, traitor_wawa.ogg modified by FairlySadPanda(Github/Discord)"
|
||||
source: "https://github.com/tgstation/tgstation/commit/827967c9650c23af64280ad1491405fed8f644c5#diff-6cc910b7cad9ac4333c8d0885fc844746066120b465d8d4ba8f7019169316574"
|
||||
- files: ["pirate_start.ogg"]
|
||||
license: "CC-BY-NC-SA-3.0"
|
||||
|
||||
BIN
Resources/Audio/Ambience/Antag/traitor_wawa.ogg
Normal file
BIN
Resources/Audio/Ambience/Antag/traitor_wawa.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/slugclappa.ogg
Normal file
BIN
Resources/Audio/Animals/slugclappa.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/wawa_achoo.ogg
Normal file
BIN
Resources/Audio/Animals/wawa_achoo.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/wawa_chatter.ogg
Normal file
BIN
Resources/Audio/Animals/wawa_chatter.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/wawa_chillin.ogg
Normal file
BIN
Resources/Audio/Animals/wawa_chillin.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/wawa_depression.ogg
Normal file
BIN
Resources/Audio/Animals/wawa_depression.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/wawa_exclaim.ogg
Normal file
BIN
Resources/Audio/Animals/wawa_exclaim.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/wawa_mock.ogg
Normal file
BIN
Resources/Audio/Animals/wawa_mock.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/wawa_protest.ogg
Normal file
BIN
Resources/Audio/Animals/wawa_protest.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/wawa_question.ogg
Normal file
BIN
Resources/Audio/Animals/wawa_question.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Animals/wawa_statement.ogg
Normal file
BIN
Resources/Audio/Animals/wawa_statement.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Announcements/Intern/alert1.ogg
Normal file
BIN
Resources/Audio/Announcements/Intern/alert1.ogg
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user