Merge remote-tracking branch 'upstream/april-fools-2025-upstream' into April

This commit is contained in:
Zekins
2025-04-01 00:48:46 +03:00
1052 changed files with 63400 additions and 2967 deletions

View File

@@ -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:

View File

@@ -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)

View File

@@ -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))
{

View 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);
}
}

View File

@@ -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);
}
}
}

View 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;
}
}
}

View File

@@ -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

View File

@@ -99,6 +99,7 @@ namespace Content.IntegrationTests.Tests
"Amber",
"Loop",
"Plasma",
"Claustrophobia",
"Elkridge",
"Convex",
"Relic"

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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
}

View File

@@ -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(

View File

@@ -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);
}
}
}

View File

@@ -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))

View File

@@ -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;

View File

@@ -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);

View File

@@ -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));

View 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);
}
}

View File

@@ -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);
}

View File

@@ -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)

View 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)));
}
}

View File

@@ -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);

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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");
}
}
}

View File

@@ -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)
{

View File

@@ -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;

View File

@@ -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);
}
}
}

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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
{
}

View 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;
}

View 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
{
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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");
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Speech.Components;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Speech.EntitySystems
{

View File

@@ -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);

View 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;
}
}

View 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
};
}
}

View File

@@ -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();

View File

@@ -70,5 +70,10 @@ namespace Content.Server.Speech.EntitySystems
return finalMessage.ToString();
}
public string Accentuate(string message)
{
return Accentuate(message, new StutteringAccentComponent());
}
}
}

View File

@@ -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()
{

View File

@@ -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;
}

View File

@@ -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");
}

View File

@@ -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
{
}

View 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);
}
}
}

View 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);
}
}
}

View File

@@ -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);
}

View 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);
}
}

View File

@@ -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);

View File

@@ -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"));
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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";

View File

@@ -0,0 +1,6 @@
using Content.Shared.Alert;
using Robust.Shared.Serialization;
namespace Content.Shared.Body.Events;
public sealed partial class ToggleBreathingAlertEvent : BaseAlertEvent;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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();
}

View File

@@ -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)

View File

@@ -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;

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View 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;
}

View File

@@ -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);

View File

@@ -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";
}

View 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;
}

View File

@@ -0,0 +1,3 @@
namespace Content.Shared.Flip;
public abstract class SharedFlipAnimationSystem : EntitySystem;

View 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;

View File

@@ -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.

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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),

View File

@@ -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))

View File

@@ -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,
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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";

View File

@@ -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 'σ';
}
}
}

View File

@@ -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"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More