Merge branch 'master' into ui/fryer

This commit is contained in:
faintspeaker 2026-06-23 10:53:08 -04:00
commit 5d49458da0
790 changed files with 145481 additions and 71033 deletions

View File

@ -269,7 +269,7 @@ Sentencing modifiers are to be applied by the sentencing officer, judge, or arbi
| style="border: 1px solid black;" | [[File:SL_BreakingAndEntering.png]]
! style="border: 1px solid black;" | {{anchor|Breaking and Entering}}Breaking and Entering
| style="border: 1px solid black;" | 5 minutes
| style="border: 1px solid black;" | To break and enter into a high security area where one is not authorised nor invited, with intent to commit a crime within.
| style="border: 1px solid black;" | To break and enter into an area where one is not authorised nor invited.
|-
| style="border: 1px solid black;" | 203
| style="border: 1px solid black;" | [[File:SL_Rioting.png]]

View File

@ -73,28 +73,88 @@ If you are adding a lot of C# code, then take advantage of partial classes. Put
Otherwise, **add comments on or around any changed lines.**
A comment on a new imported namespace:
### Single-Line Changes
Format should look like this.
```cs
using Content.Server.Psionics.Glimmer; // DeltaV
/* Importing Namespaces - Include optional comment if its not obvious what its being used for. */
using Content.Server._DV.Psionics.Glimmer; // DeltaV
using Content.Shared.Damage.Systems; // DeltaV - Addition of HandHeldArmor
/* Changing an upstream line - Same line as the change */
if (!TryComp<EyeComponent>(ent, out var eye) || _disabled) // DeltaV - check if disabled
/* Adding - Either same line or above the line. */
EnsureComp<PotentialPsionicComponent>(entity); // Deltav - Psionics
/* "Deleting" - Don't actually delete, just comment out and say why. This only applies to upstream code. */
// args.StatusIcons.Add(_prototype.Index(component.Icon)); // DeltaV - commented out. status icon now added above
```
A pair of comments enclosing a block of added code:
> * Its pretty obvious in the example above that importing `Content.Server._DV.Psionics.Glimmer` means we'll be interacting with glimmer so putting `// DeltaV - Add Glimmer` is needlessly redundant.
> * It's not as obvious what the `Content.Shared.Damage.Systems` namespace is used for, since its so broad, so adding a comment what feature is using it helps.
> * Actual code changes should almost always include the comment after ``// DeltaV`.
### Multi-Line Changes
Depending on how much you are editing, putting a comment on EACH line may be excessive, so if you have a larger block of code you are changing, denote it like so:
```cs
private EntityUid Slice(...)
// BEGIN DeltaV - Remove innate radio and radios from pockets
for (var i = 1; i <= 4; i++) // Arachnids have 4 pockets
{
...
_transform.SetLocalRotation(sliceUid, 0);
// DeltaV - start of deep frier stuff
var slicedEv = new FoodSlicedEvent(user, uid, sliceUid);
RaiseLocalEvent(uid, ref slicedEv);
// DeltaV - end of deep frier stuff
...
if (_inventory.TryGetSlotEntity(target, $"pocket{i}", out var headset) && HasComp<HeadsetComponent>(headset))
_inventory.TryUnequip(target, $"pocket{i}", true, true);
}
RemComp<ActiveRadioComponent>(target); // If the zombie has an innate radio, get rid of it.
// END DeltaV
```
> * Denoting these with a BEGIN and END clearly shows they are block of code without having to read the entire comment. This makes it easier to tell when you're dealing with single-line comments versus a block with merging in conflicts.
> * Case and order of the first two words is less of a concern. `// DeltaV Begin` or `// Begin DeltaV` will work fine too.
> * Try to make your blocks as small as possible, but use your discretion.
> * If you deleting multiple lines, use line comments (``//``) if its a few lines but if its a larger block (like commenting out an entire function), it is preferable to use block comments (`/* */`).
#### Soft Exceptions to the Multi-Line "Rules"
Some multi-line changes can use a single-line comment in certain scenarios. But if you are UNSURE, just use `// BEGIN DeltaV` and `// END DeltaV` comments like the previous section does and it'll be fine.
I'll give some examples.
```cs
/* This change comments out 3 lines but only needs a single line comment because commenting out the if statement implies that its logic will be commented out too. */
// if (obj.WasModified<TraitPrototype>()) // DeltaV - Refreshed in TraitsTab
// {
// _profileEditor.RefreshTraits();
// }
/* Same principle here. This adds two lines but the if statement implies the next line so commenting both lines isn't really needed. */
if (_flight.IsFlying(entity.Owner)) // DeltaV - Harpy Flight
return true;
```
### New Methods or Component Variables
Sometimes, you'll need to implement a whole new method or component variable and instead of wrapping it in `// BEGIN DeltaV` and `// END DeltaV`, you can just denote that it's a DeltaV function in the summary block before the function. This denotes the WHOLE function as a DeltaV addition.
```cs
/* New Method Example */
/// <summary>
/// DeltaV - Handle revealing ninja if cloaked when attacked by a hitscan attack.
/// </summary>
private void OnNinjaAttacked(Entity<SpaceNinjaComponent> ent, ref DamageChangedEvent args)
{
...
}
/* New Component Variable Example */
/// <summary>
/// DeltaV - If disabled the action will not disable when no charges remain. Use if you want to handle no charges differently.
/// </summary>
[DataField]
public bool DisableWhenEmpty = true;
```
In short:
* Use `// BEGIN DeltaV` and `// END Delta` to denote a *block* of changes.
* Keep blocks as small as possible.
* Use `// DeltaV` on or before the line if its not a block of changes.
* Use exceptions when they make sense.
### Changing Upstream Localization Fluent .ftl files
**Move all changed locale strings to a new DeltaV file** - use a `.ftl` file in the `_DV` folder. Comment out the old strings in the upstream file, and explain that they were moved.
@ -104,10 +164,10 @@ Example:
Commented out old string in `Resources\Locale\en-US\xenoarchaeology\artifact-analyzer.ftl`
```
# DeltaV - moved to _DV file
#analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
# [true] {$info}
# *[false] Unlock nodes to gain info
#}[/color][/font]
# analysis-console-info-effect-value = [font="Monospace" size=11][color=gray]{ $state ->
# [true] {$info}
# *[false] Unlock nodes to gain info
# }[/color][/font]
```
The new version of the string in `Resources\Locale\en-US\_DV\xenoarchaeology\artifact-analyzer.ftl`

View File

@ -8,12 +8,14 @@ using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Content.Shared.SSDIndicator; // DeltaV - SSD time indicator
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; // DeltaV - SSD time indicator
namespace Content.Client.Administration;
@ -26,6 +28,7 @@ internal sealed class AdminNameOverlay : Overlay
private readonly IUserInterfaceManager _userInterfaceManager;
private readonly SharedRoleSystem _roles;
private readonly IPrototypeManager _prototypeManager;
private readonly IGameTiming _timing; // DeltaV - Add timing
private readonly Font _font;
private readonly Font _fontBold;
private AdminOverlayAntagFormat _overlayFormat;
@ -53,7 +56,8 @@ internal sealed class AdminNameOverlay : Overlay
IUserInterfaceManager userInterfaceManager,
IConfigurationManager config,
SharedRoleSystem roles,
IPrototypeManager prototypeManager)
IPrototypeManager prototypeManager,
IGameTiming timing) // DeltaV - Add timing
{
_system = system;
_entityManager = entityManager;
@ -62,6 +66,7 @@ internal sealed class AdminNameOverlay : Overlay
_userInterfaceManager = userInterfaceManager;
_roles = roles;
_prototypeManager = prototypeManager;
_timing = timing; // DeltaV - Add timing
ZIndex = 200;
// Setting these to a specific ttf would break the antag symbols
_font = resourceCache.NotoStack();
@ -231,6 +236,18 @@ internal sealed class AdminNameOverlay : Overlay
currentOffset += lineoffset;
}
// DeltaV - SSD Time START
if (_entityManager.TryGetComponent<SSDIndicatorComponent>(entity, out var ssdIndicator)
&& ssdIndicator.SsdSince is {} ssdSince)
{
color = Color.MediumPurple;
color.A = alpha;
var ssdText = Loc.GetString("admin-overlay-ssd-time", ("time", (_timing.CurTime - ssdSince).ToString("%hh':'mm':'ss")));
args.ScreenHandle.DrawString(_font, screenCoordinates + currentOffset, ssdText, uiScale, color);
currentOffset += lineoffset;
}
// DeltaV END
// Determine antag symbol
string? symbol;
switch (_overlaySymbolStyle)

View File

@ -5,6 +5,7 @@ using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing; // DeltaV - SSD time indicator
namespace Content.Client.Administration.Systems
{
@ -19,6 +20,7 @@ namespace Content.Client.Administration.Systems
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IGameTiming _timing = default!; // DeltaV - added for SSD time indicator
private AdminNameOverlay _adminNameOverlay = default!;
@ -36,7 +38,8 @@ namespace Content.Client.Administration.Systems
_userInterfaceManager,
_configurationManager,
_roles,
_proto);
_proto,
_timing); // DeltaV - Add timing
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
}

View File

@ -191,6 +191,9 @@ public sealed class DoorSystem : SharedDoorSystem
case DoorState.Denying:
// ES START
// AnimationKey -> DenyKey
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.DenyKey))
return;
_animationSystem.Play(entity, (Animation)entity.Comp.DenyingAnimation, DoorComponent.DenyKey);
// ES END
@ -198,6 +201,9 @@ public sealed class DoorSystem : SharedDoorSystem
case DoorState.Emagging:
// ES START
// AnimationKey -> DenyKey
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.EmagKey))
return;
if (_sprite.TryGetLayer(entity.Owner, DoorVisualLayers.BaseEmagging, out var _, false))
_animationSystem.Play(entity, (Animation)entity.Comp.EmaggingAnimation, DoorComponent.EmagKey);
// ES END

View File

@ -20,6 +20,7 @@
<CheckBox Name="ShowLoocAboveHeadCheckBox" Text="{Loc 'ui-options-show-looc-on-head'}" />
<CheckBox Name="FancySpeechBubblesCheckBox" Text="{Loc 'ui-options-fancy-speech'}" />
<CheckBox Name="FancyNameBackgroundsCheckBox" Text="{Loc 'ui-options-fancy-name-background'}" />
<CheckBox Name="ChatFollowButton" Text="{Loc 'ui-options-chat-follow-button'}" />
<Label Text="{Loc 'ui-options-general-cursor'}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="ShowHeldItemCheckBox" Text="{Loc 'ui-options-show-held-item'}" />

View File

@ -63,6 +63,7 @@ public sealed partial class MiscTab : Control
Control.AddOptionCheckBox(CCVars.ChatEnableFancyBubbles, FancySpeechBubblesCheckBox);
Control.AddOptionCheckBox(CCVars.ChatFancyNameBackground, FancyNameBackgroundsCheckBox);
Control.AddOptionCheckBox(CCVars.StaticStorageUI, StaticStorageUI);
Control.AddOptionCheckBox(CCVars.InterfaceChatFollowButton, ChatFollowButton);
Control.Initialize();
}

View File

@ -19,6 +19,7 @@ public sealed class SSDIndicatorSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly Shared.SSDIndicator.SSDIndicatorSystem _shared = default!; // DeltaV - SSD Recency, don't want to rename the upstream class
public override void Initialize()
{
@ -35,13 +36,26 @@ public sealed class SSDIndicatorSystem : EntitySystem
!HasComp<ActiveNPCComponent>(uid) &&
HasComp<MindExaminableComponent>(uid))
{
// Begin DeltaV Addition
// Begin DeltaV Additions
var ev = new ShowSSDIndicatorEvent();
RaiseLocalEvent(uid, ref ev);
if (ev.Hidden)
return;
// End DeltaV Addition
args.StatusIcons.Add(_prototype.Index(component.Icon));
// SSD Recency Indicator
var stage = _shared.GetStage(new Entity<SSDIndicatorComponent>(uid, component));
var icon = stage switch
{
SsdStage.VeryRecent => component.VeryRecentIcon,
SsdStage.Recent => component.RecentIcon,
SsdStage.Cryoable => component.Icon,
_ => throw new InvalidOperationException($"{ToPrettyString(uid)} has an invalid SSD stage {stage}."),
};
args.StatusIcons.Add(_prototype.Index(icon));
// End DeltaV Additions
// args.StatusIcons.Add(_prototype.Index(component.Icon)); // DeltaV - commented out. status icon now added above
}
}
}

View File

@ -1,6 +0,0 @@
using Content.Shared._DV.Psionics.Systems.PsionicPowers;
namespace Content.Client._DV.Psionics.Systems.PsionicPowers;
// This does nothing here. The code is all in the shared/server version.
public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem;

View File

@ -108,6 +108,70 @@ public sealed partial class TraitCategory : BoxContainer
{
CategoryPointsLabel.Visible = false;
}
// Lock unselected entries whenever the category cap is reached so the
// player gets a clear visual cue instead of silently failing to equip.
UpdateCategoryFullState();
}
/// <summary>
/// Propagates category-cap lock state to every entry.
/// <para>
/// Selected entries are never locked - they are the traits filling the cap
/// and must stay interactive so the player can deselect them.
/// </para>
/// <para>
/// Unselected entries are locked when:
/// <list type="bullet">
/// <item><see cref="TraitCategoryPrototype.MaxTraits"/> is defined and already reached, OR</item>
/// <item><see cref="TraitCategoryPrototype.MaxPoints"/> is defined and this specific
/// trait's cost would push spending over the limit.</item>
/// </list>
/// </para>
/// </summary>
private void UpdateCategoryFullState()
{
var traitCapReached = _category.MaxTraits.HasValue && SelectedCount >= _category.MaxTraits.Value;
foreach (var (_, entry) in _traitEntries)
{
if (entry.IsSelected)
{
// Selected traits are never locked by category state.
entry.SetLockedByCategory(false);
continue;
}
var locked = traitCapReached;
// Even when the trait count cap hasn't been hit, lock this specific entry
// if adding its cost would exceed the category's point budget.
if (!locked && _category.MaxPoints.HasValue)
locked = PointsSpent + entry.TraitCost > _category.MaxPoints.Value;
entry.SetLockedByCategory(locked);
}
}
/// <summary>
/// Called by <see cref="TraitsTab"/> whenever the player's global point budget changes.
/// Locks any unselected entry whose cost the player can no longer afford.
/// <para>
/// Free (0-cost) and negative-cost traits are never locked - they cost nothing or
/// actually give points back. Selected traits are also never locked here; they
/// were affordable when picked and must stay interactive to be deselected.
/// </para>
/// </summary>
public void UpdateGlobalPointsLock(int remainingPoints)
{
foreach (var (_, entry) in _traitEntries)
{
// A selected trait is always unlocked here - same reasoning as category cap:
// it was paid for at selection time and must stay interactive.
// Traits that cost nothing or give points back are never locked by budget.
var shouldLock = !entry.IsSelected && entry.TraitCost > remainingPoints;
entry.SetLockedByPoints(shouldLock);
}
}
public void SetTraitSelected(ProtoId<TraitPrototype> traitId, bool selected)
@ -171,11 +235,11 @@ public sealed partial class TraitCategory : BoxContainer
/// Updates condition states for all trait entries based on current job/species.
/// Traits that don't meet conditions are disabled but still visible.
/// </summary>
public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
public void UpdateConditions(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences, IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
foreach (var (_, entry) in _traitEntries)
{
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences);
entry.UpdateConditionsMet(jobId, speciesId, antagPreferences, selectedTraits);
}
// Update stats since some traits may have been deselected

View File

@ -27,6 +27,9 @@ public sealed partial class TraitEntry : PanelContainer
private bool _isUpdating;
private readonly List<string> _failedConditionTooltips = new();
private bool _isLockedByCategory;
private bool _isLockedByPoints;
public TraitEntry(TraitPrototype trait)
{
RobustXamlLoader.Load(this);
@ -59,9 +62,17 @@ public sealed partial class TraitEntry : PanelContainer
foreach (var condition in _trait.Conditions)
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
tooltips.Add(tooltip);
if (condition is TraitDependencyCondition depCond)
{
// Use GetTooltips() to get individual lines without duplication
tooltips.AddRange(depCond.GetTooltips(_prototype, _loc));
}
else
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
tooltips.Add(tooltip);
}
}
if (tooltips.Count > 0)
@ -91,39 +102,92 @@ public sealed partial class TraitEntry : PanelContainer
/// <summary>
/// Updates whether conditions are met based on current job/species.
/// </summary>
public void UpdateConditionsMet(ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
public void UpdateConditionsMet(
ProtoId<JobPrototype>? jobId,
ProtoId<SpeciesPrototype>? speciesId,
IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences,
IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
_failedConditionTooltips.Clear();
MeetsConditions = true;
foreach (var condition in _trait.Conditions)
{
var result = condition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby but screws with the inversion logic
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences),
_ => true,
};
bool result;
// Apply inversion
result ^= condition.Invert;
if (condition is TraitDependencyCondition depCond)
{
result = CheckDependencyCondition(depCond, selectedTraits);
// CheckDependencyCondition adds its own tooltips directly
}
else
{
result = condition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition anyOfCond => CheckAnyOfCondition(anyOfCond, jobId, speciesId, antagPreferences, selectedTraits),
_ => true,
};
// Apply inversion for non-dependency conditions
result ^= condition.Invert;
if (!result)
{
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
_failedConditionTooltips.Add(tooltip);
}
}
if (!result)
{
MeetsConditions = false;
var tooltip = condition.GetTooltip(_prototype, _loc);
if (!string.IsNullOrEmpty(tooltip))
_failedConditionTooltips.Add(tooltip);
}
}
UpdateDisabledState();
}
/// <summary>
/// Checks a TraitDependencyCondition and adds failure tooltips directly.
/// Returns true if the condition passes.
/// </summary>
private bool CheckDependencyCondition(TraitDependencyCondition condition, IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
var passed = true;
foreach (var conflict in condition.Conflicts)
{
if (selectedTraits == null || !selectedTraits.Contains(conflict))
continue;
if (_prototype.TryIndex(conflict, out var conflictProto))
{
_failedConditionTooltips.Add(_loc.GetString("trait-condition-trait-conflict",
("trait", _loc.GetString(conflictProto.Name))));
}
passed = false;
}
foreach (var required in condition.Requires)
{
if (selectedTraits != null && selectedTraits.Contains(required))
continue;
if (_prototype.TryIndex(required, out var requiredProto))
{
_failedConditionTooltips.Add(_loc.GetString("trait-condition-trait-required",
("trait", _loc.GetString(requiredProto.Name))));
}
passed = false;
}
return passed;
}
private bool CheckSpeciesCondition(IsSpeciesCondition condition, ProtoId<SpeciesPrototype>? speciesId)
{
if (!_prototype.TryIndex(speciesId, out var species))
@ -159,7 +223,12 @@ public sealed partial class TraitEntry : PanelContainer
return antagPreferences.Contains(condition.Antag);
}
private bool CheckAnyOfCondition(AnyOfCondition condition, ProtoId<JobPrototype>? jobId, ProtoId<SpeciesPrototype>? speciesId, IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences)
private bool CheckAnyOfCondition(
AnyOfCondition condition,
ProtoId<JobPrototype>? jobId,
ProtoId<SpeciesPrototype>? speciesId,
IReadOnlySet<ProtoId<AntagPrototype>>? antagPreferences,
IReadOnlySet<ProtoId<TraitPrototype>>? selectedTraits)
{
if (condition.Conditions.Count == 0)
return false;
@ -167,19 +236,27 @@ public sealed partial class TraitEntry : PanelContainer
// Return true if ANY child condition evaluates to true
foreach (var childCondition in condition.Conditions)
{
var result = childCondition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences), // Recursive!
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
_ => true,
};
bool result;
// Apply child's inversion
result ^= childCondition.Invert;
if (childCondition is TraitDependencyCondition depCond)
{
result = CheckDependencyCondition(depCond, selectedTraits);
}
else
{
result = childCondition switch
{
IsSpeciesCondition speciesCond => CheckSpeciesCondition(speciesCond, speciesId),
HasJobCondition jobCond => CheckJobCondition(jobCond, jobId),
InDepartmentCondition deptCond => CheckDepartmentCondition(deptCond, jobId),
HasCompCondition compCond => !compCond.Invert, // can't check in lobby
IsAntagEligibleCondition antagEligibleCond => CheckAntagEligibleCondition(antagEligibleCond, antagPreferences),
AnyOfCondition nestedAnyOf => CheckAnyOfCondition(nestedAnyOf, jobId, speciesId, antagPreferences, selectedTraits),
_ => true,
};
result ^= childCondition.Invert;
}
// If any child passes, the AnyOf passes
if (result)
@ -190,16 +267,50 @@ public sealed partial class TraitEntry : PanelContainer
return false;
}
/// <summary>
/// Called by <see cref="TraitCategory"/> whenever the category's trait or
/// points-cap state changes. Locking only applies to <em>unselected</em>
/// entries - the selected traits are the ones filling the cap and must
/// remain interactive so the player can deselect them.
/// </summary>
public void SetLockedByCategory(bool locked)
{
if (_isLockedByCategory == locked)
return;
_isLockedByCategory = locked;
UpdateDisabledState();
}
/// <summary>
/// Called by <see cref="TraitCategory"/> whenever the player's global point budget
/// changes. Locks unselected entries whose cost the player can no longer afford.
/// Free (0-cost) and negative-cost traits are never locked by this.
/// </summary>
public void SetLockedByPoints(bool locked)
{
if (_isLockedByPoints == locked)
return;
_isLockedByPoints = locked;
UpdateDisabledState();
}
private void UpdateDisabledState()
{
if (!MeetsConditions)
var isSelected = TraitCheckbox.Pressed;
var conditionLocked = !MeetsConditions;
var categoryLocked = _isLockedByCategory && !isSelected;
var pointsLocked = _isLockedByPoints && !isSelected;
var isDisabled = conditionLocked || categoryLocked || pointsLocked;
if (isDisabled)
{
// Hide checkbox, show lock icon
TraitCheckbox.Visible = false;
LockIcon.Visible = true;
// Deselect if conditions no longer met
if (TraitCheckbox.Pressed)
if (isSelected && conditionLocked)
{
_isUpdating = true;
TraitCheckbox.Pressed = false;
@ -211,14 +322,24 @@ public sealed partial class TraitEntry : PanelContainer
// Add disabled styling
AddStyleClass("TraitsEntryDisabled");
// Update tooltip to show failed conditions
if (_failedConditionTooltips.Count > 0)
// Tooltip priority for conditions: condition failures > category full > insufficient points.
if (conditionLocked && _failedConditionTooltips.Count > 0)
{
var tooltipText = Loc.GetString("trait-conditions-not-met-tooltip",
("requirements", string.Join("\n", _failedConditionTooltips)));
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
else if (categoryLocked)
{
var tooltipText = Loc.GetString("trait-category-full-tooltip");
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
else if (pointsLocked)
{
var tooltipText = Loc.GetString("trait-insufficient-points-tooltip");
TooltipSupplier = _ => CreateMarkupTooltip(tooltipText);
}
}
else
{

View File

@ -154,14 +154,18 @@ public sealed partial class TraitsTab : BoxContainer
}
}
// Check conflicts
foreach (var conflict in trait.Conflicts)
// Check conflicts via TraitDependencyCondition
var depCondition = trait.Conditions.OfType<Content.Shared._DV.Traits.Conditions.TraitDependencyCondition>().FirstOrDefault();
if (depCondition != null)
{
if (!_selectedTraits.Contains(conflict))
continue;
foreach (var conflict in depCondition.Conflicts)
{
if (!_selectedTraits.Contains(conflict))
continue;
RevertTraitToggle(traitId);
return;
RevertTraitToggle(traitId);
return;
}
}
_selectedTraits.Add(traitId);
@ -176,6 +180,7 @@ public sealed partial class TraitsTab : BoxContainer
}
UpdateGlobalStats();
UpdateAllConditions();
UpdateCategoryStats(trait.Category);
OnTraitsChanged?.Invoke(_selectedTraits);
}
@ -211,7 +216,7 @@ public sealed partial class TraitsTab : BoxContainer
// If parent width is 0 (not laid out yet), defer until layout happens
if (parentWidth > 0)
{
GlobalPointsBar.SetWidth = (int)((parentWidth - 2 ) * percentage);
GlobalPointsBar.SetWidth = (int)((parentWidth - 2) * percentage);
_awaitingLayoutUpdate = false;
}
else if (!_awaitingLayoutUpdate)
@ -235,6 +240,11 @@ public sealed partial class TraitsTab : BoxContainer
> 0f => "TraitsProgressBarLow",
_ => "TraitsProgressBarEmpty"
});
foreach (var (_, categoryUi) in _categoryUis)
{
categoryUi.UpdateGlobalPointsLock(remainingPoints);
}
}
private void OnProgressBarParentResized()
@ -281,7 +291,7 @@ public sealed partial class TraitsTab : BoxContainer
foreach (var (_, categoryUi) in _categoryUis)
{
// If some fork wants to use the top selected job as well, just add that to the UpdateConditions method in the editor
categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences);
categoryUi.UpdateConditions(null, _profile?.Species, _profile?.AntagPreferences, _selectedTraits);
}
RecalculateStats();

View File

@ -20,7 +20,11 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
private const string AnimateKey = "particle-animation";
private static readonly EntProtoId InteractionParticleId = "StellarInteractionParticle";
private static readonly Dictionary<StellarInteractionParticleType, EntProtoId> InteractionParticleIds = new ()
{
{ StellarInteractionParticleType.Use, "StellarInteractionParticleUse" },
{ StellarInteractionParticleType.Pull, "StellarInteractionParticlePull" },
};
public override void Initialize()
{
@ -38,6 +42,11 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
if (!Exists(performer) || !Exists(target))
return;
if (ev.Type == StellarInteractionParticleType.Pull)
{
(performer, target) = (target, performer);
}
var performerXform = Transform(performer);
var targetXform = Transform(target);
if (performerXform.MapID == MapId.Nullspace || targetXform.MapID == MapId.Nullspace)
@ -47,7 +56,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
return;
var performerTargetDelta = targetXform.LocalPosition - performerXform.LocalPosition;
var particle = Spawn(InteractionParticleId, performerXform.Coordinates);
var particle = Spawn(InteractionParticleIds[ev.Type], performerXform.Coordinates);
if (used is { } usedEntity && Exists(usedEntity) && TryComp<SpriteComponent>(usedEntity, out var usedSprite))
{
@ -58,21 +67,27 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
}
var spriteColor = Comp<SpriteComponent>(particle).Color;
_animation.Play(particle, GetAnimation(performerTargetDelta, spriteColor), AnimateKey);
var animation = ev.Type switch
{
StellarInteractionParticleType.Use => GetUseAnimation(performerTargetDelta, spriteColor),
StellarInteractionParticleType.Pull => GetPullAnimation(performerTargetDelta, spriteColor),
_ => throw new ArgumentOutOfRangeException(nameof(ev), $"Interaction particle event has unknown particle type {ev.Type}"),
};
_animation.Play(particle, animation, AnimateKey);
}
private Animation GetAnimation(Vector2 endOffset, Color color)
private Animation GetUseAnimation(Vector2 endOffset, Color color)
{
var startRotation = _random.NextAngle(Angle.FromDegrees(-80), Angle.FromDegrees(80));
var startRotation = _random.NextAngle(Angle.FromDegrees(-40), Angle.FromDegrees(40));
var endRotation = Angle.Zero;
var startScale = new Vector2(0.3f, 0.3f);
var endScale = new Vector2(1f, 1f);
var rotationLength = TimeSpan.FromMilliseconds(600);
var startOffset = new Vector2();
var offsetLength = TimeSpan.FromMilliseconds(200);
var offsetLength = TimeSpan.FromMilliseconds(250);
var startColor = color.WithAlpha(color.A * 0.9f);
var startColor = color.WithAlpha(color.A * 0.7f);
var endColor = color.WithAlpha(0f);
var colorLength = rotationLength + offsetLength;
@ -89,7 +104,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startRotation, 0f),
new AnimationTrackProperty.KeyFrame(endRotation, (float)rotationLength.TotalSeconds, Easings.OutBounce),
new AnimationTrackProperty.KeyFrame(endRotation, (float)rotationLength.TotalSeconds, Easings.OutBack),
},
},
new AnimationTrackComponentProperty()
@ -99,7 +114,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startScale, 0f),
new AnimationTrackProperty.KeyFrame(endScale, (float)rotationLength.TotalSeconds, Easings.OutBounce),
new AnimationTrackProperty.KeyFrame(endScale, (float)rotationLength.TotalSeconds, Easings.OutBack),
},
},
new AnimationTrackComponentProperty()
@ -109,7 +124,7 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startOffset, 0f),
new AnimationTrackProperty.KeyFrame(endOffset, (float)offsetLength.TotalSeconds, Easings.OutBounce),
new AnimationTrackProperty.KeyFrame(endOffset, (float)offsetLength.TotalSeconds, Easings.OutBack),
},
},
new AnimationTrackComponentProperty()
@ -126,4 +141,44 @@ public sealed class StellarInteractionParticleSystem : EntitySystem
},
};
}
private Animation GetPullAnimation(Vector2 endOffset, Color color)
{
var rotationLength = TimeSpan.FromMilliseconds(8f * (1000f / 12f));
var startOffset = new Vector2();
var offsetLength = TimeSpan.FromMilliseconds(4f * (1000f / 12f));
var endColor = color.WithAlpha(0f);
return new Animation
{
Length = rotationLength,
AnimationTracks =
{
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Offset),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(startOffset, 0f),
new AnimationTrackProperty.KeyFrame(endOffset, (float)rotationLength.TotalSeconds, Easings.InOutCirc),
},
},
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Color),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(color, 0f),
new AnimationTrackProperty.KeyFrame(color, (float)offsetLength.TotalSeconds),
new AnimationTrackProperty.KeyFrame(endColor, (float)rotationLength.TotalSeconds, Easings.InOutCirc),
},
},
},
};
}
}

View File

@ -245,6 +245,7 @@ namespace Content.IntegrationTests.Tests
"TimedDespawnDetailed", // DeltaV
// makes an announcement on mapInit.
"AnnounceOnSpawn",
"ESTimedDespawn" // DeltaV
};
Assert.That(server.CfgMan.GetCVar(CVars.NetPVS), Is.False);

View File

@ -6,6 +6,7 @@ using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Player; // DeltaV
namespace Content.Server.AlertLevel;
@ -191,7 +192,8 @@ public sealed class AlertLevelSystem : EntitySystem
{
if (detail.Sound != null)
{
var filter = _stationSystem.GetInOwningStation(station);
//var filter = _stationSystem.GetInOwningStation(station); // DeltaV - Global Annoucements
var filter = Filter.Empty().AddAllPlayers(); // DeltaV - Global Annoucements
_audio.PlayGlobal(detail.Sound, filter, true, detail.Sound.Params);
}
else
@ -202,7 +204,7 @@ public sealed class AlertLevelSystem : EntitySystem
if (announce)
{
_chatSystem.DispatchStationAnnouncement(station, announcementFull, playDefaultSound: playDefault,
_chatSystem.DispatchGlobalAnnouncement(announcementFull, playSound: playDefault, // DeltaV - Global Annoucements
colorOverride: detail.Color, sender: stationName);
}

View File

@ -87,7 +87,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
if (anomaly.Comp.CurrentBehavior is not null)
RemoveBehavior(anomaly, anomaly.Comp.CurrentBehavior.Value);
EndAnomaly(anomaly, spawnCore: false);
EndAnomaly(anomaly, spawnCore: false, forced: true);
}
private void OnStartCollide(Entity<AnomalyComponent> anomaly, ref StartCollideEvent args)
@ -143,7 +143,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
return 0;
var multiplier = 1f;
if (component.Stability > component.GrowthThreshold)
if (component.AlwaysGrow || component.Stability > component.GrowthThreshold) // DeltaV - Add AlwaysGrow
multiplier = component.GrowingPointMultiplier; //more points for unstable
//penalty of up to 50% based on health
@ -253,7 +253,11 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
else
{
string stateLoc;
if (anomalyComp.Stability < anomalyComp.DecayThreshold)
// DeltaV - Colossus Additions START
if (anomalyComp.AlwaysGrow)
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
// DeltaV - Colossus Additions END
else if (anomalyComp.Stability < anomalyComp.DecayThreshold) // DeltaV - Add else
stateLoc = Loc.GetString("anomaly-scanner-stability-low");
else if (anomalyComp.Stability > anomalyComp.GrowthThreshold)
stateLoc = Loc.GetString("anomaly-scanner-stability-high");
@ -345,6 +349,15 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-unknown"));
else
{
// DeltaV - Colossus Additions START
if (anomalyComp.AlwaysGrow)
{
msg.AddMarkupOrThrow("- " + Loc.GetString("anomaly-behavior-always-grow"));
if (anomalyComp.CurrentBehavior != null)
msg.PushNewline();
}
// DeltaV - Colossus Additions END
if (anomalyComp.CurrentBehavior != null)
{
var behavior = _prototype.Index(anomalyComp.CurrentBehavior.Value);
@ -354,7 +367,7 @@ public sealed partial class AnomalySystem : SharedAnomalySystem
var mod = Math.Floor((behavior.EarnPointModifier) * 100);
msg.AddMarkupOrThrow("- " + Loc.GetString("anomaly-behavior-point", ("mod", mod)));
}
else
else if(!anomalyComp.AlwaysGrow) // DeltaV - Add condition, previously regular else
{
msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-balanced"));
}

View File

@ -7,7 +7,7 @@ using Robust.Shared.Player;
namespace Content.Server.Audio;
public sealed class ServerGlobalSoundSystem : SharedGlobalSoundSystem
public sealed partial class ServerGlobalSoundSystem : SharedGlobalSoundSystem // DeltaV - Made Partial
{
[Dependency] private readonly IConsoleHost _conHost = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;

View File

@ -5,6 +5,7 @@ using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.Discord.DiscordLink;
using Content.Server.Ghost;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;
using Content.Shared.Administration;
@ -15,6 +16,7 @@ using Content.Shared.Mind;
using Content.Shared.Players; // DeltaV - OOC muting
using Content.Shared.Players.RateLimiting;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Replays;
@ -47,6 +49,7 @@ internal sealed partial class ChatManager : IChatManager
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly DiscordChatLink _discordLink = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
private ISawmill _sawmill = default!;
@ -342,12 +345,35 @@ internal sealed partial class ChatManager : IChatManager
#region Utility
private bool IsValidWarpDestination(EntityUid source)
{
if (!source.Valid)
return false;
if (!_entityManager.TryGetComponent(source, out TransformComponent? transform))
return false;
return transform.MapID != MapId.Nullspace;
}
public string PrependFollowButtonIfAppropriate(string wrappedMessage, EntityUid source, INetChannel recipient)
{
if (IsValidWarpDestination(source) && ShouldShowFollowButton(recipient))
{
var btnText = _localizationManager.GetString("chat-manager-follow-button");
return $"[cmdlink=\"{btnText}\" command=\"{GhostFollowEntityCommand.CommandName} {_entityManager.GetNetEntity(source)}\" /] " + wrappedMessage;
}
return wrappedMessage;
}
public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
{
var user = author == null ? null : EnsurePlayer(author);
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
wrappedMessage = PrependFollowButtonIfAppropriate(wrappedMessage, source, client);
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendMessage(new MsgChatMessage() { Message = msg }, client);
@ -370,8 +396,12 @@ internal sealed partial class ChatManager : IChatManager
var netSource = _entityManager.GetNetEntity(source);
user?.AddEntity(netSource);
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendToMany(new MsgChatMessage() { Message = msg }, clients);
foreach (var client in clients)
{
var customWrapMessage = PrependFollowButtonIfAppropriate(wrappedMessage, source, client);
var msg = new ChatMessage(channel, message, customWrapMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_netManager.ServerSendMessage(new MsgChatMessage { Message = msg }, client);
}
if (!recordReplay)
return;
@ -379,6 +409,7 @@ internal sealed partial class ChatManager : IChatManager
if ((channel & ChatChannel.AdminRelated) == 0 ||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
{
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
_replay.RecordServerMessage(msg);
}
}
@ -439,6 +470,22 @@ internal sealed partial class ChatManager : IChatManager
}
#endregion
private bool ShouldShowFollowButton(INetChannel recipient)
{
if (!_player.TryGetSessionByChannel(recipient, out var session))
return false;
if (_entityManager.TrySystem(out GhostSystem? ghost))
{
if (!ghost.CanGhostWarp(session, out _))
{
return false;
}
}
return _netConfigManager.GetClientCVar(recipient, CCVars.InterfaceChatFollowButton);
}
}
public enum OOCChatType : byte

View File

@ -93,7 +93,6 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
Entry("('=", "chatsan-tearfully-smiles"),
Entry("['=", "chatsan-tearfully-smiles"),
Entry("?", "chatsan-confused"), //DeltaV
Entry("!", "chatsan-surprised"), //DeltaV
Entry("…", "chatsan-sighs"), //DeltaV (note that ... doesn't work as that turns into a radio message)
];

View File

@ -49,5 +49,7 @@ namespace Content.Server.Chat.Managers
/// <param name="player">The player sending a chat message.</param>
/// <returns>False if the player has violated rate limits and should be blocked from sending further messages.</returns>
RateLimitStatus HandleRateLimit(ICommonSession player);
string PrependFollowButtonIfAppropriate(string wrappedMessage, EntityUid source, INetChannel recipient);
}
}

View File

@ -8,6 +8,7 @@ using Content.Server.GameTicking;
using Content.Server.Speech.EntitySystems;
using Content.Shared.Speech.Hushing; // DeltaV
using Content.Server.Nyanotrasen.Chat;
using Content.Server.Speech.Components; // DeltaV
using Content.Server.Speech.Prototypes;
using Content.Server.Station.Systems;
using Content.Shared.ActionBlocker;
@ -35,6 +36,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Utility;
using Content.Shared._DV.Chat; // DeltaV - chat enhancements
namespace Content.Server.Chat.Systems;
@ -215,7 +217,7 @@ public sealed partial class ChatSystem : SharedChatSystem
// Was there an emote in the message? If so, send it.
if (player != null && emoteStr != message && emoteStr != null)
{
SendEntityEmote(source, emoteStr, range, nameOverride, ignoreActionBlocker);
SendEntityEmote(source, emoteStr, range, nameOverride, null, ignoreActionBlocker); // DeltaV - Had to change up for SendEntityEmote
}
// This can happen if the entire string is sanitized out.
@ -252,9 +254,18 @@ public sealed partial class ChatSystem : SharedChatSystem
case InGameICChatType.Whisper:
SendEntityWhisper(source, message, range, null, nameOverride, hideLog, ignoreActionBlocker);
break;
case InGameICChatType.Emote:
SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
break;
case InGameICChatType.Emote: // DeltaV - Emote now has different types of emotes.
var type = ProcessEmoteMessage(source, message, out var modMessage);
if (type == EmoteType.Audible || type == EmoteType.AudiblePossessive)
{
if (checkRadioPrefix && TryProcessRadioMessage(source, modMessage, out var outputMessage, out var channel, capitalize: false))
SendAudibleEntityEmote(source, outputMessage, range, nameOverride, channel, type, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
else
SendAudibleEntityEmote(source, modMessage, range, nameOverride, null, type, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
}
else
SendEntityEmote(source, modMessage, range, nameOverride, type, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker);
break; // DeltaV - End
//Nyano - Summary: case adds the telepathic chat sending ability.
case InGameICChatType.Telepathic:
_nyanoChatSystem.SendTelepathicChat(source, message, range == ChatTransmitRange.HideChat);
@ -629,6 +640,7 @@ public sealed partial class ChatSystem : SharedChatSystem
string action,
ChatTransmitRange range,
string? nameOverride,
EmoteType? emoteType, // DeltaV
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
@ -642,11 +654,21 @@ public sealed partial class ChatSystem : SharedChatSystem
var ent = Identity.Entity(source, EntityManager);
string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent));
// Begin DeltaV
string wrappedMessage;
// Emotes use Identity.Name, since it doesn't actually involve your voice at all.
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
if (emoteType == EmoteType.Possessive) // DeltaV - Emote types now get checked.
wrappedMessage = Loc.GetString("chat-manager-entity-me-possessive-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
else
wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
// End DeltaV
if (checkEmote &&
!TryEmoteChatInput(source, action))
@ -660,6 +682,59 @@ public sealed partial class ChatSystem : SharedChatSystem
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}");
}
// DeltaV - Added this to differentiate between emotes that can be heard over radio and those which can't.
protected override void SendAudibleEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
RadioChannelPrototype? channel,
EmoteType? emoteType,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
NetUserId? author = null
)
{
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;
EmoteType type = emoteType ?? EmoteType.Audible;
// get the entity's apparent name (if no override provided).
var ent = Identity.Entity(source, EntityManager);
string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent));
string wrappedMessage;
// Audible emotes use Identity.Name, since that is the status quo. Emotes like scream doesn't currently reveal identities so this won't either. [This may be changed with feedback as you can audibly emote over radios]
if (emoteType == EmoteType.AudiblePossessive)
wrappedMessage = Loc.GetString("chat-manager-entity-me-audible-possessive-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
else
wrappedMessage = Loc.GetString("chat-manager-entity-me-audible-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
if (checkEmote &&
!TryEmoteChatInput(source, action))
return;
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
var ev = new EntityAudiblyEmotedEvent(source, action, channel, type);
RaiseLocalEvent(source, ref ev, true);
if (!hideLog)
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}");
}
// DeltaV - End
// ReSharper disable once InconsistentNaming
private void SendLOOC(EntityUid source, ICommonSession player, string message, bool hideChat)
{
@ -889,6 +964,7 @@ public sealed partial class ChatSystem : SharedChatSystem
var recipients = new Dictionary<ICommonSession, ICChatRecipientData>();
var ghostHearing = GetEntityQuery<GhostHearingComponent>();
var xforms = GetEntityQuery<TransformComponent>();
var blockListening = GetEntityQuery<BlockListeningComponent>(); // DeltaV - block listening
var transformSource = xforms.GetComponent(source);
var sourceMapId = transformSource.MapID;
@ -904,6 +980,11 @@ public sealed partial class ChatSystem : SharedChatSystem
if (transformEntity.MapID != sourceMapId)
continue;
// Begin DeltaV - block listening
if (blockListening.HasComponent(playerEntity))
continue;
// End DeltaV - block listening
var observer = ghostHearing.HasComponent(playerEntity);
// even if they are a ghost hearer, in some situations we still need the range
@ -971,4 +1052,4 @@ public sealed class CheckTargetedSpeechEvent : EntityEventArgs
{
public List<EntityUid> Targets = new List<EntityUid>();
}
// END Mono
// END Mono

View File

@ -382,11 +382,6 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
var visualEnt = CreateExplosionVisualEntity(pos, queued.Proto.ID, spaceMatrix, spaceData, gridData.Values, iterationIntensity);
// camera shake
// ES START
// CameraShake(iterationIntensity.Count * 4f, pos, queued.TotalIntensity);
// ES END
//For whatever bloody reason, sound system requires ENTITY coordinates.
var mapEntityCoords = _transformSystem.ToCoordinates(_map.GetMap(pos.MapId), pos);
@ -416,12 +411,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
? queued.Proto.SmallSoundFar
: queued.Proto.SoundFar;
// ES START
var farTranslationShake = iterationIntensity.Count < queued.Proto.SmallSoundIterationThreshold
? new ESScreenshakeParameters() { Trauma = 0.4f, DecayRate = 0.2f, Frequency = 0.014f }
: new ESScreenshakeParameters() { Trauma = 0.6f, DecayRate = 0.05f, Frequency = 0.014f };
_shake.Screenshake(filter, farTranslationShake, null);
// ES END
CameraShake(iterationIntensity, pos, queued); // Starlight
_audio.PlayGlobal(farSound, farFilter, true, farSound.Params);
@ -444,8 +434,13 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
_damageableSystem);
}
private void CameraShake(float range, MapCoordinates epicenter, float totalIntensity)
private void CameraShake(List<float> rangeList, MapCoordinates epicenter, QueuedExplosion queued) // Starlight - replace range with rangeList, totalIntensity with queued
{
// Starlight BEGIN
var range = rangeList.Count * 4f;
var totalIntensity = queued.TotalIntensity * 10f;
// Starlight END
var players = Filter.Empty();
players.AddInRange(epicenter, range, _playerManager, EntityManager);
@ -463,7 +458,40 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
var distance = delta.Length();
var effect = 5 * MathF.Pow(totalIntensity, 0.5f) * (1 - distance / range);
if (effect > 0.01f)
_recoilSystem.KickCamera(uid, -delta.Normalized() * effect);
{
// DeltaV - Camera kick falloff START
// _recoilSystem.KickCamera(uid, -delta.Normalized() * effect);
// Exponential decay: N(t) = N₀ ⋅ e^(-λ ⋅ t)
// In this case, N = effect and we aren't decaying over time but with increasing distance from the epicenter,
// so t = distance / range. Higher values of λ lead to faster decay, lower values to slower decay.
//
// severity is a fixed factor used to decrease the overall severity of the camera kick,
// as the values were so high by default that decay was only becoming really noticeable near range.
//
// these changes are made here instead of directly assigning to the effect variable
// above to maintain the same overall effect range as before.
const float severity = 0.033f;
const float lambda = 4f;
_recoilSystem.KickCamera(uid, -delta.Normalized() * effect * severity * MathF.Exp(-lambda * (distance / range)));
// DeltaV END
// Starlight START
var shakeParams = rangeList.Count < queued.Proto.SmallSoundIterationThreshold
? new ESScreenshakeParameters() { Trauma = 0.4f, DecayRate = 0.2f, Frequency = 0.014f }
: new ESScreenshakeParameters() { Trauma = 0.6f, DecayRate = 0.05f, Frequency = 0.014f };
// DeltaV - Screenshake falloff START
// Linear falloff with increasing distance from epicenter,
// capped at certain values to avoid diminishing the effect completely near range.
shakeParams.DecayRate *= MathF.Min(1 + distance / range, 1.75f);
shakeParams.Frequency *= MathF.Max(1 - distance / range, 0.33f);
shakeParams.Trauma *= MathF.Max(1 - distance / range, 0.33f);
// DeltaV END
_shake.Screenshake(players, shakeParams, null);
// Starlight END
}
}
}
}

View File

@ -12,6 +12,7 @@ using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.DeviceNetwork.Systems; // DeltaV - map init ordering
using Content.Shared.Emag.Systems;
using Content.Shared.Fax;
using Content.Shared.Fax.Components;
@ -64,7 +65,7 @@ public sealed class FaxSystem : EntitySystem
// Hooks
SubscribeLocalEvent<FaxMachineComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<FaxMachineComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<FaxMachineComponent, MapInitEvent>(OnMapInit, after: [typeof(SharedDeviceNetworkSystem)]); // DeltaV - map init order, we need address assigned first
SubscribeLocalEvent<FaxMachineComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<FaxMachineComponent, EntInsertedIntoContainerMessage>(OnItemSlotChanged);
@ -301,6 +302,11 @@ public sealed class FaxSystem : EntitySystem
if (!args.Data.TryGetValue(FaxConstants.FaxPaperNameData, out string? name) ||
!args.Data.TryGetValue(FaxConstants.FaxPaperContentData, out string? content))
return;
// Begin DeltaV - we removed the power requirement from device network but we still don't want
// unpowered faxes to happen
if (TryComp<ApcPowerReceiverComponent>(uid, out var receiver) && !receiver.Powered)
return;
// End DeltaV
args.Data.TryGetValue(FaxConstants.FaxPaperLabelData, out string? label);
args.Data.TryGetValue(FaxConstants.FaxPaperStampStateData, out string? stampState);

View File

@ -35,17 +35,17 @@ public sealed partial class PuddleSystem
if (!entity.Comp.SpillWhenThrown || Openable.IsClosed(entity.Owner))
return;
// DeltaV - Beer Goggles Safe Throw
if ( args.User is { } user && _safeSolutionThrower.GetSafeThrow(user))
{
_physics.SetAngularVelocity(entity, 0);
Transform(entity).LocalRotation = Angle.Zero;
return;
}
// END DeltaV
if (TrySplashSpillAt(entity.Owner, Transform(entity).Coordinates, out _, out var solution) && args.User != null)
{
// DeltaV - Beer Goggles Safe Throw
if (_safeSolutionThrower.GetSafeThrow(args.User.Value))
{
_physics.SetAngularVelocity(entity, 0);
Transform(entity).LocalRotation = Angle.Zero;
return;
}
// END DeltaV
AdminLogger.Add(LogType.Landed,
$"{ToPrettyString(entity.Owner):entity} spilled a solution {SharedSolutionContainerSystem.ToPrettyString(solution):solution} on landing");
}

View File

@ -0,0 +1,26 @@
using Content.Shared.Administration;
using Robust.Shared.Console;
namespace Content.Server.Ghost;
[AnyCommand]
internal sealed partial class GhostFollowEntityCommand : LocalizedEntityCommands
{
public const string CommandName = "ghost_follow_entity";
[Dependency] private GhostSystem _ghost = null!;
public override string Command => CommandName;
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1 || shell.Player is not { } player)
return;
var target = args[0];
if (!NetEntity.TryParse(target, out var targetEnt))
return;
_ghost.GhostWarpRequest(player, targetEnt);
}
}

View File

@ -299,10 +299,22 @@ namespace Content.Server.Ghost
#region Warp
public bool CanGhostWarp(ICommonSession session, out EntityUid entity)
{
if (session.AttachedEntity is not { Valid: true } sessionEntity
|| !_ghostQuery.HasComp(sessionEntity))
{
entity = default;
return false;
}
entity = sessionEntity;
return true;
}
private void OnGhostWarpsRequest(GhostWarpsRequestEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {Valid: true} entity
|| !_ghostQuery.HasComp(entity))
if (!CanGhostWarp(args.SenderSession, out var entity))
{
Log.Warning($"User {args.SenderSession.Name} sent a {nameof(GhostWarpsRequestEvent)} without being a ghost.");
return;
@ -312,30 +324,33 @@ namespace Content.Server.Ghost
RaiseNetworkEvent(response, args.SenderSession.Channel);
}
public void GhostWarpRequest(ICommonSession player, NetEntity target)
{
if (!CanGhostWarp(player, out var attached))
{
Log.Warning($"User {player.Name} tried to warp to {target} without being a ghost.");
return;
}
var realTarget = GetEntity(target);
if (!Exists(realTarget))
{
Log.Warning($"User {player.Name} tried to warp to an invalid entity id: {target}");
return;
}
WarpTo(attached, realTarget);
}
private void OnGhostWarpToTargetRequest(GhostWarpToTargetRequestEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {Valid: true} attached
|| !_ghostQuery.HasComp(attached))
{
Log.Warning($"User {args.SenderSession.Name} tried to warp to {msg.Target} without being a ghost.");
return;
}
var target = GetEntity(msg.Target);
if (!Exists(target))
{
Log.Warning($"User {args.SenderSession.Name} tried to warp to an invalid entity id: {msg.Target}");
return;
}
WarpTo(attached, target);
GhostWarpRequest(args.SenderSession, msg.Target);
}
private void OnGhostnadoRequest(GhostnadoRequestEvent msg, EntitySessionEventArgs args)
{
if (args.SenderSession.AttachedEntity is not {} uid
|| !_ghostQuery.HasComp(uid))
if (CanGhostWarp(args.SenderSession, out var uid))
{
Log.Warning($"User {args.SenderSession.Name} tried to ghostnado without being a ghost.");
return;

View File

@ -211,7 +211,7 @@ namespace Content.Server.Medical.BiomassReclaimer
_solution.ResolveSolution(toProcess, stream.BloodSolutionName, ref stream.BloodSolution, out var solution))
{
component.BloodReagents = solution.Clone();
component.BloodReagents.ScaleSolution(50 / component.BloodReagents.Volume);
//component.BloodReagents.ScaleSolution(50 / component.BloodReagents.Volume); // Delta V - This doesn't need to be here. It just always makes the solution ~50u but also might divide by 0. Just use the current blood level so more blood = more mess.
}
if (TryComp<ButcherableComponent>(toProcess, out var butcherableComponent))
{

View File

@ -327,15 +327,15 @@ public sealed class NukeSystem : EntitySystem
// should play
if (nuke.RemainingTime <= _nukeSongLength + nuke.AlertSoundTime + NukeSongBuffer && !nuke.PlayedNukeSong && !ResolvedSoundSpecifier.IsNullOrEmpty(_selectedNukeSong))
{
_sound.DispatchStationEventMusic(uid, _selectedNukeSong, StationEventMusicType.Nuke);
_sound.DispatchGlobalEventMusic(_selectedNukeSong, StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
nuke.PlayedNukeSong = true;
}
// play alert sound if time is running out
if (nuke.RemainingTime <= nuke.AlertSoundTime && !nuke.PlayedAlertSound)
{
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(nuke.AlertSound), new AudioParams{Volume = -5f});
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
_sound.PlayGlobal(_audio.ResolveSound(nuke.AlertSound), new AudioParams { Volume = -5f }); // DeltaV - Global Nuke SFX
_sound.StopGlobalEventMusic(StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
nuke.PlayedAlertSound = true;
UpdateAppearance(uid, nuke);
}
@ -504,9 +504,9 @@ public sealed class NukeSystem : EntitySystem
("time", (int) component.RemainingTime),
("location", FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString((uid, nukeXform)))));
var sender = Loc.GetString("nuke-component-announcement-sender");
_chatSystem.DispatchStationAnnouncement(stationUid ?? uid, announcement, sender, false, null, Color.Red);
_chatSystem.DispatchGlobalAnnouncement(announcement, sender, false, null, Color.Red); // DeltaV - Global Nuke Annoucement
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.ArmSound));
_sound.PlayGlobal(_audio.ResolveSound(component.ArmSound)); // DeltaV - Global Nuke Music
_nukeSongLength = (float) _audio.GetAudioLength(_selectedNukeSong).TotalSeconds;
// turn on the spinny light
@ -544,11 +544,11 @@ public sealed class NukeSystem : EntitySystem
// warn a crew
var announcement = Loc.GetString("nuke-component-announcement-unarmed");
var sender = Loc.GetString("nuke-component-announcement-sender");
_chatSystem.DispatchStationAnnouncement(uid, announcement, sender, false);
_chatSystem.DispatchGlobalAnnouncement(announcement, sender, false); // DeltaV - Global Nuke Announcement
component.PlayedNukeSong = false;
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(component.DisarmSound));
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
_sound.PlayGlobal(_audio.ResolveSound(component.DisarmSound)); // DeltaV - Global Nuke SFX
_sound.StopGlobalEventMusic(StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
// reset nuke remaining time to either itself or the minimum time, whichever is higher
component.RemainingTime = Math.Max(component.RemainingTime, component.MinimumTime);
@ -610,7 +610,7 @@ public sealed class NukeSystem : EntitySystem
OwningStation = transform.GridUid,
});
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
_sound.StopGlobalEventMusic(StationEventMusicType.Nuke); // DeltaV - Global Nuke Music
Del(uid);
}

View File

@ -9,6 +9,7 @@ using Content.Shared.Radio.Components;
using Content.Shared.Radio.EntitySystems;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Content.Shared._DV.Chat;
namespace Content.Server.Radio.EntitySystems;
@ -26,6 +27,7 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
SubscribeLocalEvent<HeadsetComponent, EncryptionChannelsChangedEvent>(OnKeysChanged);
SubscribeLocalEvent<WearingHeadsetComponent, EntitySpokeEvent>(OnSpeak);
SubscribeLocalEvent<WearingHeadsetComponent, EntityAudiblyEmotedEvent>(OnAudibleEmote); // DeltaV
}
private void OnKeysChanged(EntityUid uid, HeadsetComponent component, EncryptionChannelsChangedEvent args)
@ -48,22 +50,43 @@ public sealed class HeadsetSystem : SharedHeadsetSystem
EnsureComp<ActiveRadioComponent>(uid).Channels = new(keyHolder.Channels);
}
private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpokeEvent args)
// DeltaV
// WARNING - Be very careful when modifying this method.
// The implementation right now has null forgiving operatiors in OnSpeak() and OnAudibleEmote() since this only returns true if channel is not null
private bool CheckRadioCapable(EntityUid uid, WearingHeadsetComponent headset, RadioChannelPrototype? channel)
{
if (args.Channel != null
&& TryComp(component.Headset, out EncryptionKeyHolderComponent? keys)
&& keys.Channels.Contains(args.Channel.ID))
if (channel != null
&& TryComp(headset.Headset, out EncryptionKeyHolderComponent? keys)
&& keys.Channels.Contains(channel.ID))
{
// Begin DeltaV Additions: No using headsets if you lost your hands or are cuffed
if (!TryComp<HandsComponent>(uid, out var hands) || hands.Count < 1 ||
TryComp<CuffableComponent>(uid, out var cuffable) && _cuffable.IsCuffed((uid, cuffable)))
{
_popup.PopupEntity(Loc.GetString("headset-cant-reach"), uid, uid, PopupType.SmallCaution);
return;
return false;
}
// End DeltaV Additions
_radio.SendRadioMessage(uid, args.Message, args.Channel, component.Headset);
return true;
}
return false;
}
private void OnAudibleEmote(EntityUid uid, WearingHeadsetComponent component, EntityAudiblyEmotedEvent args)
{
if (CheckRadioCapable(uid, component, args.Channel))
{
_radio.SendRadioMessage(uid, args.Message, args.Channel!, component.Headset, emType: args.Type);
args.Channel = null;
}
}
// DeltaV - End
private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpokeEvent args)
{
if (CheckRadioCapable(uid, component, args.Channel)) // DeltaV - Put all the radio checks into the CheckRadioCapable() method.
{
_radio.SendRadioMessage(uid, args.Message, args.Channel!, component.Headset); // DeltaV - Made the args.Channel null ignorant as CheckRadioCapable() guarantees it not null.
args.Channel = null; // prevent duplicate messages from other listeners.
}
}

View File

@ -1,6 +1,9 @@
using Content.Server.Administration.Logs;
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Ghost;
using Content.Server.Power.Components;
using Content.Shared._DV.Chat;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Radio;
@ -27,6 +30,8 @@ public sealed class RadioSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly GhostSystem _ghost = default!;
// set used to prevent radio feedback loops.
private readonly HashSet<string> _messages = new();
@ -38,6 +43,7 @@ public sealed class RadioSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<IntrinsicRadioReceiverComponent, RadioReceiveEvent>(OnIntrinsicReceive);
SubscribeLocalEvent<IntrinsicRadioTransmitterComponent, EntitySpokeEvent>(OnIntrinsicSpeak);
SubscribeLocalEvent<IntrinsicRadioTransmitterComponent, EntityAudiblyEmotedEvent>(OnIntrinsicAudibleEmote); // DeltaV - Robots should be allowed to emote over radio.
_exemptQuery = GetEntityQuery<TelecomExemptComponent>();
}
@ -53,16 +59,43 @@ public sealed class RadioSystem : EntitySystem
private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, ref RadioReceiveEvent args)
{
if (TryComp(uid, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.Channel);
if (!TryComp(uid, out ActorComponent? actor))
return;
var msg = args.ChatMsg;
if (_ghost.CanGhostWarp(actor.PlayerSession, out _))
{
msg = new MsgChatMessage
{
Message = new ChatMessage(args.ChatMsg.Message)
{
WrappedMessage = _chatManager.PrependFollowButtonIfAppropriate(
args.ChatMsg.Message.WrappedMessage,
args.MessageSource,
actor.PlayerSession.Channel),
},
};
}
_netMan.ServerSendMessage(msg, actor.PlayerSession.Channel);
}
// DeltaV
private void OnIntrinsicAudibleEmote(EntityUid uid, IntrinsicRadioTransmitterComponent component, EntityAudiblyEmotedEvent args)
{
if (args.Channel != null && component.Channels.Contains(args.Channel.ID))
{
SendRadioMessage(uid, args.Message, args.Channel, uid, emType: args.Type);
}
}
// DeltaV - End
/// <summary>
/// Send radio message to all active radio listeners
/// </summary>
public void SendRadioMessage(EntityUid messageSource, string message, ProtoId<RadioChannelPrototype> channel, EntityUid radioSource, bool escapeMarkup = true)
public void SendRadioMessage(EntityUid messageSource, string message, ProtoId<RadioChannelPrototype> channel, EntityUid radioSource, bool escapeMarkup = true, EmoteType? emType = null) // DeltaV - EmoteType? added.
{
SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup);
SendRadioMessage(messageSource, message, _prototype.Index(channel), radioSource, escapeMarkup: escapeMarkup, emType: emType);
}
/// <summary>
@ -70,7 +103,7 @@ public sealed class RadioSystem : EntitySystem
/// </summary>
/// <param name="messageSource">Entity that spoke the message</param>
/// <param name="radioSource">Entity that picked up the message and will send it, e.g. headset</param>
public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true)
public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true, EmoteType? emType = null) // DeltaV - EmoteType? added.
{
// TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this.
if (!_messages.Add(message))
@ -92,14 +125,31 @@ public sealed class RadioSystem : EntitySystem
? FormattedMessage.EscapeText(message)
: message;
var wrappedMessage = Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap",
("color", channel.Color),
("fontType", speech.FontId),
("fontSize", speech.FontSize),
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
("channel", $"\\[{channel.LocalizedName}\\]"),
("name", name),
("message", content));
// DeltaV - This change is to change up how the messages are wrapped up. Basically changing the formatting depending on the emote type.
string wrappedMessage;
if (emType == EmoteType.Audible)
wrappedMessage = Loc.GetString("chat-radio-message-audible-emote-wrap",
("color", channel.Color),
("channel", $"\\[{channel.LocalizedName}\\]"),
("name", name),
("message", content));
else if (emType == EmoteType.AudiblePossessive)
wrappedMessage = Loc.GetString("chat-radio-message-audible-possessive-emote-wrap",
("color", channel.Color),
("channel", $"\\[{channel.LocalizedName}\\]"),
("name", name),
("message", content));
else
wrappedMessage = Loc.GetString(speech.Bold ? "chat-radio-message-wrap-bold" : "chat-radio-message-wrap",
("color", channel.Color),
("fontType", speech.FontId),
("fontSize", speech.FontSize),
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
("channel", $"\\[{channel.LocalizedName}\\]"),
("name", name),
("message", content));
// DeltaV - End
// most radios are relayed to chat, so lets parse the chat message beforehand
var chat = new ChatMessage(
@ -159,7 +209,7 @@ public sealed class RadioSystem : EntitySystem
}
/// <inheritdoc cref="TelecomServerComponent"/>
private bool HasActiveServer(MapId mapId, string channelId)
public bool HasActiveServer(MapId mapId, string channelId) // DeltaV - we need this
{
var servers = EntityQuery<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent, TransformComponent>();
foreach (var (_, keys, power, transform) in servers)

View File

@ -1,3 +1,4 @@
using System.Linq; // DeltaV
using System.Numerics;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Systems;
@ -8,6 +9,8 @@ using Content.Server.Popups;
using Content.Shared.Atmos;
using Content.Shared.Chat;
using Content.Shared.Dataset;
using Content.Shared.Mobs; // DeltaV
using Content.Shared.Mobs.Components; // DeltaV
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Pointing;
@ -47,14 +50,33 @@ namespace Content.Server.RatKing
if (!TryComp<HungerComponent>(uid, out var hunger))
return;
// DeltaV - modify cost of Raise Army based on the amount of alive servants
// Check on how many servants are alive to calculate the cost of a new servant
var aliveServants = component.Servants.Count(servant
=> TryComp<MobStateComponent>(servant, out var mobState) && mobState.CurrentState == MobState.Alive);
// calculate the cost multiplier
var multiplier = aliveServants switch
{
< 5 => 1.0f, // 1-5 servants: 10 hunger
< 10 => 1.5f, // 6-10 servants: 15 hunger
< 15 => 2.5f, // 11-15 servants: 25 hunger
< 20 => 5.0f, // 16-20 servants: 50 hunger
< 25 => 10.0f, // 21-25 servants: 100 hunger
_ => 15.0f, // Above 25 servants: 150 hunger
};
var hungerPerArmyUseAdjusted = component.HungerPerArmyUse * multiplier;
//make sure the hunger doesn't go into the negatives
if (_hunger.GetHunger(hunger) < component.HungerPerArmyUse)
if (_hunger.GetHunger(hunger) < hungerPerArmyUseAdjusted)
{
_popup.PopupEntity(Loc.GetString("rat-king-too-hungry"), uid, uid);
return;
}
args.Handled = true;
_hunger.ModifyHunger(uid, -component.HungerPerArmyUse, hunger);
_hunger.ModifyHunger(uid, - hungerPerArmyUseAdjusted, hunger);
// DeltaV - end to the modified cost of Raise Army
var servant = Spawn(component.ArmyMobSpawnId, Transform(uid).Coordinates);
var comp = EnsureComp<RatKingServantComponent>(servant);
comp.King = uid;

View File

@ -27,6 +27,11 @@ public sealed partial class BorgSystem
var query = EntityQueryEnumerator<BorgTransponderComponent, BorgChassisComponent, DeviceNetworkComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta))
{
// DeltaV Begin
if (!comp.Active)
return;
// DeltaV End
if (comp.NextDisable is { } nextDisable && now >= nextDisable)
DoDisable((uid, comp, chassis, meta));
@ -83,6 +88,11 @@ public sealed partial class BorgSystem
private void OnPacketReceived(Entity<BorgTransponderComponent> ent, ref DeviceNetworkPacketEvent args)
{
// DeltaV Begin
if (!ent.Comp.Active)
return;
// DeltaV End
var payload = args.Data;
if (!payload.TryGetValue(DeviceNetworkConstants.Command, out string? command))
return;
@ -197,4 +207,17 @@ public sealed partial class BorgSystem
return true;
}
// DeltaV Begin
/// <summary>
/// DeltaV - sets if the borg transponder is active
/// </summary>
public void SetTransponderActive(Entity<BorgTransponderComponent?> ent, bool active)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.Active = active;
}
// DeltaV End
}

View File

@ -29,12 +29,14 @@ public sealed partial class SpeechWireAction : ComponentWireAction<SpeechCompone
public override bool Cut(EntityUid user, Wire wire, SpeechComponent component)
{
_speech.SetSpeech(wire.Owner, false, component);
EntityManager.GetComponentOrNull<Components.UnblockableSpeechComponent>(user)?.Active = false; // DeltaV - we need this to override unblockable speech
return true;
}
public override bool Mend(EntityUid user, Wire wire, SpeechComponent component)
{
_speech.SetSpeech(wire.Owner, true, component);
EntityManager.GetComponentOrNull<Components.UnblockableSpeechComponent>(user)?.Active = true; // DeltaV - we need this to override unblockable speech
return true;
}

View File

@ -3,5 +3,9 @@ namespace Content.Server.Speech.Components
[RegisterComponent]
public sealed partial class UnblockableSpeechComponent : Component
{
// Begin DeltaV
[DataField]
public bool Active = true;
// End DeltaV
}
}

View File

@ -12,7 +12,7 @@ namespace Content.Server.Speech.EntitySystems
private void OnCheck(EntityUid uid, UnblockableSpeechComponent component, CheckIgnoreSpeechBlockerEvent args)
{
args.IgnoreBlocker = true;
args.IgnoreBlocker = component.Active; // DeltaV
}
}
}

View File

@ -1,4 +1,3 @@
using Content.Server._DV.Psionics.Systems;
using Content.Server.Administration.Managers;
using Content.Server.Atmos.Components;
using Content.Server.Body.Components;
@ -14,7 +13,6 @@ using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
using Content.Server.StationEvents.Components;
using Content.Server.Speech.Components;
using Content.Shared._DV.Psionics.Components; // DeltaV
using Content.Shared.Body.Components;
using Content.Shared.CombatMode;
using Content.Shared.CombatMode.Pacification;
@ -27,7 +25,6 @@ using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.NPC.Components; // DeltaV
using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.AnimalHusbandry;
using Content.Shared.Nutrition.Components;
@ -72,7 +69,6 @@ public sealed partial class ZombieSystem
[Dependency] private readonly NPCSystem _npc = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly PsionicSystem _psionic = default!; // DeltaV
private static readonly ProtoId<TagPrototype> InvalidForGlobalSpawnSpellTag = "InvalidForGlobalSpawnSpell";
private static readonly ProtoId<TagPrototype> CannotSuicideTag = "CannotSuicide";
@ -134,6 +130,9 @@ public sealed partial class ZombieSystem
//you're a real zombie now, son.
var zombiecomp = AddComp<ZombieComponent>(target);
// DeltaV - Save factions, psionics, etc. before modifications are made.
PreserveEntityComponentState((target, zombiecomp));
//we need to basically remove all of these because zombies shouldn't
//get diseases, breath, be thirst, be hungry, die in space, get double sentience, have offspring or be paraplegic.
RemComp<RespiratorComponent>(target);
@ -146,12 +145,6 @@ public sealed partial class ZombieSystem
RemComp<ComplexInteractionComponent>(target);
RemComp<SentienceTargetComponent>(target);
// DeltaV Start - Prevent Psionic Zombies
RemComp<PotentialPsionicComponent>(target);
if (HasComp<PsionicComponent>(target))
_psionic.MindBreakEntity(target, false, true);
// DeltaV End - Prevent Psionic Zombies
//funny voice
var accentType = "zombie";
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
@ -245,6 +238,9 @@ public sealed partial class ZombieSystem
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
_inventory.TryUnequip(target, "ears", true, true);
// DeltaV - Extra zombification removals, etc.
ZombifyEntityDV((target, zombiecomp), mobState);
//popup
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
@ -261,7 +257,6 @@ public sealed partial class ZombieSystem
_faction.ClearFactions(target, dirty: false);
_faction.AddFaction(target, ZombieFaction);
EnsureComp<NoFriendlyFireComponent>(target); // DeltaV - prevent shitters biting other zombies
//gives it the funny "Zombie ___" name.
_nameMod.RefreshNameModifiers(target);

View File

@ -78,6 +78,8 @@ namespace Content.Server.Zombies
SubscribeLocalEvent<IncurableZombieComponent, MapInitEvent>(OnPendingMapInit);
SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
InitializeDV(); // DeltaV - Additional Subscriptions for ZombieComponent
}
private void OnBeforeRemoveAnomalyOnDeath(Entity<PendingZombieComponent> ent, ref BeforeRemoveAnomalyOnDeathEvent args)
@ -325,7 +327,7 @@ namespace Content.Server.Zombies
// Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
{
_role.MindRemoveRole<ZombieRoleComponent>((args.Mind.Owner, args.Mind.Comp));
_role.MindRemoveRole<ZombieRoleComponent>((args.Mind.Owner, args.Mind.Comp));
}
}
}

View File

@ -0,0 +1,73 @@
using Content.Shared.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Map;
namespace Content.Server.Audio;
/// <summary>
/// Extends upstream's Content.Server/Audio/ServerGlobalSoundSystem.cs.
/// </summary>
public sealed partial class ServerGlobalSoundSystem
{
/// <summary>
/// DeltaV - Plays a sound globally for all players, no matter where they are.
/// </summary>
/// <param name="specifier"></param>
/// <param name="audioParams"></param>
public void PlayGlobal(ResolvedSoundSpecifier specifier, AudioParams? audioParams = null)
{
var msg = new GameGlobalSoundEvent(specifier, audioParams);
RaiseNetworkEvent(msg);
}
public void StopGlobalEventMusic(StationEventMusicType type)
{
var msg = new StopStationEventMusic(type);
RaiseNetworkEvent(msg);
}
public void DispatchGlobalEventMusic(SoundSpecifier sound, StationEventMusicType type)
{
DispatchGlobalEventMusic(_audio.ResolveSound(sound), type);
}
public void DispatchGlobalEventMusic(ResolvedSoundSpecifier specifier, StationEventMusicType type)
{
var audio = AudioParams.Default.WithVolume(-8);
var msg = new StationEventMusicEvent(specifier, type, audio);
RaiseNetworkEvent(msg);
}
/// <summary>
/// DeltaV - Plays a sound globally for all players on a specified Map.
/// </summary>
/// <param name="specifier"></param>
/// <param name="audioParams"></param>
public void PlayGlobalOnMap(MapId map, ResolvedSoundSpecifier specifier, AudioParams? audioParams = null)
{
var msg = new GameGlobalSoundEvent(specifier, audioParams);
var filter = Filter.Empty().AddInMap(map);
RaiseNetworkEvent(msg, filter);
}
public void StopMapEventMusic(MapId map, StationEventMusicType type)
{
var msg = new StopStationEventMusic(type);
var filter = Filter.Empty().AddInMap(map);
RaiseNetworkEvent(msg, filter);
}
public void DispatchMapEventMusic(MapId map, SoundSpecifier sound, StationEventMusicType type)
{
DispatchMapEventMusic(map, _audio.ResolveSound(sound), type);
}
public void DispatchMapEventMusic(MapId map, ResolvedSoundSpecifier specifier, StationEventMusicType type)
{
var audio = AudioParams.Default.WithVolume(-8);
var msg = new StationEventMusicEvent(specifier, type, audio);
var filter = Filter.Empty().AddInMap(map);
RaiseNetworkEvent(msg, filter);
}
}

View File

@ -6,12 +6,6 @@ namespace Content.Server._DV.CartridgeLoader.Cartridges;
[RegisterComponent, Access(typeof(NanoChatCartridgeSystem))]
public sealed partial class NanoChatCartridgeComponent : Component
{
/// <summary>
/// Station entity to keep track of.
/// </summary>
[DataField]
public EntityUid? Station;
/// <summary>
/// The NanoChat card to keep track of.
/// </summary>

View File

@ -2,9 +2,8 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.CartridgeLoader;
using Content.Server.Power.Components;
using Content.Server.Radio;
using Content.Server.Station.Systems;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Access.Components;
using Content.Shared.CartridgeLoader;
using Content.Shared.CCVar;
@ -30,7 +29,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly SharedNanoChatSystem _nanoChat = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly RadioSystem _radio = default!;
private EntityQuery<PdaComponent> _pdaQuery;
private EntityQuery<NanoChatCardComponent> _cardQuery;
@ -456,9 +455,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
{
// First verify we can send from this device
var channel = _prototype.Index(sender.Comp.RadioChannel);
var sendAttemptEvent = new RadioSendAttemptEvent(channel, sender);
RaiseLocalEvent(ref sendAttemptEvent);
if (sendAttemptEvent.Cancelled)
if (!CanSend(sender))
return (true, new List<Entity<NanoChatCardComponent>>());
var foundRecipients = new List<Entity<NanoChatCardComponent>>();
@ -487,26 +484,19 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
if (receiverCart.Card != recipient.Owner)
continue;
// Check if devices are on same station/map
var recipientStation = _station.GetOwningStation(receiverUid);
var senderStation = _station.GetOwningStation(sender);
var receiverMapId = Transform(receiverUid).MapID;
var senderMapId = Transform(sender).MapID;
// Both entities must be on a station
if (recipientStation == null || senderStation == null)
// Must be on the same map unless long range is allowed.
if (!channel.LongRange && receiverMapId != senderMapId)
continue;
// Must be on same map/station unless long range allowed
if (!channel.LongRange && recipientStation != senderStation)
continue;
// Needs telecomms
if (!HasActiveServer(senderStation.Value) || !HasActiveServer(recipientStation.Value))
// Check if telecommunications are active on both ends
if (!_radio.HasActiveServer(receiverMapId, receiverCart.RadioChannel) || !_radio.HasActiveServer(senderMapId, sender.Comp.RadioChannel))
continue;
// Check if recipient can receive
var receiveAttemptEv = new RadioReceiveAttemptEvent(channel, sender, receiverUid);
RaiseLocalEvent(ref receiveAttemptEv);
if (receiveAttemptEv.Cancelled)
if (!CanReceive(sender, receiverUid))
continue;
// Found valid cartridge that can receive
@ -519,21 +509,26 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
}
/// <summary>
/// Checks if there are any active telecomms servers on the given station
/// Tests if a NanoChat cartridge can send messages
/// </summary>
private bool HasActiveServer(EntityUid station)
/// <param name="sender">The NanoChat cartridge trying to send</param>
private bool CanSend(Entity<NanoChatCartridgeComponent> sender)
{
// I have no idea why this isn't public in the RadioSystem
var query =
EntityQueryEnumerator<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent>();
var sendAttemptEvent = new RadioSendAttemptEvent(_prototype.Index(sender.Comp.RadioChannel), sender);
RaiseLocalEvent(ref sendAttemptEvent);
return !sendAttemptEvent.Cancelled;
}
while (query.MoveNext(out var uid, out _, out _, out var power))
{
if (_station.GetOwningStation(uid) == station && power.Powered)
return true;
}
return false;
/// <summary>
/// Tests if a receiver can receive from a given NanoChat cartridge
/// </summary>
/// <param name="sender">The NanoChat cartridge trying to send</param>
/// <param name="receiver">The receiver cartridge trying to receive</param>
private bool CanReceive(Entity<NanoChatCartridgeComponent> sender, EntityUid receiver)
{
var receiveAttemptEv = new RadioReceiveAttemptEvent(_prototype.Index(sender.Comp.RadioChannel), sender, receiver);
RaiseLocalEvent(ref receiveAttemptEv);
return !receiveAttemptEv.Cancelled;
}
/// <summary>
@ -688,16 +683,15 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
private void UpdateUI(Entity<NanoChatCartridgeComponent> ent, EntityUid loader)
{
List<NanoChatRecipient>? contacts;
if (_station.GetOwningStation(loader) is { } station)
{
ent.Comp.Station = station;
if (CanSend(ent) && _radio.HasActiveServer(Transform(ent).MapID, ent.Comp.RadioChannel))
{
contacts = [];
var query = AllEntityQuery<NanoChatCardComponent, IdCardComponent>();
while (query.MoveNext(out var entityId, out var nanoChatCard, out var idCardComponent))
{
if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && idCardComponent.FullName is string fullName && _station.GetOwningStation(entityId) == station)
if (nanoChatCard.ListNumber && nanoChatCard.Number is uint nanoChatNumber && idCardComponent.FullName is string fullName)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, fullName));
}
@ -706,7 +700,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
var borgQuery = AllEntityQuery<NanoChatCardComponent, BorgChassisComponent>();
while (borgQuery.MoveNext(out var borgId, out var borgChatCard, out var _))
{
if (borgChatCard.ListNumber && borgChatCard.Number is uint nanoChatNumber && _station.GetOwningStation(borgId) == station)
if (borgChatCard.ListNumber && borgChatCard.Number is uint nanoChatNumber)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, MetaData(borgId).EntityName));
}
@ -715,7 +709,7 @@ public sealed class NanoChatCartridgeSystem : EntitySystem
var aiQuery = AllEntityQuery<NanoChatCardComponent, StationAiHeldComponent>();
while (aiQuery.MoveNext(out var aiId, out var aiChatCard, out var _))
{
if (aiChatCard.ListNumber && aiChatCard.Number is uint nanoChatNumber && _station.GetOwningStation(aiId) == station)
if (aiChatCard.ListNumber && aiChatCard.Number is uint nanoChatNumber)
{
contacts.Add(new NanoChatRecipient(nanoChatNumber, MetaData(aiId).EntityName));
}

View File

@ -0,0 +1,63 @@
using Content.Server._DV.CosmicCult.Components;
using Content.Shared._DV.CosmicCult.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Weapons.Melee;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.CosmicCult.Abilities.Colossus;
public sealed class CosmicColossusBuffsSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damage = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MultiplyAttackRateOnSupercriticalComponent, CosmicColossusEffigySupercriticalEvent>(HandleAttackRate);
SubscribeLocalEvent<FlatAttackBonusOnSupercriticalComponent, CosmicColossusEffigySupercriticalEvent>(HandleDamageBonus);
SubscribeLocalEvent<MultiplyCorruptingSpeedOnSupercriticalComponent, CosmicColossusEffigySupercriticalEvent>(HandleCorruptingSpeed);
SubscribeLocalEvent<HealOnSupercriticalComponent, CosmicColossusEffigySupercriticalEvent>(HandleHeal);
}
private void HandleAttackRate(Entity<MultiplyAttackRateOnSupercriticalComponent> ent,
ref CosmicColossusEffigySupercriticalEvent args)
{
if (TryComp<MeleeWeaponComponent>(ent, out var weapon))
{
weapon.AttackRate *= ent.Comp.Multiplier;
}
}
private void HandleDamageBonus(Entity<FlatAttackBonusOnSupercriticalComponent> ent,
ref CosmicColossusEffigySupercriticalEvent args)
{
if (!TryComp<CosmicColossusComponent>(ent, out var colossusComp))
return;
colossusComp.BonusDamage +=
new DamageSpecifier(_proto.Index(ent.Comp.BonusDamageType), ent.Comp.BonusDamage);
}
private void HandleCorruptingSpeed(Entity<MultiplyCorruptingSpeedOnSupercriticalComponent> ent,
ref CosmicColossusEffigySupercriticalEvent args)
{
if (TryComp<CosmicCorruptingComponent>(ent, out var corrupting))
{
corrupting.CorruptionSpeed *= ent.Comp.Multiplier;
}
}
private void HandleHeal(Entity<HealOnSupercriticalComponent> ent, ref CosmicColossusEffigySupercriticalEvent args)
{
if (TryComp<DamageableComponent>(ent, out var damageable))
{
_damage.TryChangeDamage(ent.Owner,
-damageable.Damage * Math.Abs(ent.Comp.DamageFractionHealed), // just in case someone sets a negative value in YML
true);
}
}
}

View File

@ -0,0 +1,8 @@
using Content.Server._DV.CosmicCult.Components;
namespace Content.Server._DV.CosmicCult.Abilities.Colossus;
[ByRefEvent]
public readonly record struct CosmicColossusEffigySupercriticalEvent(
Entity<CosmicEffigyComponent> Effigy
);

View File

@ -1,15 +1,24 @@
using System.Numerics;
using Content.Server._DV.CosmicCult.Abilities.Colossus;
using Content.Server._DV.CosmicCult.Components;
using Content.Server.Actions;
using Content.Server.Chat.Managers;
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Server.Popups;
using Content.Shared._DV.CosmicCult;
using Content.Shared._DV.CosmicCult.Components;
using Content.Shared.Anomaly.Components;
using Content.Shared.Maps;
using Content.Shared.Mind;
using Content.Shared.Popups;
using Content.Shared.Warps;
using Robust.Server.Audio;
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server._DV.CosmicCult.Abilities;
@ -23,12 +32,93 @@ public sealed class CosmicEffigySystem : EntitySystem
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly CosmicCultObjectiveSystem _cultObjective = default!;
[Dependency] private readonly IGameTiming _time = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CosmicColossusComponent, EventCosmicColossusEffigy>(OnColossusEffigy);
SubscribeLocalEvent<CosmicEffigyComponent, AnomalySupercriticalEvent>(OnSupercritical);
SubscribeLocalEvent<CosmicEffigyComponent, AnomalyShutdownEvent>(OnAnomShutdown);
}
private void OnAnomShutdown(Entity<CosmicEffigyComponent> ent, ref AnomalyShutdownEvent args)
{
if (args.Forced || args.Supercritical || !Exists(ent.Comp.Colossus) || !TryComp<CosmicColossusComponent>(ent.Comp.Colossus, out var colossusComp))
return;
colossusComp.DeathTimer = _time.CurTime;
colossusComp.Timed = true;
}
private void OnSupercritical(Entity<CosmicEffigyComponent> ent, ref AnomalySupercriticalEvent args)
{
if (!Exists(ent.Comp.Colossus)
|| !TryComp<CosmicColossusComponent>(ent.Comp.Colossus, out var colossusComp)
|| !_mind.TryGetMind(ent.Comp.Colossus.Value, out _, out var mind))
return;
var colossus = ent.Comp.Colossus.Value;
var ev = new CosmicColossusEffigySupercriticalEvent(ent);
RaiseLocalEvent(colossus, ref ev);
var transform = Transform(ent.Comp.Colossus.Value);
Spawn(colossusComp.BuffVfx, transform.Coordinates);
if (colossusComp.CompletedEffigies == 0)
{
_audio.PlayStatic(colossusComp.ReawakenSfx,
Filter.BroadcastMap(transform.MapID),
transform.Coordinates,
true);
}
else
{
_audio.PlayPvs(colossusComp.ReawakenSfx, ent);
}
colossusComp.CompletedEffigies += 1;
if (colossusComp.CompletedEffigies >= colossusComp.MaxEffigies)
{
colossusComp.Timed = false;
_popup.PopupEntity(Loc.GetString("colossus-buff-final-popup"), colossus, PopupType.Large);
return;
}
_popup.PopupEntity(Loc.GetString("colossus-buff-popup"), colossus, PopupType.Large);
var objIndex = mind.Objectives.FindIndex(HasComp<CosmicEffigyConditionComponent>);
if (objIndex == -1 ||
!TryComp<CosmicEffigyConditionComponent>(mind.Objectives[objIndex], out var conditionComp))
{
Log.Error($"Failed to find effigy objective on {ToPrettyString(colossus)}!");
return;
}
var objective = mind.Objectives[objIndex];
if (_cultObjective.RandomizeEffigyTarget(objective, conditionComp, setDescription: true) is not {} nextTarget)
{
Log.Error("Failed to randomize effigy objective location!");
return;
}
if (mind.UserId is { } userId)
{
_chat.DispatchServerMessage(_player.GetSessionById(userId), Loc.GetString("colossus-next-target", ("location", nextTarget)));
}
_codeCondition.SetCompleted(objective, false);
colossusComp.EffigyPlaceActionEntity = _actions.AddAction(colossus, colossusComp.EffigyPlaceAction);
colossusComp.DeathTimer = _time.CurTime + colossusComp.DeathWaitEffigy;
colossusComp.Timed = true;
}
private void OnColossusEffigy(Entity<CosmicColossusComponent> ent, ref EventCosmicColossusEffigy args)
@ -38,8 +128,16 @@ public sealed class CosmicEffigySystem : EntitySystem
_actions.RemoveAction(ent.Owner, ent.Comp.EffigyPlaceActionEntity);
_codeCondition.SetCompleted(ent.Owner, ent.Comp.EffigyObjective);
Spawn(ent.Comp.EffigyPrototype, pos);
var effigy = Spawn(ent.Comp.EffigyPrototype, pos);
ent.Comp.Timed = false;
if (!TryComp<CosmicEffigyComponent>(effigy, out var effigyComp))
{
Log.Error("Colossus tried to place Effigy prototype missing CosmicEffigyComponent!");
return;
}
effigyComp.Colossus = ent.Owner;
}
private bool VerifyPlacement(Entity<CosmicColossusComponent> ent, out EntityCoordinates outPos)

View File

@ -0,0 +1,56 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.CosmicCult.Components;
/// <summary>
/// The owning colossus's attack rate will be multiplied by
/// the given multiplier once its effigy goes supercritical.
/// Component meant to be applied to Cosmic Colossus.
/// </summary>
[RegisterComponent]
public sealed partial class MultiplyAttackRateOnSupercriticalComponent : Component
{
[DataField]
public float Multiplier = 1.1f;
}
/// <summary>
/// The owning colossus's corrupting speed will be multiplied by
/// the given multiplier once its effigy goes supercritical.
/// Component meant to be applied to Cosmic Colossus.
/// </summary>
[RegisterComponent]
public sealed partial class MultiplyCorruptingSpeedOnSupercriticalComponent : Component
{
[DataField]
public float Multiplier = 0.9f;
}
/// <summary>
/// The owning colossus will receive a flat bonus to
/// its melee attack damage once its effigy goes supercritical.
/// Component meant to be applied to Cosmic Colossus.
/// </summary>
[RegisterComponent]
public sealed partial class FlatAttackBonusOnSupercriticalComponent : Component
{
[DataField]
public FixedPoint2 BonusDamage = 10;
[DataField]
public ProtoId<DamageTypePrototype> BonusDamageType = "Blunt";
}
/// <summary>
/// The owning colossus will be healed for `-CurrentDamage * Abs(DamageFractionHealed)` damage
/// once its effigy goes supercritical.
/// Component meant to be applied to Cosmic Colossus.
/// </summary>
[RegisterComponent]
public sealed partial class HealOnSupercriticalComponent : Component
{
[DataField]
public float DamageFractionHealed = 1.0f;
}

View File

@ -1,3 +1,4 @@
using Content.Server._DV.CosmicCult.Abilities.Colossus;
using Content.Server._DV.CosmicCult.EntitySystems;
using Content.Shared.Maps;
using Robust.Shared.Prototypes;
@ -5,7 +6,7 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server._DV.CosmicCult.Components;
[RegisterComponent, Access(typeof(CosmicCorruptingSystem))]
[RegisterComponent, Access(typeof(CosmicCorruptingSystem), typeof(CosmicColossusBuffsSystem))]
[AutoGenerateComponentPause]
public sealed partial class CosmicCorruptingComponent : Component
{

View File

@ -0,0 +1,19 @@
using Content.Server._DV.CosmicCult.Abilities;
using Content.Server._DV.CosmicCult.EntitySystems;
namespace Content.Server._DV.CosmicCult.Components;
/// <summary>
/// Component for cosmic effigy anomalies, spawned by Cosmic Colossi.
/// </summary>
/// <seelso cref="CosmicEffigySystem"/>.
/// <seelso cref="CosmicColossusSystem"/>.
[RegisterComponent]
public sealed partial class CosmicEffigyComponent : Component
{
/// <summary>
/// The colossus that placed this effigy.
/// </summary>
[DataField]
public EntityUid? Colossus;
}

View File

@ -35,6 +35,12 @@ public sealed class CosmicCultObjectiveSystem : EntitySystem
if (args.Cancelled || !_roles.MindHasRole<CosmicColossusRoleComponent>(args.MindId))
return;
if (RandomizeEffigyTarget(uid, comp) is null)
args.Cancelled = true;
}
public string? RandomizeEffigyTarget(EntityUid uid, CosmicEffigyConditionComponent comp, bool setDescription = false)
{
var warps = new List<EntityUid>();
var query = EntityQueryEnumerator<WarpPointComponent>();
var effigyBlacklist = comp.Blacklist;
@ -51,10 +57,22 @@ public sealed class CosmicCultObjectiveSystem : EntitySystem
if (warps.Count <= 0)
{
args.Cancelled = true;
return;
return null;
}
comp.EffigyTarget = _random.Pick(warps);
var newWarp = _random.Pick(warps);
var warpComp = Comp<WarpPointComponent>(newWarp);
comp.EffigyTarget = newWarp;
if (setDescription)
{
_metaData.SetEntityDescription(uid,
warpComp.Location != null
? Loc.GetString("objective-condition-effigy", ("location", warpComp.Location))
: Loc.GetString("objective-condition-effigy-no-target"));
}
return warpComp.Location;
}
private void OnEffigyAfterAssign(EntityUid uid, CosmicEffigyConditionComponent comp, ref ObjectiveAfterAssignEvent args)

View File

@ -521,7 +521,7 @@ public sealed class CosmicCultRuleSystem : GameRuleSystem<CosmicCultRuleComponen
{
finComp.CurrentState = FinaleState.Unavailable;
_popup.PopupCoordinates(Loc.GetString("cosmiccult-monument-powerdown"), Transform(gameruleMonument).Coordinates, PopupType.Large);
_sound.StopStationEventMusic(gameruleMonument, StationEventMusicType.CosmicCult);
_sound.StopGlobalEventMusic(StationEventMusicType.CosmicCult);
_monument.UpdateMonumentAppearance(gameruleMonument, false);
}

View File

@ -82,11 +82,9 @@ public sealed partial class CosmicCultSystem : SharedCosmicCultSystem
_corrupting.SetCorruptionTime((uid, corruptingComp), TimeSpan.FromSeconds(1));
_appearance.SetData(uid, MonumentVisuals.FinaleReached, 2);
_sound.DispatchStationEventMusic(uid, comp.SelectedSong, StationEventMusicType.CosmicCult);
_chatSystem.DispatchStationAnnouncement(uid,
Loc.GetString("cosmiccult-finale-location", ("location", indicatedLocation)),
null, false, null,
Color.FromHex("#cae8e8"));
_sound.DispatchGlobalEventMusic(comp.SelectedSong, StationEventMusicType.CosmicCult);
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("cosmiccult-finale-location", ("location", indicatedLocation)),
null, false, null, Color.FromHex("#cae8e8"));
var stationUid = _station.GetStationInMap(Transform(uid).MapID);
if (stationUid != null)
@ -128,8 +126,8 @@ public sealed partial class CosmicCultSystem : SharedCosmicCultSystem
if (stationUid != null)
_alert.SetLevel(stationUid.Value, "green", true, true, true);
_sound.PlayGlobalOnStation(uid, _audio.ResolveSound(comp.CancelEventSound));
_sound.StopStationEventMusic(uid, StationEventMusicType.CosmicCult);
_sound.PlayGlobal(_audio.ResolveSound(comp.CancelEventSound));
_sound.StopGlobalEventMusic(StationEventMusicType.CosmicCult);
uid.Comp.CurrentState = FinaleState.ReadyFinale;
if (TryComp<CosmicCorruptingComponent>(uid, out var corruptingComp))

View File

@ -8,6 +8,7 @@ using Content.Shared.Mind;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@ -25,6 +26,7 @@ public sealed class CosmicChantrySystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _role = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly NavMapSystem _navMap = default!;
/// <summary>
@ -79,11 +81,11 @@ public sealed class CosmicChantrySystem : EntitySystem
comp.SpawnTimer = _timing.CurTime + comp.SpawningTime;
comp.CountdownTimer = _timing.CurTime + comp.EventTime;
_sound.PlayGlobalOnStation(ent, _audio.ResolveSound(comp.ChantryAlarm));
_chatSystem.DispatchStationAnnouncement(ent,
Loc.GetString("cosmiccult-chantry-location", ("location", indicatedLocation)),
null, false, null,
Color.FromHex("#cae8e8"));
var targetMap = _transform.GetMapId(ent.Owner);
_sound.PlayGlobalOnMap(targetMap, _audio.ResolveSound(comp.ChantryAlarm));
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("cosmiccult-chantry-location",
("location", indicatedLocation)), null, false, null, Color.FromHex("#cae8e8"));
if (_mind.TryGetMind(comp.InternalVictim, out _, out var mind))
mind.PreventGhosting = true;

View File

@ -13,6 +13,7 @@ using Content.Shared.Popups;
using Content.Shared.Station.Components;
using Content.Shared.Throwing;
using Content.Shared.Warps;
using Content.Shared.Weapons.Melee.Events;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Physics.Components;
@ -41,6 +42,7 @@ public sealed class CosmicColossusSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<CosmicColossusComponent, ComponentInit>(OnSpawn);
SubscribeLocalEvent<CosmicColossusComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<CosmicColossusComponent, MeleeHitEvent>(OnMeleeHit);
}
public override void Update(float frameTime)
@ -82,7 +84,7 @@ public sealed class CosmicColossusSystem : EntitySystem
private void OnSpawn(Entity<CosmicColossusComponent> ent, ref ComponentInit args) // I WANT THIS BIG GUY HURLED TOWARDS THE STATION
{
ent.Comp.DeathTimer = _timing.CurTime + ent.Comp.DeathWait;
ent.Comp.DeathTimer = _timing.CurTime + ent.Comp.DeathWaitSpawn;
var station = _station.GetStationInMap(Transform(ent).MapID);
if (TryComp<StationDataComponent>(station, out var stationData))
{
@ -115,4 +117,9 @@ public sealed class CosmicColossusSystem : EntitySystem
RemComp<WarpPointComponent>(ent);
RemComp<CosmicCorruptingComponent>(ent);
}
private void OnMeleeHit(Entity<CosmicColossusComponent> colossus, ref MeleeHitEvent args)
{
args.BonusDamage += colossus.Comp.BonusDamage;
}
}

View File

@ -79,13 +79,13 @@ public sealed class MonumentSystem : SharedMonumentSystem
{
comp.SongTimer = null;
if (comp.SelectedSong is { } song)
_sound.DispatchStationEventMusic(uid, song, StationEventMusicType.CosmicCult);
_sound.DispatchGlobalEventMusic(song, StationEventMusicType.CosmicCult);
}
if (comp.CurrentState == FinaleState.ActiveFinale && comp.FinaleAnnounceCheck && comp.FinaleTimer - _timing.CurTime < comp.VisualsThreshold)
{
_appearance.SetData(uid, MonumentVisuals.FinaleReached, 3);
_chatSystem.DispatchStationAnnouncement(uid, Loc.GetString("cosmiccult-announce-finale-warning"), null, false, null, Color.FromHex("#cae8e8"));
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("cosmiccult-announce-finale-warning"), null, false, null, Color.FromHex("#cae8e8"));
comp.FinaleAnnounceCheck = false;
}

View File

@ -1,5 +1,6 @@
using Content.Server._DV.Psionics.Systems;
using Content.Shared._DV.Psionics.Components;
using Content.Shared._DV.Traits.Assorted;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
@ -17,10 +18,12 @@ public sealed partial class ParadoxCloneRuleSystem
private void FilterTargets(HashSet<Entity<MindComponent>> minds)
{
// TODO: use generic IMindFilter
// no picking other antags or non-crew
// no picking other antags or non-crew and entities with no paradox clone trait
minds.RemoveWhere(mind => _role.MindIsAntagonist(mind) ||
!_role.MindHasRole<JobRoleComponent>((mind, mind), out var role) ||
role?.Comp1.JobPrototype == null);
role?.Comp1.JobPrototype == null ||
(mind.Comp.OwnedEntity is { } entity && HasComp<NoParadoxCloneComponent>(entity))
);
}
private void PostClone(EntityUid mob)

View File

@ -6,7 +6,6 @@ using Content.Shared._DV.Kitchen.Systems;
using Content.Shared.Audio;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Power;
@ -107,6 +106,7 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
private void OnPowerChanged(Entity<DeepFryerComponent> ent, ref PowerChangedEvent args)
{
UpdateAppearance(ent);
ResetCookingItemsStartTime(ent);
UpdateUserInterfaceState(ent);
}
@ -718,4 +718,26 @@ public sealed class DeepFryerSystem : SharedDeepFryerSystem
// Add the split solution to the food
Solution.AddSolution(foodSolutionEnt.Value, transferredSolution);
}
/// <summary>
/// Resets the starttime for cooking items (on power change)
/// </summary>
private void ResetCookingItemsStartTime(Entity<DeepFryerComponent> ent)
{
if (_power.IsPowered(ent.Owner) && !ent.Comp.WasPreviouslyPowered)
{
// Power just came back on - reset all cooking timers
foreach (var itemUid in ent.Comp.CookingItems.Keys.ToList())
{
var cookingItem = ent.Comp.CookingItems[itemUid];
cookingItem.TimeStarted = _timing.CurTime;
ent.Comp.CookingItems[itemUid] = cookingItem;
}
ent.Comp.WasPreviouslyPowered = true;
}
else if (!_power.IsPowered(ent.Owner) && ent.Comp.WasPreviouslyPowered)
{
ent.Comp.WasPreviouslyPowered = false;
}
}
}

View File

@ -1,212 +0,0 @@
using Content.Server.Cloning;
using Content.Server.Mind;
using Content.Server.Station.Systems;
using Content.Shared._DV.Psionics.Components;
using Content.Shared._DV.Psionics.Components.PsionicPowers;
using Content.Shared._DV.Psionics.Systems.PsionicPowers;
using Content.Shared._DV.Species;
using Content.Shared.Bed.Sleep;
using Content.Shared.Body.Components;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.Preferences;
using Robust.Server.GameObjects;
using Robust.Server.GameStates;
using Robust.Server.Player;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server._DV.Psionics.Systems.PsionicPowers;
public sealed class FracturedFormPowerSystem : SharedFracturedFormPowerSystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly CloningSystem _cloning = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly PvsOverrideSystem _pvsOverride = default!;
// holy initialize performance? but better for it to happen once than the double dict lookup every tick!!
private EntityQuery<FracturedFormBodyComponent> _bodyQuery;
private EntityQuery<SleepingComponent> _sleepingQuery;
public override void Initialize()
{
base.Initialize();
_bodyQuery = GetEntityQuery<FracturedFormBodyComponent>();
_sleepingQuery = GetEntityQuery<SleepingComponent>();
}
protected override void OnPowerInit(Entity<FracturedFormPowerComponent> power, ref MapInitEvent args)
{
base.OnPowerInit(power, ref args);
// The next random swap is between 5 and 20 minutes.
var randomTime = Random.Next(power.Comp.NextSwapMinTime, power.Comp.NextSwapMaxTime);
power.Comp.NextSwap = Timing.CurTime + randomTime;
power.Comp.NextVoluntarySwap = Timing.CurTime + power.Comp.VoluntarySwapCooldown;
// Don't generate a new body if we're already part of a network.
if (HasComp<FracturedFormBodyComponent>(power))
return;
// Don't make bodies if there is no body. This is solely for test fails.
if (!HasComp<BodyComponent>(power))
return;
var bodyComp = AddComp<FracturedFormBodyComponent>(power);
bodyComp.ControllingForm = power.Owner;
power.Comp.Bodies.Add(power);
var body = GenerateForm(power);
// hide the SSD indicator.
if (SsdQuery.TryComp(body, out var ssdComp))
ssdComp.IsSSD = false;
}
private EntityUid GenerateForm(Entity<FracturedFormPowerComponent> original)
{
// Form:
// - Same appearance as original
// - Different apperance, still humanoid
// Equipment:
// - Same as original body
// - Nude and helpless
var xform = Transform(original);
var hasGear = Random.Prob(original.Comp.HasGearChance);
if (Random.Prob(original.Comp.DifferentSpeciesChance) || !_cloning.TryCloning(original, _transform.GetMapCoordinates(original), hasGear ? original.Comp.CopyClothed : original.Comp.CopyNaked, out var newBody)) // Slightly lower chance to copy the original body
{
// Either the dice rolled poorly, or the cloning failed. Either way, make a new body instead. (Or try to)
var validSpecies = new List<ProtoId<SpeciesPrototype>>();
var speciesPrototypes = _prototype.EnumeratePrototypes<SpeciesPrototype>();
foreach (var proto in speciesPrototypes)
{
var speciesEntityPrototype = _prototype.Index<EntityPrototype>(proto.Prototype);
// If they have the PotentialPsionicComponent, they can be psionic.
if (proto.RoundStart && speciesEntityPrototype.TryGetComponent<PotentialPsionicComponent>(out _, Factory) && !SpeciesHiderSystem.IsHidden(proto.ID))
validSpecies.Add(proto.ID);
}
var species = Random.Pick(validSpecies);
var character = HumanoidCharacterProfile.RandomWithSpecies(species);
newBody = _stationSpawning.SpawnPlayerMob(xform.Coordinates, hasGear ? original.Comp.VisitorJob : original.Comp.NakedJob, character, _station.GetOwningStation(original.Owner));
if (newBody is not { } bodyV || Deleted(bodyV))
{
Log.Error($"Failed to create a new body for {ToPrettyString(original)}. This is a bug.");
return EntityUid.Invalid;
}
}
if (newBody is not { } body || Deleted(body))
return default!;
var bodyComp = AddComp<FracturedFormBodyComponent>(body);
original.Comp.Bodies.Add(body);
bodyComp.ControllingForm = original.Owner;
if (_player.TryGetSessionByEntity(original, out var session))
_pvsOverride.AddSessionOverride(body, session);
Dirty(original);
return body;
}
private bool TryGetValidBody(Entity<FracturedFormPowerComponent> psionic, out EntityUid validBody)
{
foreach (var body in psionic.Comp.Bodies)
{
if (!IsValidBody(psionic, body))
continue;
validBody = body;
return true;
}
validBody = default;
return false;
}
private void Swap(Entity<FracturedFormPowerComponent> psionic)
{
if (!TryGetValidBody(psionic, out var targetBody))
return;
_audio.PlayPvs(psionic.Comp.SwapSound, psionic);
// Transfer mind if present
if (MindContainerQuery.TryComp(psionic, out var mindContainer) && mindContainer.Mind.HasValue)
_mind.TransferTo(mindContainer.Mind.Value, targetBody);
// Wake up the new body
Sleeping.TryWaking(targetBody);
// Remove the action.
Action.RemoveAction(psionic.Comp.ActionEntity);
// Create new component on target and copy data
var duplicate = EnsureComp<FracturedFormPowerComponent>(targetBody);
duplicate.Bodies = psionic.Comp.Bodies;
// Update all body references
foreach (var body in duplicate.Bodies)
{
if (_bodyQuery.TryComp(body, out var bodyComp))
bodyComp.ControllingForm = targetBody;
}
if (_player.TryGetSessionByEntity(targetBody, out var session))
{
_pvsOverride.AddSessionOverride(psionic, session);
_pvsOverride.RemoveSessionOverride(targetBody, session);
}
RemCompDeferred<FracturedFormPowerComponent>(psionic);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
List<Entity<FracturedFormPowerComponent>> swapTargets = [];
var entities = EntityQueryEnumerator<FracturedFormPowerComponent, MobStateComponent>();
while (entities.MoveNext(out var uid, out var comp, out var mobState))
{
// Check sleep warning
if (!comp.SleepWarned && Timing.CurTime > comp.NextSwap - comp.WarningTimeBeforeSleep)
{
comp.SleepWarned = true;
Popup.PopupEntity(Loc.GetString("psionic-power-fractured-form-sleepy"), uid, uid, PopupType.LargeCaution);
Chat.TryEmoteWithChat(uid, "Yawn");
}
// Swap check
if ((_sleepingQuery.HasComp(uid) || MobState.IsIncapacitated(uid, mobState)) && Timing.CurTime > comp.NextVoluntarySwap
|| Timing.CurTime > comp.NextSwap)
swapTargets.Add((uid, comp));
}
foreach (var target in swapTargets)
{
Swap(target);
}
// Process bodies
var bodies = EntityQueryEnumerator<FracturedFormBodyComponent, MobStateComponent>();
while (bodies.MoveNext(out var uid, out var comp, out var mobState))
{
// Put to sleep if no sleeping component and no mind
if (!_sleepingQuery.HasComp(uid) && !_mind.GetMind(uid).HasValue && !FracturedQuery.HasComp(uid))
Sleeping.TrySleeping((uid, mobState));
// Cleanup invalid bodies
if (!comp.ControllingForm.IsValid()
|| Deleted(comp.ControllingForm)
|| !FracturedQuery.HasComp(comp.ControllingForm))
{
RemCompDeferred<FracturedFormBodyComponent>(uid);
}
}
}
}

View File

@ -0,0 +1,39 @@
using Content.Server.Wires;
using Content.Shared._DV.Silicons.Borgs;
using Content.Shared._DV.Silicons;
using Content.Shared.Wires;
namespace Content.Server._DV.Silicons;
/// <summary>
/// Controls whether the cyborg's ID chip slot is active
/// </summary>
public sealed partial class BorgIdChipWireAction : ComponentWireAction<IdChipSlotComponent>
{
public override string Name { get; set; } = "wire-name-borg-id-chip";
public override Color Color { get; set; } = Color.Thistle;
public override object StatusKey => BorgWireActionKey.ChipKey;
public override StatusLightState? GetLightState(Wire wire, IdChipSlotComponent component)
{
return component.Active ? StatusLightState.On : StatusLightState.Off;
}
public override bool Cut(EntityUid user, Wire wire, IdChipSlotComponent component)
{
EntityManager.System<IdChipSlotSystem>()
.SetActive((wire.Owner, component), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, IdChipSlotComponent component)
{
EntityManager.System<IdChipSlotSystem>()
.SetActive((wire.Owner, component), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, IdChipSlotComponent component)
{
}
}

View File

@ -0,0 +1,40 @@
using Content.Server.Silicons.Borgs;
using Content.Server.Wires;
using Content.Shared._DV.Silicons;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Wires;
namespace Content.Server._DV.Silicons;
/// <summary>
/// Controls whether the cyborg's transponder is active
/// </summary>
public sealed partial class BorgTransponderWireAction : ComponentWireAction<BorgTransponderComponent>
{
public override string Name { get; set; } = "wire-name-borg-transponder";
public override Color Color { get; set; } = Color.YellowGreen;
public override object StatusKey => BorgWireActionKey.TransponderKey;
public override StatusLightState? GetLightState(Wire wire, BorgTransponderComponent component)
{
return component.Active ? StatusLightState.On : StatusLightState.Off;
}
public override bool Cut(EntityUid user, Wire wire, BorgTransponderComponent component)
{
EntityManager.System<BorgSystem>()
.SetTransponderActive((wire.Owner, component), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, BorgTransponderComponent component)
{
EntityManager.System<BorgSystem>()
.SetTransponderActive((wire.Owner, component), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, BorgTransponderComponent component)
{
}
}

View File

@ -13,6 +13,7 @@ namespace Content.Server._DV.Silicons.Laws;
public sealed class SlavedBorgSystem : SharedSlavedBorgSystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedSiliconLawSystem _siliconLaws = default!;
public override void Initialize()
{
@ -24,12 +25,12 @@ public sealed class SlavedBorgSystem : SharedSlavedBorgSystem
private void OnGetSiliconLaws(Entity<SlavedBorgComponent> ent, ref GetSiliconLawsEvent args)
{
if (ent.Comp.Added || !TryComp<SiliconLawProviderComponent>(ent, out var provider))
if (ent.Comp.HasBeenAdded || !TryComp<SiliconLawProviderComponent>(ent, out var provider))
return;
if (provider.Lawset is {} lawset)
if (provider.Lawset is {} lawset && ent.Comp.ShouldBeAdded)
AddLaw(lawset, ent.Comp.Law);
ent.Comp.Added = true; // prevent opening the ui adding more law 0's
ent.Comp.HasBeenAdded = true; // prevent opening the ui adding more law 0's
}
/// <summary>
@ -39,4 +40,30 @@ public sealed class SlavedBorgSystem : SharedSlavedBorgSystem
{
lawset.Laws.Insert(0, _proto.Index(law).ShallowClone());
}
/// <summary>
/// Sets whether the slaving is active.
/// </summary>
public void SetShouldBeAdded(Entity<SlavedBorgComponent?> ent, bool active)
{
if (!Resolve(ent, ref ent.Comp))
return;
if (!TryComp<SiliconLawProviderComponent>(ent, out var provider) || provider.Lawset is not { } lawset)
return;
if (!ent.Comp.ShouldBeAdded && active)
{
AddLaw(lawset, ent.Comp.Law);
ent.Comp.HasBeenAdded = true;
_siliconLaws.NotifyLawsChanged(ent);
}
else if (ent.Comp.ShouldBeAdded && !active)
{
lawset.Laws.Remove(_proto.Index(ent.Comp.Law));
ent.Comp.HasBeenAdded = false;
_siliconLaws.NotifyLawsChanged(ent);
}
ent.Comp.ShouldBeAdded = active;
}
}

View File

@ -0,0 +1,40 @@
using Content.Server.Wires;
using Content.Shared._DV.Silicons;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Wires;
namespace Content.Server._DV.Silicons;
/// <summary>
/// Controls whether the power cell slot is active
/// </summary>
public sealed partial class PowerCellSlotWireAction : ComponentWireAction<PowerCellSlotComponent>
{
public override string Name { get; set; } = "wire-name-power-cell";
public override Color Color { get; set; } = Color.BurlyWood;
public override object StatusKey => BorgWireActionKey.CellKey;
public override StatusLightState? GetLightState(Wire wire, PowerCellSlotComponent component)
{
return component.Active ? StatusLightState.On : StatusLightState.Off;
}
public override bool Cut(EntityUid user, Wire wire, PowerCellSlotComponent component)
{
EntityManager.System<PowerCellSystem>()
.SetSlotActive((wire.Owner, component), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, PowerCellSlotComponent component)
{
EntityManager.System<PowerCellSystem>()
.SetSlotActive((wire.Owner, component), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, PowerCellSlotComponent component)
{
}
}

View File

@ -0,0 +1,41 @@
using Content.Server._DV.Silicons.Laws;
using Content.Server.Wires;
using Content.Shared.Doors;
using Content.Shared._DV.Silicons.Laws;
using Content.Shared._DV.Silicons;
using Content.Shared.Wires;
namespace Content.Server._DV.Silicons;
/// <summary>
/// Controls whether the cyborg's transponder is active
/// </summary>
public sealed partial class SlavedBorgWireAction : ComponentWireAction<SlavedBorgComponent>
{
public override string Name { get; set; } = "wire-name-slaved-borg";
public override Color Color { get; set; } = Color.Coral;
public override object StatusKey => BorgWireActionKey.SlavedKey;
public override StatusLightState? GetLightState(Wire wire, SlavedBorgComponent component)
{
return component.ShouldBeAdded ? StatusLightState.On : StatusLightState.Off;
}
public override bool Cut(EntityUid user, Wire wire, SlavedBorgComponent component)
{
EntityManager.System<SlavedBorgSystem>()
.SetShouldBeAdded((wire.Owner, component), false);
return true;
}
public override bool Mend(EntityUid user, Wire wire, SlavedBorgComponent component)
{
EntityManager.System<SlavedBorgSystem>()
.SetShouldBeAdded((wire.Owner, component), true);
return true;
}
public override void Pulse(EntityUid user, Wire wire, SlavedBorgComponent component)
{
}
}

View File

@ -1,9 +1,9 @@
using Content.Server.Power.Components;
using Content.Server.StationEvents.Events;
using Content.Server._DV.StationEvents.Events;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.StationEvents.Components;
namespace Content.Server._DV.StationEvents.Components;
/// <summary>
/// When fired, turns off power on the station for a few seconds, playing <see cref="EpsilonEventRuleComponent.PowerOffSound"/>

View File

@ -1,14 +1,15 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.StationEvents.Components;
using Content.Server.StationEvents.Events;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Server.AlertLevel;
using Content.Server.Audio;
using Content.Server._DV.StationEvents.Components;
using Robust.Shared.Audio.Systems;
namespace Content.Server.StationEvents.Events
{
namespace Content.Server._DV.StationEvents.Events;
public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleComponent>
{
[Dependency] private readonly ApcSystem _apcSystem = default!;
@ -25,7 +26,7 @@ public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleCompon
return;
component.AffectedStation = chosenStation.Value;
// Plays the power off sound to the station.
// Plays the power off sound for those who are on the station.
_sound.PlayGlobalOnStation(component.AffectedStation, _audio.ResolveSound(component.PowerOffSound));
var query = AllEntityQuery<ApcComponent, TransformComponent>();
@ -70,4 +71,3 @@ public sealed class EpsilonEventRule : StationEventSystem<EpsilonEventRuleCompon
_alertLevelSystem.SetLevel(component.AffectedStation, "epsilon", true, true, true);
}
}
}

View File

@ -109,7 +109,8 @@ public sealed class TraitSystem : EntitySystem
JobId = jobId,
SpeciesId = speciesId,
Profile = profile,
StatusEffects = _statusEffects
StatusEffects = _statusEffects,
SelectedTraits = selectedTraits
};
foreach (var traitId in selectedTraits)

View File

@ -0,0 +1,124 @@
using Content.Server._DV.Psionics.Systems;
using Content.Server.Antag;
using Content.Shared._DV.Movement.Components;
using Content.Shared._DV.Psionics.Components;
using Content.Shared.CharacterInfo;
using Content.Shared.Cloning.Events;
using Content.Shared.CombatMode.Pacification;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.NPC.Components;
using Content.Shared.Radio.Components;
using Content.Shared.Roles.Components;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
namespace Content.Server.Zombies;
public sealed partial class ZombieSystem
{
//private static readonly string MindRoleInitialInfected = "MindRoleInitialInfected";
private static readonly EntProtoId InitialInfectedFailureSurviveObjective = "InitialInfectedFailureSurviveObjective";
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly PsionicSystem _psionic = default!; // DeltaV
[Dependency] private readonly SharedJetpackSystem _jetpack = default!; // DeltaV - Prevent Jetpacks on Zombies
private void InitializeDV()
{
// This needs to be done before CloneEvent or else the proper attributes won't be restored or modified on
// the new entity.
SubscribeLocalEvent<ZombieComponent, CloningAttemptEvent>(OnBeforeUnzombify);
SubscribeLocalEvent<InitialInfectedComponent, CloningEvent>(OnInitialInfectedCloning);
}
/// <summary>
/// DeltaV - Extra things we want to ensure, remove, or take care of when someone is zombified.
/// </summary>
private void ZombifyEntityDV(Entity<ZombieComponent> ent, MobStateComponent? mobState = null)
{
// Remove innate radio
RemComp<ActiveRadioComponent>(ent); // If the zombie has an innate radio, get rid of it.
// Remove headsets in pockets
for (var i = 1; i <= 4; i++) // Arachnids have 4 pockets
{
if (_inventory.TryGetSlotEntity(ent, $"pocket{i}", out var headset) && HasComp<HeadsetComponent>(headset))
_inventory.TryUnequip(ent, $"pocket{i}", true, true);
}
// Prevent Psionic Zombies
RemComp<PotentialPsionicComponent>(ent);
_psionic.MindBreakEntity(ent.Owner, false, true);
// Prevent Jetpacks on Zombies
if (TryComp<JetpackUserComponent>(ent, out var jetpackUser))
{
if (TryComp<JetpackComponent>(jetpackUser.Jetpack, out var jetpack))
_jetpack.SetEnabled(jetpackUser.Jetpack, jetpack, false, ent);
}
RemComp<AutomaticJetpackUserComponent>(ent);
// Prevent shitters biting other zombies
EnsureComp<NoFriendlyFireComponent>(ent);
}
/// <summary>
/// DeltaV - We need to save some extra things to the ZombieComponent so we can restore them when
/// they are about to be cloned.
/// </summary>
/// <param name="ent"></param>
private void PreserveEntityComponentState(Entity<ZombieComponent> ent)
{
SaveFactionsBeforeZombification(ent);
// Zombification removes pacifism but lets track if they have it so the pacifist players will be... pacified.
ent.Comp.WasPacisfistBeforeZombification = HasComp<PacifiedComponent>(ent);
}
private void SaveFactionsBeforeZombification(Entity<ZombieComponent> ent)
{
if (!TryComp<NpcFactionMemberComponent>(ent, out var faction))
return;
ent.Comp.FactionsBeforeZombification.AddRange(faction.Factions);
}
/// <summary>
/// DeltaV - Restores certain components to their previous state that the upstream unzombify doesn't handle.
/// We need to do this before CloningEvent so the cloned body will have the updated components BEFORE cloning happens.
/// </summary>
/// <param name="ent"></param>
/// <param name="args"></param>
private void OnBeforeUnzombify(Entity<ZombieComponent> ent, ref CloningAttemptEvent args)
{
// Restore factions
_faction.ClearFactions(ent.Owner); // Should only be zombie, but might as well clear the whole list in case.
ent.Comp.FactionsBeforeZombification.ForEach(previousFaction => _faction.AddFaction(ent.Owner, previousFaction));
// Restore pacifism
if (ent.Comp.WasPacisfistBeforeZombification)
EnsureComp<PacifiedComponent>(ent);
}
/// <summary>
/// DeltaV - Adds a survival objective to Initial Infected who are cloned. Chances are that the crew has recovered, so
/// bioterrorism time is probably over.
/// </summary>
/// <param name="ent"></param>
/// <param name="args"></param>
private void OnInitialInfectedCloning(Entity<InitialInfectedComponent> ent, ref CloningEvent args)
{
// Change II objectives so they no longer have infect objectives. Now, they'll have survival objectives. Live to infect another day. :)
_mind.TryGetMind(ent.Owner, out var mindId, out var mindContainer);
// If we can't find the mind, oh well. We tried.
if (mindContainer is not { } mind)
return;
if (_role.MindHasRole<InitialInfectedRoleComponent>(mindId))
_mind.TryAddObjective(mindId, mind, InitialInfectedFailureSurviveObjective);
}
}

View File

@ -285,6 +285,11 @@ public sealed partial class AnomalyComponent : Component
[DataField]
public bool DeleteEntity = true;
// DeltaV - Colossus additions START
[DataField]
public bool AlwaysGrow;
// DeltaV - Colossus additions END
}
/// <summary>
@ -308,7 +313,7 @@ public readonly record struct AnomalySupercriticalEvent(EntityUid Anomaly, float
/// <param name="Anomaly">The anomaly being shut down.</param>
/// <param name="Supercritical">Whether or not the anomaly shut down passively or via a supercritical event.</param>
[ByRefEvent]
public readonly record struct AnomalyShutdownEvent(EntityUid Anomaly, bool Supercritical);
public readonly record struct AnomalyShutdownEvent(EntityUid Anomaly, bool Supercritical, bool Forced); // DeltaV - Add Forced
/// <summary>
/// Event broadcast when an anomaly's severity is changed.

View File

@ -84,7 +84,7 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
Log.Info($"Performing anomaly pulse. Entity: {ToPrettyString(uid)}");
// if we are above the growth threshold, then grow before the pulse
if (component.Stability > component.GrowthThreshold)
if (component.AlwaysGrow || component.Stability > component.GrowthThreshold) // DeltaV - Add AlwaysGrow
{
ChangeAnomalySeverity(uid, GetSeverityIncreaseFromGrowth(component), component);
}
@ -192,7 +192,8 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
/// <param name="supercritical">Whether or not the anomaly ended via supercritical event</param>
/// <param name="spawnCore">Create anomaly cores based on the result of completing an anomaly?</param>
/// <param name="logged">Whether or not the anomaly decaying/going supercritical is logged</param>
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true, bool logged = false)
/// <param name="forced">Whether or not the anomaly shutdown was caused by component shutdown</param> // DeltaV - Add forced
public void EndAnomaly(EntityUid uid, AnomalyComponent? component = null, bool supercritical = false, bool spawnCore = true, bool logged = false, bool forced = false) // DeltaV - Add forced
{
if (logged)
{
@ -207,7 +208,7 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
if (!Resolve(uid, ref component))
return;
var ev = new AnomalyShutdownEvent(uid, supercritical);
var ev = new AnomalyShutdownEvent(uid, supercritical, forced); // DeltaV - Add forced
RaiseLocalEvent(uid, ref ev, true);
if (Terminating(uid) || _net.IsClient)
@ -345,7 +346,7 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
// if the stability is under the death threshold,
// update it every second to start killing it slowly.
if (anomaly.Stability < anomaly.DecayThreshold)
if (!anomaly.AlwaysGrow && anomaly.Stability < anomaly.DecayThreshold) // DeltaV - Add AlwaysGrow
{
ChangeAnomalyHealth(ent, anomaly.HealthChangePerSecond * frameTime, anomaly);
}
@ -479,6 +480,14 @@ public abstract partial class SharedAnomalySystem : EntitySystem // DeltaV - Mad
if (!Resolve(ent, ref ent.Comp, logMissing: false))
return false;
// DeltaV - Colossus Additions START
if (ent.Comp.AlwaysGrow)
{
visual = AnomalyStabilityVisuals.Growing;
return true;
}
// DeltaV - Colossus Additions END
visual = AnomalyStabilityVisuals.Stable;
if (ent.Comp.Stability <= ent.Comp.DecayThreshold)
{

View File

@ -135,4 +135,10 @@ public sealed partial class CCVars
/// </summary>
public static readonly CVarDef<int> AdminOverlayStackMax =
CVarDef.Create("ui.admin_overlay_stack_max", 3, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// If true, ghosts will see an "(F)" button next to chat messages, which can be used to follow the sender.
/// </summary>
public static readonly CVarDef<bool> InterfaceChatFollowButton =
CVarDef.Create("ui.chat_follow_button", true, CVar.CLIENT | CVar.REPLICATED | CVar.ARCHIVE);
}

View File

@ -53,6 +53,20 @@ namespace Content.Shared.Chat
AudioPath = audioPath;
AudioVolume = audioVolume;
}
public ChatMessage(ChatMessage copyFrom)
{
Channel = copyFrom.Channel;
Message = copyFrom.Message;
WrappedMessage = copyFrom.WrappedMessage;
SenderEntity = copyFrom.SenderEntity;
SenderKey = copyFrom.SenderKey;
HideChat = copyFrom.HideChat;
MessageColorOverride = copyFrom.MessageColorOverride;
AudioPath = copyFrom.AudioPath;
AudioVolume = copyFrom.AudioVolume;
Read = copyFrom.Read;
}
}
/// <summary>

View File

@ -1,5 +1,6 @@
using System.Collections.Frozen;
using System.Collections.Immutable;
using Content.Shared._DV.Chat; // DeltaV - chat
using Content.Shared.Chat.Prototypes;
using Content.Shared.Speech;
using Robust.Shared.Audio;
@ -98,7 +99,7 @@ public abstract partial class SharedChatSystem
{
// not all emotes are loc'd, but for the ones that are we pass in entity
var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker);
SendEntityEmote(source, action, range, nameOverride, null, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker); // DeltaV
}
return didEmote;
@ -208,6 +209,48 @@ public abstract partial class SharedChatSystem
return true;
}
// DeltaV
/// <summary>
/// Checks which type of emote the message contains and returns the type while outputting the string without the prefixes.
/// </summary>
public static EmoteType? ProcessEmoteMessage(EntityUid source, string input, out string output)
{
output = input.Trim();
EmoteType? type = null;
if (input.Length == 0)
return type;
if (!(input.StartsWith(AudibleEmotePrefix) || input.StartsWith(PossessiveEmotePrefix)))
{
type = EmoteType.Normal;
return type;
}
var emoteType = input[0];
output = input[1..].TrimStart();
if (emoteType == AudibleEmotePrefix)
{
emoteType = input[1]; // Check for if we want AudiblePossessiveEmote
type = EmoteType.Audible;
}
if (emoteType == PossessiveEmotePrefix)
{
if (type == EmoteType.Audible)
{
type = EmoteType.AudiblePossessive;
output = input[2..].TrimStart();
}
else
type = EmoteType.Possessive;
}
return type;
}
// DeltaV - End
/// <summary>
/// Creates and raises <see cref="BeforeEmoteEvent"/> and then <see cref="EmoteEvent"/> to let other systems do things like play audio.
/// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player

View File

@ -1,5 +1,6 @@
using System.Collections.Frozen;
using System.Text.RegularExpressions;
using Content.Shared._DV.Chat;
using Content.Shared.ActionBlocker;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Popups;
@ -29,6 +30,8 @@ public abstract partial class SharedChatSystem : EntitySystem
public const char OOCPrefix = '[';
public const char EmotesPrefix = '@';
public const char EmotesAltPrefix = '*';
public const char AudibleEmotePrefix = '!'; // DeltaV - You may now scream audibly!
public const char PossessiveEmotePrefix = '\''; // DeltaV - You may now be possessive of things! Whatever that means.
public const char AdminPrefix = ']';
public const char WhisperPrefix = ',';
public const char TelepathicPrefix = '='; //Nyano - Summary: Adds the telepathic channel's prefix.
@ -152,6 +155,7 @@ public abstract partial class SharedChatSystem : EntitySystem
string input,
out string output,
out RadioChannelPrototype? channel,
bool capitalize = true, // DeltaV - We might not want to capitalize the first letter if we send in emotes.
bool quiet = false)
{
output = input.Trim();
@ -162,7 +166,7 @@ public abstract partial class SharedChatSystem : EntitySystem
if (input.StartsWith(RadioCommonPrefix))
{
output = SanitizeMessageCapital(input[1..].TrimStart());
output = capitalize ? SanitizeMessageCapital(input[1..].TrimStart()) : input[1..].TrimStart(); // DeltaV
channel = _prototypeManager.Index<RadioChannelPrototype>(CommonChannel);
return true;
}
@ -172,7 +176,7 @@ public abstract partial class SharedChatSystem : EntitySystem
if (input.Length < 2 || char.IsWhiteSpace(input[1]))
{
output = SanitizeMessageCapital(input[1..].TrimStart());
output = capitalize ? SanitizeMessageCapital(input[1..].TrimStart()) : input[1..].TrimStart(); // DeltaV
if (!quiet)
_popup.PopupEntity(Loc.GetString("chat-manager-no-radio-key"), source, source);
return true;
@ -180,7 +184,7 @@ public abstract partial class SharedChatSystem : EntitySystem
var channelKey = input[1];
channelKey = char.ToLower(channelKey);
output = SanitizeMessageCapital(input[2..].TrimStart());
output = capitalize ? SanitizeMessageCapital(input[2..].TrimStart()) : input[2..].TrimStart(); // DeltaV
if (channelKey == DefaultChannelKey)
{
@ -314,11 +318,29 @@ public abstract partial class SharedChatSystem : EntitySystem
return rawmsg.Substring(tagStart, tagEnd - tagStart);
}
// DeltaV
protected virtual void SendAudibleEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
RadioChannelPrototype? channel,
EmoteType? emoteType,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
NetUserId? author = null
)
{
}
// DeltaV - End
protected virtual void SendEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
EmoteType? emoteType,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,

View File

@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Ghost;
using Content.Shared.Interaction;
using Content.Shared.Inventory; // Harmony - for lanyards
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
@ -293,7 +294,7 @@ namespace Content.Shared.Examine
/// If you're pushing multiple messages that should be grouped together (or ordered in some way),
/// call <see cref="PushGroup"/> before pushing and <see cref="PopGroup"/> when finished.
/// </summary>
public sealed class ExaminedEvent : EntityEventArgs
public sealed class ExaminedEvent : EntityEventArgs, IInventoryRelayEvent //Harmony InventoryRelayEvent for Lanyards
{
/// <summary>
/// The message that will be displayed as the examine text.
@ -529,6 +530,7 @@ namespace Content.Shared.Examine
}
private record ExamineMessagePart(FormattedMessage Message, int Priority, bool DoNewLine, string? Group);
public SlotFlags TargetSlots { get; } = SlotFlags.NECK; // Harmony - For targeting lanyards.
}

View File

@ -17,6 +17,17 @@ namespace Content.Shared.Humanoid
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
// DeltaV - i hate this hack but https://github.com/space-wizards/RobustToolbox/issues/6576
private ISawmill _locSawmill = default!;
public override void Initialize()
{
base.Initialize();
_locSawmill = Logger.GetSawmill("loc");
}
// End DeltaV - i hate this hack
public string GetName(string species, Gender? gender = null)
{
// if they have an old species or whatever just fall back to human I guess?
@ -47,31 +58,67 @@ namespace Content.Shared.Humanoid
return Loc.GetString("namepreset-lastfirst",
("last", GetLastName(speciesProto)), ("first", GetFirstName(speciesProto, gender)));
case SpeciesNaming.FirstLast:
// Begin DeltaV - more complex naming
default:
{
var firstId = GetFirstNameId(speciesProto, gender);
var lastId = GetLastNameId(speciesProto);
var plural = false;
var oldLevel =_locSawmill.Level;
_locSawmill.Level = LogLevel.Fatal; // this is a hack to avoid testfails because TryGetString still logs errors for not-found stuff anyways (wtf)
if (Loc.TryGetString($"{lastId}.plural", out var pluralStr))
plural = pluralStr == "true";
var last = Loc.GetString(lastId);
if (Loc.TryGetString($"{firstId}.intersperse", out var firstIntersperse, ("last", last), ("lastPlural", plural)))
{
_locSawmill.Level = oldLevel;
return firstIntersperse;
}
_locSawmill.Level = oldLevel;
return Loc.GetString("namepreset-firstlast",
("first", GetFirstName(speciesProto, gender)), ("last", GetLastName(speciesProto)));
("first", Loc.GetString(firstId)),
("last", last),
("lastPlural", plural));
}
// End DeltaV - more complex naming
}
}
// Begin DeltaV - we want IDs
public string GetFirstName(SpeciesPrototype speciesProto, Gender? gender = null)
{
return Loc.GetString(GetFirstNameId(speciesProto, gender));
}
public string GetFirstNameId(SpeciesPrototype speciesProto, Gender? gender = null)
{
switch (gender)
{
case Gender.Male:
return _random.Pick(_prototypeManager.Index(speciesProto.MaleFirstNames));
return _random.PickId(_prototypeManager.Index(speciesProto.MaleFirstNames));
case Gender.Female:
return _random.Pick(_prototypeManager.Index(speciesProto.FemaleFirstNames));
return _random.PickId(_prototypeManager.Index(speciesProto.FemaleFirstNames));
default:
if (_random.Prob(0.5f))
return _random.Pick(_prototypeManager.Index(speciesProto.MaleFirstNames));
return _random.PickId(_prototypeManager.Index(speciesProto.MaleFirstNames));
else
return _random.Pick(_prototypeManager.Index(speciesProto.FemaleFirstNames));
return _random.PickId(_prototypeManager.Index(speciesProto.FemaleFirstNames));
}
}
public string GetLastNameId(SpeciesPrototype speciesProto)
{
return _random.PickId(_prototypeManager.Index(speciesProto.LastNames));
}
public string GetLastName(SpeciesPrototype speciesProto)
{
return _random.Pick(_prototypeManager.Index(speciesProto.LastNames));
return Loc.GetString(GetLastNameId(speciesProto));
}
// End DeltaV - we want IDs
}
}

View File

@ -28,6 +28,11 @@ public sealed class ActivateInWorldEvent : HandledEntityEventArgs, ITargetedInte
/// </summary>
public bool WasLogged { get; set; }
/// <summary>
/// Stellar - set to false if this interaction shouldn't have an interaction particle
/// </summary>
public bool InteractionParticle = true;
public ActivateInWorldEvent(EntityUid user, EntityUid target, bool complex)
{
User = user;

View File

@ -30,6 +30,11 @@ public sealed class InteractUsingEvent : HandledEntityEventArgs
/// </summary>
public EntityCoordinates ClickLocation { get; }
/// <summary>
/// Stellar - set to false if this interaction shouldn't have an interaction particle
/// </summary>
public bool InteractionParticle = true;
public InteractUsingEvent(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation)
{
// Interact using should not have the same used and target.

View File

@ -31,6 +31,7 @@ using Content.Shared.UserInterface;
using Content.Shared.Verbs;
using Content.Shared.Wall;
using Content.Shared._Goobstation.DoAfter; // Goobstation
using Content.Shared.Inventory.VirtualItem; // Stellar - interaction particles
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Input;
@ -1073,8 +1074,8 @@ namespace Content.Shared.Interaction
var userInteractUsingEvent = new UserInteractUsingEvent(user, used, target, clickLocation);
RaiseLocalEvent(user, userInteractUsingEvent, true);
DoContactInteraction(user, used, null, true, interactUsingEvent); // Stellar - interaction particles
DoContactInteraction(user, target, used, true, interactUsingEvent); // Stellar - interaction particles
DoContactInteraction(user, used, null, true, interactUsingEvent, interactionParticles: interactUsingEvent.InteractionParticle); // Stellar - interaction particles
DoContactInteraction(user, target, used, true, interactUsingEvent, interactionParticles: interactUsingEvent.InteractionParticle); // Stellar - interaction particles
// Contact interactions are currently only used for forensics, so we don't raise used -> target
if (interactUsingEvent.Handled || userInteractUsingEvent.Handled)
return true;
@ -1189,7 +1190,7 @@ namespace Content.Shared.Interaction
RaiseLocalEvent(used, activateMsg, true);
if (activateMsg.Handled)
{
DoContactInteraction(user, used, null, true); // Interaction particles
DoContactInteraction(user, used, null, true, interactionParticles: activateMsg.InteractionParticle); // Stellar - Interaction particles
if (!activateMsg.WasLogged)
_adminLogger.Add(LogType.InteractActivate, LogImpact.Low, $"{ToPrettyString(user):user} activated {ToPrettyString(used):used}");
@ -1437,7 +1438,8 @@ namespace Content.Shared.Interaction
/// <param name="predicted">Whether this interaction is predicted. <see cref="uidA"/> is assumed to be the client entity.</param>
/// <param name="args">Optional handleable entity event to check.</param>
/// <param name="interactionParticles">Whether to spawn interaction particles on this contact.</param>
public void DoContactInteraction(EntityUid uidA, EntityUid? uidB, EntityUid? used, bool predicted, HandledEntityEventArgs? args = null, bool interactionParticles = true) // Stellar/ES - interaction particles
/// <param name="interactionParticleType">The type of interaction particle to spawn for this event.</param>
public void DoContactInteraction(EntityUid uidA, EntityUid? uidB, EntityUid? used, bool predicted, HandledEntityEventArgs? args = null, bool interactionParticles = true, StellarInteractionParticleType interactionParticleType = StellarInteractionParticleType.Use) // Stellar/ES - interaction particles
{
if (uidB == null || args?.Handled == false)
return;
@ -1459,7 +1461,7 @@ namespace Content.Shared.Interaction
RaiseLocalEvent(uidB.Value, ev);
// Begin Stellar/ES Additions - Interaction particles
if (!interactionParticles)
if (!interactionParticles || HasComp<VirtualItemComponent>(uidB))
return;
if (_net.IsServer)
@ -1468,11 +1470,11 @@ namespace Content.Shared.Interaction
? Filter.PvsExcept(uidA, entityManager: EntityManager)
: Filter.Pvs(uidA, entityManager: EntityManager);
RaiseNetworkEvent(new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), false), filter);
RaiseNetworkEvent(new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), false, interactionParticleType), filter);
}
else if (_gameTiming.IsFirstTimePredicted)
{
var evt = new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), true);
var evt = new StellarInteractionParticleEvent(GetNetEntity(uidA), GetNetEntity(used), GetNetEntity(uidB.Value), true, interactionParticleType);
RaiseLocalEvent(evt);
}
// End Stellar/ES Additions - Interaction particles

View File

@ -33,6 +33,7 @@ using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Wieldable;
using Content.Shared.Zombies;
using Content.Shared.Examine; //Harmony
namespace Content.Shared.Inventory;
@ -64,6 +65,7 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, BeforeEmoteEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, StoodEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, DownedEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ExaminedEvent>(RelayInventoryEvent); // Harmony - added for lanyards.
// by-ref events
SubscribeLocalEvent<InventoryComponent, RefreshFrictionModifiersEvent>(RefRelayInventoryEvent);

View File

@ -1,6 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement; //Harmony Lanyards
using Content.Shared.Inventory; //Harmony Lanyards
using Content.Shared.Labels.Components;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Paper;
@ -8,6 +10,7 @@ using Content.Shared.Tag; // DeltaV
using Robust.Shared.Containers;
using Robust.Shared.Prototypes; // DeltaV
using Robust.Shared.Utility;
using System.Linq; //Harmony Lanyards
namespace Content.Shared.Labels.EntitySystems;
@ -34,6 +37,11 @@ public sealed partial class LabelSystem : EntitySystem
SubscribeLocalEvent<PaperLabelComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<PaperLabelComponent, EntRemovedFromContainerMessage>(OnContainerModified);
SubscribeLocalEvent<PaperLabelComponent, ExaminedEvent>(OnExamined);
// Harmony - OnExamined can now be inventory relayed from the neck slot for lanyard implementation,
// getting the entity's pronouns and changing the label inspection text to reflect it being from a lanyard.
// This would cause any other neck slot item with the label component to be described as a lanyard on inspection,
// but currently no others exist.
SubscribeLocalEvent<PaperLabelComponent, InventoryRelayedEvent<ExaminedEvent>>((e, c, ev) => OnExaminedInInventory(e, c, ev.Args));
}
private void OnLabelCompMapInit(Entity<LabelComponent> ent, ref MapInitEvent args)
@ -123,9 +131,65 @@ public sealed partial class LabelSystem : EntitySystem
args.PushMarkup(Loc.GetString("comp-paper-label-has-label"));
var text = paper.Content;
args.PushMarkup(text.TrimEnd());
// Harmony addition begins - shows which stamps have been applied to a label when inspected. Copied from PaperSystem.
if (paper.StampedBy.Count > 0)
{
var commaSeparated =
string.Join(", ", paper.StampedBy.Select(s => Loc.GetString(s.StampedName)));
args.PushMarkup(
Loc.GetString(
"comp-label-examine-detail-stamped-by",
("stamps", commaSeparated))
);
}
// Harmony addition ends
}
}
// Harmony addition begins - version of OnExamined for when the event is inventory relayed. Used when reading from a worn lanyard.
private void OnExaminedInInventory(EntityUid uid, PaperLabelComponent comp, ExaminedEvent args)
{
if (comp.LabelSlot.Item is not { Valid: true } item)
return;
using (args.PushGroup(nameof(PaperLabelComponent)))
{
// UID's parent is saved to be used for localisation grammar when the label is from a lanyard, since the text is changed.
var user = Comp<TransformComponent>(uid).ParentUid;
if (!args.IsInDetailsRange)
{
args.PushMarkup(Loc.GetString("comp-lanyard-has-lanyard-cant-read", ("user", Identity.Entity(user, EntityManager))));
return;
}
if (!TryComp(item, out PaperComponent? paper))
// Assuming yaml has the correct entity whitelist, this should not happen.
return;
if (string.IsNullOrWhiteSpace(paper.Content))
{
args.PushMarkup(Loc.GetString("comp-lanyard-has-lanyard-blank", ("user", Identity.Entity(user, EntityManager))));
return;
}
args.PushMarkup(Loc.GetString("comp-lanyard-has-lanyard", ("user", Identity.Entity(user, EntityManager))));
var text = paper.Content;
args.PushMarkup(text.TrimEnd());
// Harmony - shows which stamps have been applied to a lanyard's label when inspected. Copied from PaperSystem.
if (paper.StampedBy.Count > 0)
{
var commaSeparated =
string.Join(", ", paper.StampedBy.Select(s => Loc.GetString(s.StampedName)));
args.PushMarkup(
Loc.GetString(
"comp-lanyard-examine-detail-stamped-by",
("stamps", commaSeparated))
);
}
}
}
// Harmony Addition Ends
// Not ref-sub due to being used for multiple subscriptions.
private void OnContainerModified(EntityUid uid, PaperLabelComponent label, ContainerModifiedMessage args)
{

View File

@ -122,6 +122,11 @@ public sealed class MindExamineSystem : EntitySystem
else
ent.Comp.State = MindState.None;
// DeltaV - SSD Recency START
var ev = new MindStateUpdatedEvent(ent.Comp.State);
RaiseLocalEvent(ent, ref ev);
// DeltaV END
Dirty(ent);
}
}

View File

@ -1,3 +1,4 @@
using Content.Shared._ST.Interaction; // Stellar - interaction particles
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
@ -545,7 +546,7 @@ public sealed class PullingSystem : EntitySystem
// Pulling confirmed
_interaction.DoContactInteraction(pullableUid, pullerUid, null, true); // Stellar - Interaction particles
_interaction.DoContactInteraction(pullerUid, pullableUid,null, true, interactionParticleType: StellarInteractionParticleType.Pull); // Stellar - Interaction particles
// Use net entity so it's consistent across client and server.
pullableComp.PullJointId = $"pull-joint-{GetNetEntity(pullableUid)}";

View File

@ -23,5 +23,12 @@ public sealed partial class PowerCellSlotComponent : Component
[DataField, AutoNetworkedField]
public bool FitsInCharger = true;
// Begin DeltaV
/// <summary>
/// DeltaV - power cell slots
/// </summary>
[DataField, AutoNetworkedField]
public bool Active = true;
// End DeltaV
}

View File

@ -39,6 +39,14 @@ public sealed partial class PowerCellSystem
return false;
}
// Begin DeltaV
if (!ent.Comp.Active)
{
battery = null;
return false;
}
// End DeltaV
if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out var slot))
{
battery = null;
@ -153,6 +161,21 @@ public sealed partial class PowerCellSystem
[PublicAPI]
public bool HasCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
{
// Begin DeltaV
if (Resolve(ent, ref ent.Comp, false) && !ent.Comp.Active)
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-disabled"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-disabled"), ent.Owner, user.Value);
return false;
}
// End DeltaV
if (!TryGetBatteryFromSlot(ent, out var battery))
{
if (user == null)
@ -192,6 +215,21 @@ public sealed partial class PowerCellSystem
[PublicAPI]
public bool TryUseCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
{
// Begin DeltaV
if (Resolve(ent, ref ent.Comp, false) && !ent.Comp.Active)
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-disabled"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-disabled"), ent.Owner, user.Value);
return false;
}
// End DeltaV
if (!TryGetBatteryFromSlot(ent, out var battery))
{
if (user == null)
@ -247,6 +285,29 @@ public sealed partial class PowerCellSystem
return _battery.GetMaxUses(battery.Value.AsNullable(), cost);
}
// Begin DeltaV
/// <summary>
/// Sets whether a power cell slot is capable of drawing from a battery
/// </summary>
[PublicAPI]
public void SetSlotActive(Entity<PowerCellSlotComponent?> ent, bool active)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.Active = active;
Dirty(ent, ent.Comp);
if (!active)
{
var emptyEv = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(ent, ref emptyEv);
}
_battery.RefreshChargeRate(ent.Owner);
}
// End DeltaV
}
// Begin DeltaV - event-based search for battery

View File

@ -14,13 +14,21 @@ namespace Content.Shared.Random.Helpers
return random.Pick(prototype.Values);
}
/// <summary>
/// DeltaV - Randomly selects an entry from <paramref name="prototype"/> and returns the result.
/// </summary>
public static string PickId(this IRobustRandom random, LocalizedDatasetPrototype prototype)
{
var index = random.Next(prototype.Values.Count);
return prototype.Values[index];
}
/// <summary>
/// Randomly selects an entry from <paramref name="prototype"/>, attempts to localize it, and returns the result.
/// </summary>
public static string Pick(this IRobustRandom random, LocalizedDatasetPrototype prototype)
{
var index = random.Next(prototype.Values.Count);
return Loc.GetString(prototype.Values[index]);
return Loc.GetString(random.PickId(prototype)); // DeltaV - we need LocIds too
}
public static string Pick(this IWeightedRandomPrototype prototype, System.Random random)

View File

@ -18,11 +18,13 @@ public sealed partial class RatKingComponent : Component
[DataField("actionRaiseArmyEntity")]
public EntityUid? ActionRaiseArmyEntity;
// Delta-V - switched to a base cost modified by the amount of living servants
/// <summary>
/// The amount of hunger one use of Raise Army consumes
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("hungerPerArmyUse", required: true)]
public float HungerPerArmyUse = 25f;
public float HungerPerArmyUse = 10f;
// end DeltaV
/// <summary>
/// The entity prototype of the mob that Raise Army summons

View File

@ -46,4 +46,49 @@ public sealed partial class SSDIndicatorComponent : Component
/// </summary>
[DataField]
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(1);
// DeltaV - SSD Recency Additions START
/// <summary>
/// The time at which the player became SSD.
/// This will remain unset on SSD entities that never had minds attached, such as newly spawn ghost roles.
/// </summary>
[AutoNetworkedField, AutoPausedField]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? SsdSince;
/// <summary>
/// The icon displayed next to the associated entity when it is recently SSD (stage 2).
/// </summary>
[DataField]
public ProtoId<SsdIconPrototype> RecentIcon = "RecentSSDIcon";
/// <summary>
/// The icon displayed next to the associated entity when it is very recently SSD (stage 1).
/// </summary>
[DataField]
public ProtoId<SsdIconPrototype> VeryRecentIcon = "VeryRecentSSDIcon";
// DeltaV END
}
// DeltaV - SSD Recency START
// If you change this enum, remember to update `Resources/Locale/en-US/_DV/ssdIndicator/examine.ftl`
public enum SsdStage : byte
{
/// <summary>
/// Stage 1: SSD Indicator is red. They might just be recovering from a crash/timeout.
/// </summary>
VeryRecent,
/// <summary>
/// Stage 2: SSD Indicator is yellow. They've been gone for a bit, but they shouldn't be moved to cryo yet.
/// </summary>
Recent,
/// <summary>
/// Stage 3: SSD Indicator is green/default. They've been gone for a long time, they can be moved to cryo.
/// </summary>
Cryoable,
}
// DeltaV END

View File

@ -1,4 +1,9 @@
using Content.Shared._DV.CCVars; // DeltaV - SSD Recency
using Content.Shared._DV.Mind; // DeltaV - SSD Recency
using Content.Shared.CCVar;
using Content.Shared.Examine; // DeltaV - SSD Recency
using Content.Shared.Mobs.Systems; // DeltaV - SSD Recency
using Content.Shared.Mind.Components; // DeltaV - SSD Recency
using Content.Shared.StatusEffectNew;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
@ -17,20 +22,75 @@ public sealed class SSDIndicatorSystem : EntitySystem
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly MobStateSystem _mobState = default!; // DeltaV - SSD Recency
private bool _icSsdSleep;
private float _icSsdSleepTime;
private TimeSpan _cryoableSsdSeconds; // DeltaV - SSD Recency
private TimeSpan _recentSsdSeconds; // DeltaV - SSD Recency
public override void Initialize()
{
SubscribeLocalEvent<SSDIndicatorComponent, PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<SSDIndicatorComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<SSDIndicatorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SSDIndicatorComponent, MindStateUpdatedEvent>(OnMindStateUpdated); // DeltaV - SSD Recency
_cfg.OnValueChanged(CCVars.ICSSDSleep, obj => _icSsdSleep = obj, true);
_cfg.OnValueChanged(CCVars.ICSSDSleepTime, obj => _icSsdSleepTime = obj, true);
_cfg.OnValueChanged(DCCVars.SsdIndicatorCryoableAfterSeconds, obj => _cryoableSsdSeconds = TimeSpan.FromSeconds(obj), true); // DeltaV - SSD Recency
_cfg.OnValueChanged(DCCVars.SsdIndicatorRecentAfterSeconds, obj => _recentSsdSeconds = TimeSpan.FromSeconds(obj), true); // DeltaV - SSD Recency
SubscribeLocalEvent<SSDIndicatorComponent, ExaminedEvent>(OnExamine); // DeltaV - SSD Recency
}
// DeltaV - SSD Recency START
private void OnExamine(Entity<SSDIndicatorComponent> ent, ref ExaminedEvent args)
{
if (ent.Comp.SsdSince is not { } ssdSince)
return;
if (_mobState.IsDead(ent))
return;
using (args.PushGroup(nameof(SSDIndicatorComponent)))
{
var timestamp = (_timing.CurTime - ssdSince).ToString("%hh':'mm':'ss");
args.PushMarkup(Loc.GetString("ssd-examine-duration", ("time", timestamp)));
args.PushMarkup(Loc.GetString($"ssd-examine-{GetStage(ent).ToString().ToLower()}"));
}
}
public SsdStage GetStage(Entity<SSDIndicatorComponent> ent)
{
var curTime = _timing.CurTime;
if (ent.Comp.SsdSince + _recentSsdSeconds >= curTime)
{
return SsdStage.VeryRecent;
}
if (ent.Comp.SsdSince + _cryoableSsdSeconds >= curTime)
{
return SsdStage.Recent;
}
return SsdStage.Cryoable;
}
public void OnMindStateUpdated(Entity<SSDIndicatorComponent> ent, ref MindStateUpdatedEvent args)
{
if (args.State is MindState.SSD or MindState.DeadSSD) {
if (ent.Comp.SsdSince is null)
ent.Comp.SsdSince = _timing.CurTime;
}
else
ent.Comp.SsdSince = null;
Dirty(ent, ent.Comp);
}
// DeltaV END
private void OnPlayerAttached(EntityUid uid, SSDIndicatorComponent component, PlayerAttachedEvent args)
{
component.IsSSD = false;

View File

@ -77,4 +77,10 @@ public sealed partial class BorgTransponderComponent : Component
/// </summary>
[DataField]
public bool FakeDisabled;
/// <summary>
/// DeltaV - whether the transponder is active
/// </summary>
[DataField]
public bool Active = true;
}

View File

@ -62,6 +62,7 @@ public abstract class SharedEmitSoundSystem : EntitySystem
if (_whitelistSystem.IsWhitelistFail(component.Blacklist, args.User))
{
TryEmitSound(uid, component, args.User);
args.InteractionParticle = true; // Stellar - interaction particles
}
}

View File

@ -106,6 +106,7 @@ public abstract partial class SharedStackSystem : EntitySystem
var localRotation = Transform(args.Used).LocalRotation;
_storage.PlayPickupAnimation(args.Used, popupPos, userCoords, localRotation, args.User);
args.InteractionParticle = false; // Stellar
}
/// <summary>

View File

@ -718,8 +718,13 @@ public abstract class SharedStrippableSystem : EntitySystem
if (args.Handled || !args.Complex || args.Target == args.User)
return;
// Begin Stellar Changes - don't play an interact particle for examining the strip UI
if (TryOpenStrippingUi(args.User, (uid, component)))
{
args.Handled = true;
args.InteractionParticle = false;
}
// End Stellar Changes - don't play an interact particle for examining the strip UI
}
/// <summary>

View File

@ -52,6 +52,11 @@ public sealed class AfterActivatableUIOpenEvent(EntityUid user) : EntityEventArg
/// The player that opened the UI.
/// </summary>
public readonly EntityUid User = user;
/// <summary>
/// Stellar - if an interaction particle should be played for this event.
/// </summary>
public bool InteractionParticle;
}
/// <summary>

View File

@ -21,6 +21,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
public override void Initialize()
{
@ -72,7 +73,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
args.Verbs.Add(new ActivationVerb
{
Act = () => InteractUI(args.User, uid, component),
Act = () => InteractUI(args.User, (uid, component)), // Stellar - interaction particles
Text = Loc.GetString(component.VerbText),
// TODO VERB ICON find a better icon
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
@ -86,7 +87,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
args.Verbs.Add(new Verb
{
Act = () => InteractUI(args.User, uid, component),
Act = () => InteractUI(args.User, (uid, component)), // Stellar - interaction particles
Text = Loc.GetString(component.VerbText),
// TODO VERB ICON find a better icon
Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
@ -133,7 +134,8 @@ public sealed partial class ActivatableUISystem : EntitySystem
if (component.RequiredItems != null)
return;
args.Handled = InteractUI(args.User, uid, component);
var interactionParticle = false; // Stellar - interaction particles
args.Handled = InteractUI(args.User, uid, component, ref interactionParticle); // Stellar - interaction particles
}
private void OnActivate(EntityUid uid, ActivatableUIComponent component, ActivateInWorldEvent args)
@ -147,7 +149,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
if (component.RequiredItems != null)
return;
args.Handled = InteractUI(args.User, uid, component);
args.Handled = InteractUI(args.User, uid, component, ref args.InteractionParticle); // Stellar - interaction particles
}
private void OnInteractUsing(EntityUid uid, ActivatableUIComponent component, InteractUsingEvent args)
@ -164,7 +166,7 @@ public sealed partial class ActivatableUISystem : EntitySystem
if (_whitelistSystem.IsWhitelistFail(component.RequiredItems, args.Used))
return;
args.Handled = InteractUI(args.User, uid, component);
args.Handled = InteractUI(args.User, uid, component, ref args.InteractionParticle); // Stellar - interaction particles
}
private void OnUIClose(EntityUid uid, ActivatableUIComponent component, BoundUIClosedEvent args)
@ -180,13 +182,23 @@ public sealed partial class ActivatableUISystem : EntitySystem
SetCurrentSingleUser(uid, null, component);
}
private bool InteractUI(EntityUid user, EntityUid uiEntity, ActivatableUIComponent aui)
// Begin Stellar - interaction particles
private void InteractUI(EntityUid user, Entity<ActivatableUIComponent> ui)
{
var interactionParticle = false;
InteractUI(user, ui, ui, ref interactionParticle);
_interaction.DoContactInteraction(user, ui, null, true, interactionParticles: interactionParticle);
}
// End Stellar - interaction particles
private bool InteractUI(EntityUid user, EntityUid uiEntity, ActivatableUIComponent aui, ref bool interactionParticle) // Stellar - interaction particles
{
if (aui.Key == null || !_uiSystem.HasUi(uiEntity, aui.Key))
return false;
if (_uiSystem.IsUiOpen(uiEntity, aui.Key, user))
{
interactionParticle = false; // Stellar - interaction particles
_uiSystem.CloseUi(uiEntity, aui.Key, user);
return true;
}
@ -243,6 +255,8 @@ public sealed partial class ActivatableUISystem : EntitySystem
var aae = new AfterActivatableUIOpenEvent(user);
RaiseLocalEvent(uiEntity, aae);
interactionParticle = aae.InteractionParticle; // Stellar
return true;
}

View File

@ -31,6 +31,7 @@ using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems;
using Content.Shared.Wieldable.Components; // Starlight | ES Screenshake
using Content.Shared.Zombies; // DeltaV - Buff Zombies
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
@ -597,18 +598,7 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
if (damageResult.GetTotal() > FixedPoint2.Zero)
{
DoDamageEffect(targets, user, targetXform);
// ES START
// dog shit copy plaste but thats melee for you
var userShakeRotation = new ESScreenshakeParameters()
{ Trauma = 0.08f, DecayRate = 1.0f, Frequency = 0.009f };
var otherShakeTranslation = new ESScreenshakeParameters() { Trauma = 0.45f, DecayRate = 1.1f, Frequency = 0.04f };
_shake.Screenshake(user, null, userShakeRotation);
foreach (var shakeTarget in targets)
{
_shake.Screenshake(shakeTarget, otherShakeTranslation, null);
}
// ES END
DoScreenshake(meleeUid, damageResult, user, targets); // Starlight | ES Screenshake
}
}
@ -774,21 +764,10 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
_meleeSound.PlayHitSound(target, user, GetHighestDamageSound(appliedDamage, _protoManager), hitEvent.HitSoundOverride, component);
}
// ES START
// dog shit copy plaste but thats melee for you
var userShakeRotation = new ESScreenshakeParameters()
{ Trauma = 0.08f, DecayRate = 1.0f, Frequency = 0.009f };
var otherShakeTranslation = new ESScreenshakeParameters() { Trauma = 0.45f, DecayRate = 1.1f, Frequency = 0.04f };
_shake.Screenshake(user, null, userShakeRotation);
foreach (var shakeTarget in targets)
{
_shake.Screenshake(shakeTarget, otherShakeTranslation, null);
}
// ES END
if (appliedDamage.GetTotal() > FixedPoint2.Zero)
{
DoDamageEffect(targets, user, Transform(targets[0]));
DoScreenshake(meleeUid, damage, user, targets); // Starlight | ES Screenshake
}
return true;
@ -1104,4 +1083,55 @@ public abstract class SharedMeleeWeaponSystem : EntitySystem
}
}
}
//Starlight begin | ES Screenshake
private void DoScreenshake(EntityUid weapon, DamageSpecifier damage, EntityUid attacker, List<EntityUid> targets)
{
if(damage.GetTotal()>4) // only show to others if it hurts real bad // DeltaV - reduce from 8 to 4
{
var otherTranslation = new ESScreenshakeParameters
{
Trauma = 0.45f,
DecayRate = 1.1f,
Frequency = 0.04f,
};
foreach(var target in targets)
_shake.Screenshake(target, otherTranslation, null);
}
// only show to attacker if they put real oompf into it, or the weapon is just THAT strong
// var bluntRequirement = damage.DamageDict.TryGetValue(BluntDamageName, out var blunt) && blunt >= 20; // DeltaV - unused
var isWielding = TryComp<WieldableComponent>(weapon, out var wieldable) && wieldable.Wielded;
// DeltaV - unused
// if (!bluntRequirement && !wieldRequirement)
// return;
// DeltaV - heavy/light screenshake variants START
ESScreenshakeParameters userRotation;
if (damage.GetTotal() >= 15 || isWielding)
{
// heavy damage or two-handed
userRotation = new ESScreenshakeParameters
{
Trauma = 0.08f,
DecayRate = 1,
Frequency = 0.009f,
};
}
else
{
// light damage
userRotation = new ESScreenshakeParameters
{
Trauma = 0.06f,
DecayRate = 1,
Frequency = 0.0045f,
};
}
// DeltaV END
_shake.Screenshake(attacker, null, userRotation);
}
//Starlight end
}

View File

@ -316,4 +316,19 @@ public sealed partial class DCCVars
/// </summary>
public static readonly CVarDef<bool> EsScreenshakeDisabled =
CVarDef.Create("deltav.es_screenshake.disabled", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// The total time a player has to be SSD to be considered cryoable (stage 3).
/// Default is 20 minutes. Value should be bigger than <see cref="SsdIndicatorRecentAfterSeconds"/>.
/// </summary>
public static readonly CVarDef<float> SsdIndicatorCryoableAfterSeconds =
CVarDef.Create("deltav.ssd.cryoable_after_seconds", 1200f, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// The total time a player has to be SSD to be considered recently SSD (stage 2).
/// If the player has been SSD for less than this time, they are considered "very recently" SSD (stage 1).
/// Default is 5 minutes. Value should be smaller than <see cref="SsdIndicatorCryoableAfterSeconds"/>.
/// </summary>
public static readonly CVarDef<float> SsdIndicatorRecentAfterSeconds =
CVarDef.Create("deltav.ssd.recent_after_seconds", 300f, CVar.SERVER | CVar.REPLICATED);
}

View File

@ -0,0 +1,6 @@
using Content.Shared.Radio;
namespace Content.Shared._DV.Chat;
[ByRefEvent]
public record struct EntityAudiblyEmotedEvent(EntityUid Source, string Message, RadioChannelPrototype? Channel, EmoteType? Type);

View File

@ -0,0 +1,15 @@
namespace Content.Shared._DV.Chat;
// Note for future: If you want to make this more robust to handle more types of emotes while still being able to check off audible as an option for it then it would
// probably be better to make it a struct and have audible be a flag for it and then the type of emote. This would avoid having to make two different types for audible and visual.
/// <summary>
/// Different ways of emoting. For that little extra in RP!
/// </summary>
public enum EmoteType : byte
{
Normal, // Character emotes
Audible, // Character screams
Possessive, // Character's emote
AudiblePossessive // Character's scream
}

View File

@ -1,3 +1,5 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@ -36,6 +38,8 @@ public sealed partial class CosmicColossusComponent : Component
[DataField] public EntProtoId Attack1Vfx = "CosmicColossusAttack1Vfx";
[DataField] public EntProtoId BuffVfx = "ColossusBuffVfx";
[DataField] public EntProtoId TileDetonations = "MobTileDamageZone";
[DataField] public EntProtoId EffigyPrototype = "CosmicEffigy";
@ -52,13 +56,23 @@ public sealed partial class CosmicColossusComponent : Component
[DataField] public TimeSpan HibernationWait = TimeSpan.FromSeconds(30);
[DataField] public TimeSpan DeathWait = TimeSpan.FromMinutes(15);
[DataField] public TimeSpan DeathWaitSpawn = TimeSpan.FromMinutes(15);
[DataField] public TimeSpan DeathWaitEffigy = TimeSpan.FromMinutes(10);
[DataField] public bool Attacking;
[DataField] public bool Hibernating;
[DataField] public bool Timed;
[DataField] public short CompletedEffigies;
[DataField] public short MaxEffigies = 3;
[DataField] public DamageSpecifier BonusDamage = new();
}
[Serializable, NetSerializable]

View File

@ -137,6 +137,12 @@ public sealed partial class DeepFryerComponent : Component
/// <seealso cref="ProfessionalChefComponent"/>
[DataField]
public float MissChance = 0.25f;
/// <summary>
/// Check for whether the cooking timer needs to be reset after powering down
/// </summary>
[DataField]
public bool WasPreviouslyPowered = true;
}
/// <summary>

View File

@ -0,0 +1,6 @@
namespace Content.Shared._DV.Mind;
using Content.Shared.Mind.Components;
[ByRefEvent]
public record struct MindStateUpdatedEvent(MindState State);

View File

@ -1,3 +1,4 @@
using Content.Shared.ActionBlocker;
using Content.Shared._DV.Movement.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
@ -7,12 +8,19 @@ namespace Content.Shared.Movement.Systems;
public abstract partial class SharedJetpackSystem
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
private void OnJetpackToggle(Entity<JetpackComponent> jetpack, ref ToggleJetpackEvent args)
{
if (args.Handled)
return;
if (!_actionBlocker.CanComplexInteract(args.Performer))
{
_popup.PopupClient(Loc.GetString("jetpack-too-complex"), jetpack, args.Performer);
return;
}
jetpack.Comp.AutomaticMode = !jetpack.Comp.AutomaticMode;
jetpack.Comp.AutomaticUser = args.Performer;
Dirty(jetpack);

View File

@ -42,6 +42,9 @@ public sealed class PagerSystem : EntitySystem
private void OnAfterInteract(Entity<PagerComponent> ent, ref AfterInteractEvent args)
{
if (!args.CanReach)
return;
if (args.Handled || !TryComp<DeviceNetworkComponent>(args.Target, out var targetNetwork) || !HasComp<PageSenderComponent>(args.Target))
return;

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