Merge remote-tracking branch 'refs/remotes/upstream/master' into ed-13-05-2024-upstream
# Conflicts: # Content.Shared/Lock/LockSystem.cs # Resources/Prototypes/Maps/oasis.yml
This commit is contained in:
@@ -65,7 +65,7 @@ namespace Content.Shared.APC
|
||||
/// Bitmask for the full state for a given APC lock indicator.
|
||||
/// </summary>
|
||||
All = (Lock),
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The log 2 width in bits of the bitfields indicating the status of an APC lock indicator.
|
||||
/// Used for bit shifting operations (Mask for the state for indicator i is (All << (i << LogWidth))).
|
||||
@@ -175,7 +175,7 @@ namespace Content.Shared.APC
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ApcBoundInterfaceState : BoundUserInterfaceState
|
||||
public sealed class ApcBoundInterfaceState : BoundUserInterfaceState, IEquatable<ApcBoundInterfaceState>
|
||||
{
|
||||
public readonly bool MainBreaker;
|
||||
public readonly bool HasAccess;
|
||||
@@ -191,6 +191,27 @@ namespace Content.Shared.APC
|
||||
ApcExternalPower = apcExternalPower;
|
||||
Charge = charge;
|
||||
}
|
||||
|
||||
public bool Equals(ApcBoundInterfaceState? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return MainBreaker == other.MainBreaker &&
|
||||
HasAccess == other.HasAccess &&
|
||||
Power == other.Power &&
|
||||
ApcExternalPower == other.ApcExternalPower &&
|
||||
MathHelper.CloseTo(Charge, other.Charge);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is ApcBoundInterfaceState other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(MainBreaker, HasAccess, Power, (int) ApcExternalPower, Charge);
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
@@ -198,7 +219,7 @@ namespace Content.Shared.APC
|
||||
{
|
||||
}
|
||||
|
||||
public enum ApcExternalPowerState
|
||||
public enum ApcExternalPowerState : byte
|
||||
{
|
||||
None,
|
||||
Low,
|
||||
@@ -206,7 +227,7 @@ namespace Content.Shared.APC
|
||||
}
|
||||
|
||||
[NetSerializable, Serializable]
|
||||
public enum ApcUiKey
|
||||
public enum ApcUiKey : byte
|
||||
{
|
||||
Key,
|
||||
}
|
||||
|
||||
@@ -40,4 +40,10 @@ public sealed partial class IdCardComponent : Component
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool BypassLogging;
|
||||
|
||||
[DataField]
|
||||
public LocId NameLocId = "access-id-card-component-owner-name-job-title-text";
|
||||
|
||||
[DataField]
|
||||
public LocId FullNameLocId = "access-id-card-component-owner-full-name-job-title-text";
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@ using Content.Shared.Containers.ItemSlots;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Access.Components;
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using Content.Shared.Access.Systems;
|
||||
|
||||
namespace Content.Shared.Access.Components;
|
||||
|
||||
[RegisterComponent, Access(typeof(IdExaminableSystem))]
|
||||
public sealed partial class IdExaminableComponent : Component;
|
||||
@@ -153,7 +153,7 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
return IsAllowedInternal(access, stationKeys, reader);
|
||||
|
||||
if (!_containerSystem.TryGetContainer(target, reader.ContainerAccessProvider, out var container))
|
||||
return false;
|
||||
return Paused(target); // when mapping, containers with electronics arent spawned
|
||||
|
||||
foreach (var entity in container.ContainedEntities)
|
||||
{
|
||||
|
||||
80
Content.Shared/Access/Systems/IdExaminableSystem.cs
Normal file
80
Content.Shared/Access/Systems/IdExaminableSystem.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Access.Systems;
|
||||
|
||||
public sealed class IdExaminableSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<IdExaminableComponent, GetVerbsEvent<ExamineVerb>>(OnGetExamineVerbs);
|
||||
}
|
||||
|
||||
private void OnGetExamineVerbs(EntityUid uid, IdExaminableComponent component, GetVerbsEvent<ExamineVerb> args)
|
||||
{
|
||||
var detailsRange = _examineSystem.IsInDetailsRange(args.User, uid);
|
||||
var info = GetMessage(uid);
|
||||
|
||||
var verb = new ExamineVerb()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
var markup = FormattedMessage.FromMarkup(info);
|
||||
_examineSystem.SendExamineTooltip(args.User, uid, markup, false, false);
|
||||
},
|
||||
Text = Loc.GetString("id-examinable-component-verb-text"),
|
||||
Category = VerbCategory.Examine,
|
||||
Disabled = !detailsRange,
|
||||
Message = detailsRange ? null : Loc.GetString("id-examinable-component-verb-disabled"),
|
||||
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/character.svg.192dpi.png"))
|
||||
};
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
public string GetMessage(EntityUid uid)
|
||||
{
|
||||
return GetInfo(uid) ?? Loc.GetString("id-examinable-component-verb-no-id");
|
||||
}
|
||||
|
||||
public string? GetInfo(EntityUid uid)
|
||||
{
|
||||
if (_inventorySystem.TryGetSlotEntity(uid, "id", out var idUid))
|
||||
{
|
||||
// PDA
|
||||
if (EntityManager.TryGetComponent(idUid, out PdaComponent? pda) &&
|
||||
TryComp<IdCardComponent>(pda.ContainedId, out var id))
|
||||
{
|
||||
return GetNameAndJob(id);
|
||||
}
|
||||
// ID Card
|
||||
if (EntityManager.TryGetComponent(idUid, out id))
|
||||
{
|
||||
return GetNameAndJob(id);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetNameAndJob(IdCardComponent id)
|
||||
{
|
||||
var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})";
|
||||
|
||||
var val = string.IsNullOrWhiteSpace(id.FullName)
|
||||
? Loc.GetString(id.NameLocId,
|
||||
("jobSuffix", jobSuffix))
|
||||
: Loc.GetString(id.FullNameLocId,
|
||||
("fullName", id.FullName),
|
||||
("jobSuffix", jobSuffix));
|
||||
|
||||
return val;
|
||||
}
|
||||
}
|
||||
@@ -207,9 +207,9 @@ public abstract class SharedIdCardSystem : EntitySystem
|
||||
var jobSuffix = string.IsNullOrWhiteSpace(id.JobTitle) ? string.Empty : $" ({id.JobTitle})";
|
||||
|
||||
var val = string.IsNullOrWhiteSpace(id.FullName)
|
||||
? Loc.GetString("access-id-card-component-owner-name-job-title-text",
|
||||
? Loc.GetString(id.NameLocId,
|
||||
("jobSuffix", jobSuffix))
|
||||
: Loc.GetString("access-id-card-component-owner-full-name-job-title-text",
|
||||
: Loc.GetString(id.FullNameLocId,
|
||||
("fullName", id.FullName),
|
||||
("jobSuffix", jobSuffix));
|
||||
_metaSystem.SetEntityName(uid, val);
|
||||
|
||||
@@ -169,8 +169,16 @@ namespace Content.Shared.ActionBlocker
|
||||
|
||||
public bool CanAttack(EntityUid uid, EntityUid? target = null, Entity<MeleeWeaponComponent>? weapon = null, bool disarm = false)
|
||||
{
|
||||
// If target is in a container can we attack
|
||||
if (target != null && _container.IsEntityInContainer(target.Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_container.TryGetOuterContainer(uid, Transform(uid), out var outerContainer);
|
||||
if (target != null && target != outerContainer?.Owner && _container.IsEntityInContainer(uid))
|
||||
|
||||
// If we're in a container can we attack the target.
|
||||
if (target != null && target != outerContainer?.Owner && _container.IsEntityInContainer(uid))
|
||||
{
|
||||
var containerEv = new CanAttackFromContainerEvent(uid, target);
|
||||
RaiseLocalEvent(uid, containerEv);
|
||||
|
||||
@@ -155,4 +155,9 @@ public abstract partial class BaseActionEvent : HandledEntityEventArgs
|
||||
/// The user performing the action.
|
||||
/// </summary>
|
||||
public EntityUid Performer;
|
||||
|
||||
/// <summary>
|
||||
/// The action the event belongs to.
|
||||
/// </summary>
|
||||
public EntityUid Action;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Shared.Mobs;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -25,6 +24,11 @@ public abstract partial class BaseActionComponent : Component
|
||||
/// </summary>
|
||||
[DataField("iconOn")] public SpriteSpecifier? IconOn;
|
||||
|
||||
/// <summary>
|
||||
/// For toggle actions only, background to show when toggled on.
|
||||
/// </summary>
|
||||
[DataField] public SpriteSpecifier? BackgroundOn;
|
||||
|
||||
/// <summary>
|
||||
/// If not null, this color will modulate the action icon color.
|
||||
/// </summary>
|
||||
|
||||
8
Content.Shared/Actions/Events/ActionPerformedEvent.cs
Normal file
8
Content.Shared/Actions/Events/ActionPerformedEvent.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Content.Shared.Actions.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Raised on the action entity when it is used and <see cref="BaseActionEvent.Handled"/>.
|
||||
/// </summary>
|
||||
/// <param name="Performer">The entity that performed this action.</param>
|
||||
[ByRefEvent]
|
||||
public readonly record struct ActionPerformedEvent(EntityUid Performer);
|
||||
@@ -144,9 +144,6 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
|
||||
public void SetCooldown(EntityUid? actionId, TimeSpan start, TimeSpan end)
|
||||
{
|
||||
if (actionId == null)
|
||||
return;
|
||||
|
||||
if (!TryGetActionData(actionId, out var action))
|
||||
return;
|
||||
|
||||
@@ -162,9 +159,6 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
|
||||
public void ClearCooldown(EntityUid? actionId)
|
||||
{
|
||||
if (actionId == null)
|
||||
return;
|
||||
|
||||
if (!TryGetActionData(actionId, out var action))
|
||||
return;
|
||||
|
||||
@@ -175,6 +169,27 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
Dirty(actionId.Value, action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the cooldown for this action only if it is bigger than the one it already has.
|
||||
/// </summary>
|
||||
public void SetIfBiggerCooldown(EntityUid? actionId, TimeSpan? cooldown)
|
||||
{
|
||||
if (cooldown == null ||
|
||||
cooldown.Value <= TimeSpan.Zero ||
|
||||
!TryGetActionData(actionId, out var action))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var start = GameTiming.CurTime;
|
||||
var end = start + cooldown;
|
||||
if (action.Cooldown?.End > end)
|
||||
return;
|
||||
|
||||
action.Cooldown = (start, end.Value);
|
||||
Dirty(actionId.Value, action);
|
||||
}
|
||||
|
||||
public void StartUseDelay(EntityUid? actionId)
|
||||
{
|
||||
if (actionId == null)
|
||||
@@ -438,7 +453,10 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
}
|
||||
|
||||
if (performEvent != null)
|
||||
{
|
||||
performEvent.Performer = user;
|
||||
performEvent.Action = actionEnt;
|
||||
}
|
||||
|
||||
// All checks passed. Perform the action!
|
||||
PerformAction(user, component, actionEnt, action, performEvent, curTime);
|
||||
@@ -551,13 +569,12 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
handled = actionEvent.Handled;
|
||||
}
|
||||
|
||||
_audio.PlayPredicted(action.Sound, performer,predicted ? performer : null);
|
||||
handled |= action.Sound != null;
|
||||
|
||||
if (!handled)
|
||||
return; // no interaction occurred.
|
||||
|
||||
// reduce charges, start cooldown, and mark as dirty (if required).
|
||||
// play sound, reduce charges, start cooldown, and mark as dirty (if required).
|
||||
|
||||
_audio.PlayPredicted(action.Sound, performer,predicted ? performer : null);
|
||||
|
||||
var dirty = toggledBefore == action.Toggled;
|
||||
|
||||
@@ -580,6 +597,9 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
|
||||
if (dirty && component != null)
|
||||
Dirty(performer, component);
|
||||
|
||||
var ev = new ActionPerformedEvent(performer);
|
||||
RaiseLocalEvent(actionId, ref ev);
|
||||
}
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
|
||||
namespace Content.Shared.Administration;
|
||||
|
||||
[RegisterComponent, Access(typeof(AdminFrozenSystem))]
|
||||
[NetworkedComponent]
|
||||
[RegisterComponent, Access(typeof(SharedAdminFrozenSystem))]
|
||||
[NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class AdminFrozenComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the player is also muted.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Muted;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Emoting;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Pulling.Components;
|
||||
using Content.Shared.Movement.Pulling.Events;
|
||||
using Content.Shared.Movement.Pulling.Systems;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Throwing;
|
||||
|
||||
namespace Content.Shared.Administration;
|
||||
|
||||
public sealed class AdminFrozenSystem : EntitySystem
|
||||
public abstract class SharedAdminFrozenSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
|
||||
[Dependency] private readonly PullingSystem _pulling = default!;
|
||||
@@ -28,6 +30,16 @@ public sealed class AdminFrozenSystem : EntitySystem
|
||||
SubscribeLocalEvent<AdminFrozenComponent, PullAttemptEvent>(OnPullAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, AttackAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, ChangeDirectionAttemptEvent>(OnAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, EmoteAttemptEvent>(OnEmoteAttempt);
|
||||
SubscribeLocalEvent<AdminFrozenComponent, SpeakAttemptEvent>(OnSpeakAttempt);
|
||||
}
|
||||
|
||||
private void OnSpeakAttempt(EntityUid uid, AdminFrozenComponent component, SpeakAttemptEvent args)
|
||||
{
|
||||
if (!component.Muted)
|
||||
return;
|
||||
|
||||
args.Cancel();
|
||||
}
|
||||
|
||||
private void OnAttempt(EntityUid uid, AdminFrozenComponent component, CancellableEntityEventArgs args)
|
||||
@@ -62,4 +74,10 @@ public sealed class AdminFrozenSystem : EntitySystem
|
||||
{
|
||||
_blocker.UpdateCanMove(uid);
|
||||
}
|
||||
|
||||
private void OnEmoteAttempt(EntityUid uid, AdminFrozenComponent component, EmoteAttemptEvent args)
|
||||
{
|
||||
if (component.Muted)
|
||||
args.Cancel();
|
||||
}
|
||||
}
|
||||
10
Content.Shared/Armor/AllowSuitStorageComponent.cs
Normal file
10
Content.Shared/Armor/AllowSuitStorageComponent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Content.Shared.Armor;
|
||||
|
||||
/// <summary>
|
||||
/// Used on outerclothing to allow use of suit storage
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class AllowSuitStorageComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
33
Content.Shared/Atmos/GetFireProtectionEvent.cs
Normal file
33
Content.Shared/Atmos/GetFireProtectionEvent.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Content.Shared.Inventory;
|
||||
|
||||
namespace Content.Shared.Atmos;
|
||||
|
||||
/// <summary>
|
||||
/// Raised on a burning entity to check its fire protection.
|
||||
/// Damage taken is multiplied by the final amount, but not temperature.
|
||||
/// TemperatureProtection is needed for that.
|
||||
/// </summary>
|
||||
[ByRefEvent]
|
||||
public sealed class GetFireProtectionEvent : EntityEventArgs, IInventoryRelayEvent
|
||||
{
|
||||
public SlotFlags TargetSlots { get; } = ~SlotFlags.POCKET;
|
||||
|
||||
/// <summary>
|
||||
/// What to multiply the fire damage by.
|
||||
/// If this is 0 then it's ignored
|
||||
/// </summary>
|
||||
public float Multiplier;
|
||||
|
||||
public GetFireProtectionEvent()
|
||||
{
|
||||
Multiplier = 1f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reduce fire damage taken by a percentage.
|
||||
/// </summary>
|
||||
public void Reduce(float by)
|
||||
{
|
||||
Multiplier -= by;
|
||||
}
|
||||
}
|
||||
@@ -322,15 +322,17 @@ public partial class SharedBodySystem
|
||||
launchImpulseVariance:GibletLaunchImpulseVariance, launchCone: splatCone);
|
||||
}
|
||||
}
|
||||
|
||||
var bodyTransform = Transform(bodyId);
|
||||
if (TryComp<InventoryComponent>(bodyId, out var inventory))
|
||||
{
|
||||
foreach (var item in _inventory.GetHandOrInventoryEntities(bodyId))
|
||||
{
|
||||
SharedTransform.AttachToGridOrMap(item);
|
||||
SharedTransform.DropNextTo(item, (bodyId, bodyTransform));
|
||||
gibs.Add(item);
|
||||
}
|
||||
}
|
||||
_audioSystem.PlayPredicted(gibSoundOverride, Transform(bodyId).Coordinates, null);
|
||||
_audioSystem.PlayPredicted(gibSoundOverride, bodyTransform.Coordinates, null);
|
||||
return gibs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Shared.CCVar
|
||||
{
|
||||
@@ -1378,6 +1379,49 @@ namespace Content.Shared.CCVar
|
||||
public static readonly CVarDef<bool> GridFill =
|
||||
CVarDef.Create("shuttle.grid_fill", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// Whether to automatically preloading grids by GridPreloaderSystem
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool> PreloadGrids =
|
||||
CVarDef.Create("shuttle.preload_grids", true, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// How long the warmup time before FTL start should be.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> FTLStartupTime =
|
||||
CVarDef.Create("shuttle.startup_time", 5.5f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// How long a shuttle spends in FTL.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> FTLTravelTime =
|
||||
CVarDef.Create("shuttle.travel_time", 20f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// How long the final stage of FTL before arrival should be.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> FTLArrivalTime =
|
||||
CVarDef.Create("shuttle.arrival_time", 5f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// How much time needs to pass before a shuttle can FTL again.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> FTLCooldown =
|
||||
CVarDef.Create("shuttle.cooldown", 10f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// The maximum <see cref="PhysicsComponent.Mass"/> a grid can have before it becomes unable to FTL.
|
||||
/// Any value equal to or less than zero will disable this check.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> FTLMassLimit =
|
||||
CVarDef.Create("shuttle.mass_limit", 300f, CVar.SERVERONLY);
|
||||
|
||||
/// <summary>
|
||||
/// How long to knock down entities for if they aren't buckled when FTL starts and stops.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<float> HyperspaceKnockdownTime =
|
||||
CVarDef.Create("shuttle.hyperspace_knockdown_time", 5f, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* Emergency
|
||||
*/
|
||||
|
||||
@@ -60,7 +60,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
[Dependency] protected readonly SharedAppearanceSystem AppearanceSystem = default!;
|
||||
[Dependency] protected readonly SharedHandsSystem Hands = default!;
|
||||
[Dependency] protected readonly SharedContainerSystem ContainerSystem = default!;
|
||||
[Dependency] protected readonly MetaDataSystem MetaData = default!;
|
||||
[Dependency] protected readonly MetaDataSystem MetaDataSys = default!;
|
||||
[Dependency] protected readonly INetManager NetManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -1123,7 +1123,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
Dirty(uid, container);
|
||||
return solution;
|
||||
}
|
||||
|
||||
|
||||
private Entity<SolutionComponent, ContainedSolutionComponent> SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 maxVol, Solution prototype)
|
||||
{
|
||||
var coords = new EntityCoordinates(container.Owner, Vector2.Zero);
|
||||
@@ -1135,7 +1135,7 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
|
||||
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
|
||||
AddComp(uid, relation);
|
||||
|
||||
MetaData.SetEntityName(uid, $"solution - {name}");
|
||||
MetaDataSys.SetEntityName(uid, $"solution - {name}");
|
||||
ContainerSystem.Insert(uid, container, force: true);
|
||||
|
||||
return (uid, solution, relation);
|
||||
|
||||
@@ -17,7 +17,6 @@ namespace Content.Shared.Chemistry.EntitySystems;
|
||||
/// </summary>
|
||||
public sealed class SolutionTransferSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solution = default!;
|
||||
|
||||
@@ -6,6 +6,9 @@ namespace Content.Shared.Chemistry.Reaction
|
||||
{
|
||||
public interface ITileReaction
|
||||
{
|
||||
FixedPoint2 TileReact(TileRef tile, ReagentPrototype reagent, FixedPoint2 reactVolume);
|
||||
FixedPoint2 TileReact(TileRef tile,
|
||||
ReagentPrototype reagent,
|
||||
FixedPoint2 reactVolume,
|
||||
IEntityManager entityManager);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace Content.Shared.Chemistry.Reagent
|
||||
[DataField]
|
||||
public SoundSpecifier FootstepSound = new SoundCollectionSpecifier("FootstepWater", AudioParams.Default.WithVolume(6));
|
||||
|
||||
public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume)
|
||||
public FixedPoint2 ReactionTile(TileRef tile, FixedPoint2 reactVolume, IEntityManager entityManager)
|
||||
{
|
||||
var removed = FixedPoint2.Zero;
|
||||
|
||||
@@ -151,7 +151,7 @@ namespace Content.Shared.Chemistry.Reagent
|
||||
|
||||
foreach (var reaction in TileReactions)
|
||||
{
|
||||
removed += reaction.TileReact(tile, this, reactVolume - removed);
|
||||
removed += reaction.TileReact(tile, this, reactVolume - removed, entityManager);
|
||||
|
||||
if (removed > reactVolume)
|
||||
throw new Exception("Removed more than we have!");
|
||||
|
||||
@@ -20,6 +20,46 @@ namespace Content.Shared.Chemistry
|
||||
{
|
||||
ReagentDispenserDispenseAmount = amount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance from interpreting a String as an integer,
|
||||
/// throwing an exception if it is unable to parse.
|
||||
/// </summary>
|
||||
public ReagentDispenserSetDispenseAmountMessage(String s)
|
||||
{
|
||||
switch (s)
|
||||
{
|
||||
case "1":
|
||||
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U1;
|
||||
break;
|
||||
case "5":
|
||||
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U5;
|
||||
break;
|
||||
case "10":
|
||||
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U10;
|
||||
break;
|
||||
case "15":
|
||||
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U15;
|
||||
break;
|
||||
case "20":
|
||||
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U20;
|
||||
break;
|
||||
case "25":
|
||||
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U25;
|
||||
break;
|
||||
case "30":
|
||||
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U30;
|
||||
break;
|
||||
case "50":
|
||||
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U50;
|
||||
break;
|
||||
case "100":
|
||||
ReagentDispenserDispenseAmount = ReagentDispenserDispenseAmount.U100;
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"Cannot convert the string `{s}` into a valid ReagentDispenser DispenseAmount");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
@@ -52,20 +92,30 @@ namespace Content.Shared.Chemistry
|
||||
U100 = 100,
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ReagentInventoryItem(string storageSlotId, string reagentLabel, string storedAmount, Color reagentColor)
|
||||
{
|
||||
public string StorageSlotId = storageSlotId;
|
||||
public string ReagentLabel = reagentLabel;
|
||||
public string StoredAmount = storedAmount;
|
||||
public Color ReagentColor = reagentColor;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ReagentDispenserBoundUserInterfaceState : BoundUserInterfaceState
|
||||
{
|
||||
public readonly ContainerInfo? OutputContainer;
|
||||
|
||||
public readonly NetEntity? OutputContainerEntity;
|
||||
|
||||
/// <summary>
|
||||
/// A list of the reagents which this dispenser can dispense.
|
||||
/// </summary>
|
||||
public readonly List<KeyValuePair<string, KeyValuePair<string, string>>> Inventory;
|
||||
public readonly List<ReagentInventoryItem> Inventory;
|
||||
|
||||
public readonly ReagentDispenserDispenseAmount SelectedDispenseAmount;
|
||||
|
||||
public ReagentDispenserBoundUserInterfaceState(ContainerInfo? outputContainer, NetEntity? outputContainerEntity, List<KeyValuePair<string, KeyValuePair<string, string>>> inventory, ReagentDispenserDispenseAmount selectedDispenseAmount)
|
||||
public ReagentDispenserBoundUserInterfaceState(ContainerInfo? outputContainer, NetEntity? outputContainerEntity, List<ReagentInventoryItem> inventory, ReagentDispenserDispenseAmount selectedDispenseAmount)
|
||||
{
|
||||
OutputContainer = outputContainer;
|
||||
OutputContainerEntity = outputContainerEntity;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Content.Shared.Clothing.EntitySystems;
|
||||
|
||||
namespace Content.Shared.Clothing.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Makes this clothing reduce fire damage when worn.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(FireProtectionSystem))]
|
||||
public sealed partial class FireProtectionComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Percentage to reduce fire damage by, subtracted not multiplicative.
|
||||
/// 0.25 means 25% less fire damage.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public float Reduction;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Clothing.Components;
|
||||
|
||||
@@ -10,7 +11,20 @@ public sealed partial class LoadoutComponent : Component
|
||||
/// <summary>
|
||||
/// A list of starting gears, of which one will be given.
|
||||
/// All elements are weighted the same in the list.
|
||||
///
|
||||
/// If not specified, <see cref="RoleLoadout"/> will be used instead.
|
||||
/// </summary>
|
||||
[DataField("prototypes", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<StartingGearPrototype>)), AutoNetworkedField]
|
||||
public List<string>? Prototypes;
|
||||
[DataField("prototypes")]
|
||||
[AutoNetworkedField]
|
||||
public List<ProtoId<StartingGearPrototype>>? StartingGear;
|
||||
|
||||
/// <summary>
|
||||
/// A list of role loadouts, of which one will be given.
|
||||
/// All elements are weighted the same in the list.
|
||||
///
|
||||
/// If not specified, <see cref="StartingGear"/> will be used instead.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[AutoNetworkedField]
|
||||
public List<ProtoId<RoleLoadoutPrototype>>? RoleLoadout;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Inventory;
|
||||
|
||||
namespace Content.Shared.Clothing.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles reducing fire damage when wearing clothing with <see cref="FireProtectionComponent"/>.
|
||||
/// </summary>
|
||||
public sealed class FireProtectionSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<FireProtectionComponent, InventoryRelayedEvent<GetFireProtectionEvent>>(OnGetProtection);
|
||||
}
|
||||
|
||||
private void OnGetProtection(Entity<FireProtectionComponent> ent, ref InventoryRelayedEvent<GetFireProtectionEvent> args)
|
||||
{
|
||||
args.Args.Reduce(ent.Comp.Reduction);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using Robust.Shared.Random;
|
||||
namespace Content.Shared.Clothing;
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a loadout to an entity based on the startingGear prototype
|
||||
/// Assigns a loadout to an entity based on the RoleLoadout prototype
|
||||
/// </summary>
|
||||
public sealed class LoadoutSystem : EntitySystem
|
||||
{
|
||||
@@ -110,10 +110,22 @@ public sealed class LoadoutSystem : EntitySystem
|
||||
|
||||
private void OnMapInit(EntityUid uid, LoadoutComponent component, MapInitEvent args)
|
||||
{
|
||||
if (component.Prototypes == null)
|
||||
// Use starting gear if specified
|
||||
if (component.StartingGear != null)
|
||||
{
|
||||
var gear = _protoMan.Index(_random.Pick(component.StartingGear));
|
||||
_station.EquipStartingGear(uid, gear);
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.RoleLoadout == null)
|
||||
return;
|
||||
|
||||
var proto = _protoMan.Index<StartingGearPrototype>(_random.Pick(component.Prototypes));
|
||||
_station.EquipStartingGear(uid, proto);
|
||||
// ...otherwise equip from role loadout
|
||||
var id = _random.Pick(component.RoleLoadout);
|
||||
var proto = _protoMan.Index(id);
|
||||
var loadout = new RoleLoadout(id);
|
||||
loadout.SetDefault(_protoMan, true);
|
||||
_station.EquipRoleLoadout(uid, loadout, proto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Content.Shared.CriminalRecords.Systems;
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.CriminalRecords.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Lets the user hack a criminal records console, once.
|
||||
/// Everyone is set to wanted with a randomly picked reason.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedCriminalRecordsHackerSystem))]
|
||||
public sealed partial class CriminalRecordsHackerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How long the doafter is for hacking it.
|
||||
/// </summary>
|
||||
public TimeSpan Delay = TimeSpan.FromSeconds(20);
|
||||
|
||||
/// <summary>
|
||||
/// Dataset of random reasons to use.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<DatasetPrototype> Reasons = "CriminalRecordsWantedReasonPlaceholders";
|
||||
|
||||
/// <summary>
|
||||
/// Announcement made after the console is hacked.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId Announcement = "ninja-criminal-records-hack-announcement";
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using Content.Shared.CriminalRecords.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Ninja.Systems;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.CriminalRecords.Systems;
|
||||
|
||||
public abstract class SharedCriminalRecordsHackerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CriminalRecordsHackerComponent, BeforeInteractHandEvent>(OnBeforeInteractHand);
|
||||
}
|
||||
|
||||
private void OnBeforeInteractHand(Entity<CriminalRecordsHackerComponent> ent, ref BeforeInteractHandEvent args)
|
||||
{
|
||||
// TODO: generic event
|
||||
if (args.Handled || !_gloves.AbilityCheck(ent, args, out var target))
|
||||
return;
|
||||
|
||||
if (!HasComp<CriminalRecordsConsoleComponent>(target))
|
||||
return;
|
||||
|
||||
var doAfterArgs = new DoAfterArgs(EntityManager, ent, ent.Comp.Delay, new CriminalRecordsHackDoAfterEvent(), target: target, used: ent, eventTarget: ent)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnMove = true,
|
||||
MovementThreshold = 0.5f
|
||||
};
|
||||
|
||||
_doAfter.TryStartDoAfter(doAfterArgs);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised on the user when the doafter completes.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class CriminalRecordsHackDoAfterEvent : SimpleDoAfterEvent
|
||||
{
|
||||
}
|
||||
@@ -13,6 +13,7 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
|
||||
public const string InvokedPort = "link_port";
|
||||
|
||||
@@ -529,7 +530,7 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
|
||||
private bool InRange(EntityUid sourceUid, EntityUid sinkUid, float range)
|
||||
{
|
||||
// TODO: This should be using an existing method and also coordinates inrange instead.
|
||||
return Transform(sourceUid).MapPosition.InRange(Transform(sinkUid).MapPosition, range);
|
||||
return _transform.GetMapCoordinates(sourceUid).InRange(_transform.GetMapCoordinates(sinkUid), range);
|
||||
}
|
||||
|
||||
private void SendNewLinkEvent(EntityUid? user, EntityUid sourceUid, string source, EntityUid sinkUid, string sink)
|
||||
|
||||
@@ -210,8 +210,8 @@ namespace Content.Shared.Examine
|
||||
public bool InRangeUnOccluded(EntityUid origin, EntityUid other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var originPos = entMan.GetComponent<TransformComponent>(origin).MapPosition;
|
||||
var otherPos = entMan.GetComponent<TransformComponent>(other).MapPosition;
|
||||
var originPos = _transform.GetMapCoordinates(origin);
|
||||
var otherPos = _transform.GetMapCoordinates(other);
|
||||
|
||||
return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker);
|
||||
}
|
||||
@@ -219,7 +219,7 @@ namespace Content.Shared.Examine
|
||||
public bool InRangeUnOccluded(EntityUid origin, EntityCoordinates other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var originPos = entMan.GetComponent<TransformComponent>(origin).MapPosition;
|
||||
var originPos = _transform.GetMapCoordinates(origin);
|
||||
var otherPos = other.ToMap(entMan, _transform);
|
||||
|
||||
return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker);
|
||||
@@ -228,7 +228,7 @@ namespace Content.Shared.Examine
|
||||
public bool InRangeUnOccluded(EntityUid origin, MapCoordinates other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
|
||||
{
|
||||
var entMan = IoCManager.Resolve<IEntityManager>();
|
||||
var originPos = entMan.GetComponent<TransformComponent>(origin).MapPosition;
|
||||
var originPos = _transform.GetMapCoordinates(origin);
|
||||
|
||||
return InRangeUnOccluded(originPos, other, range, predicate, ignoreInsideBlocker);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,11 @@ public sealed partial class BlindableComponent : Component
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("EyeDamage"), AutoNetworkedField]
|
||||
public int EyeDamage = 0;
|
||||
|
||||
public const int MaxDamage = 9;
|
||||
[ViewVariables(VVAccess.ReadOnly), DataField]
|
||||
public int MaxDamage = 9;
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly), DataField]
|
||||
public int MinDamage = 0;
|
||||
|
||||
/// <description>
|
||||
/// Used to ensure that this doesn't break with sandbox or admin tools.
|
||||
|
||||
@@ -37,7 +37,7 @@ public sealed class BlindableSystem : EntitySystem
|
||||
var old = blindable.Comp.IsBlind;
|
||||
|
||||
// Don't bother raising an event if the eye is too damaged.
|
||||
if (blindable.Comp.EyeDamage >= BlindableComponent.MaxDamage)
|
||||
if (blindable.Comp.EyeDamage >= blindable.Comp.MaxDamage)
|
||||
{
|
||||
blindable.Comp.IsBlind = true;
|
||||
}
|
||||
@@ -62,13 +62,31 @@ public sealed class BlindableSystem : EntitySystem
|
||||
return;
|
||||
|
||||
blindable.Comp.EyeDamage += amount;
|
||||
blindable.Comp.EyeDamage = Math.Clamp(blindable.Comp.EyeDamage, 0, BlindableComponent.MaxDamage);
|
||||
Dirty(blindable);
|
||||
UpdateIsBlind(blindable);
|
||||
UpdateEyeDamage(blindable, true);
|
||||
}
|
||||
private void UpdateEyeDamage(Entity<BlindableComponent?> blindable, bool isDamageChanged)
|
||||
{
|
||||
if (!Resolve(blindable, ref blindable.Comp, false))
|
||||
return;
|
||||
|
||||
var previousDamage = blindable.Comp.EyeDamage;
|
||||
blindable.Comp.EyeDamage = Math.Clamp(blindable.Comp.EyeDamage, blindable.Comp.MinDamage, blindable.Comp.MaxDamage);
|
||||
Dirty(blindable);
|
||||
if (!isDamageChanged && previousDamage == blindable.Comp.EyeDamage)
|
||||
return;
|
||||
|
||||
UpdateIsBlind(blindable);
|
||||
var ev = new EyeDamageChangedEvent(blindable.Comp.EyeDamage);
|
||||
RaiseLocalEvent(blindable.Owner, ref ev);
|
||||
}
|
||||
public void SetMinDamage(Entity<BlindableComponent?> blindable, int amount)
|
||||
{
|
||||
if (!Resolve(blindable, ref blindable.Comp, false))
|
||||
return;
|
||||
|
||||
blindable.Comp.MinDamage = amount;
|
||||
UpdateEyeDamage(blindable, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Eye.Blinding.Components;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
@@ -124,7 +123,7 @@ public sealed class EyeClosingSystem : EntitySystem
|
||||
if (_entityManager.TryGetComponent<EyeClosingComponent>(blindable, out var eyelids) && !eyelids.NaturallyCreated)
|
||||
return;
|
||||
|
||||
if (ev.Blur < BlurryVisionComponent.MaxMagnitude || ev.Blur >= BlindableComponent.MaxDamage)
|
||||
if (ev.Blur < BlurryVisionComponent.MaxMagnitude || ev.Blur >= blindable.Comp.MaxDamage)
|
||||
{
|
||||
RemCompDeferred<EyeClosingComponent>(blindable);
|
||||
return;
|
||||
|
||||
@@ -101,4 +101,6 @@ public sealed partial class ToggleLightingActionEvent : InstantActionEvent { }
|
||||
|
||||
public sealed partial class ToggleGhostHearingActionEvent : InstantActionEvent { }
|
||||
|
||||
public sealed partial class ToggleGhostVisibilityToAllEvent : InstantActionEvent { }
|
||||
|
||||
public sealed partial class BooActionEvent : InstantActionEvent { }
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.GridPreloader.Prototypes;
|
||||
|
||||
/// <summary>
|
||||
/// Creating this prototype will automatically load the grid at the specified path at the beginning of the round,
|
||||
/// and allow the GridPreloader system to load them in the middle of the round. This is needed for optimization,
|
||||
/// because loading grids in the middle of a round causes the server to lag.
|
||||
/// </summary>
|
||||
[Prototype("preloadedGrid")]
|
||||
public sealed partial class PreloadedGridPrototype : IPrototype
|
||||
{
|
||||
[IdDataField] public string ID { get; } = string.Empty;
|
||||
|
||||
[DataField(required: true)]
|
||||
public ResPath Path;
|
||||
|
||||
[DataField]
|
||||
public int Copies = 1;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
namespace Content.Shared.GridPreloader.Systems;
|
||||
public abstract class SharedGridPreloaderSystem : EntitySystem
|
||||
{
|
||||
}
|
||||
@@ -120,17 +120,12 @@ public abstract partial class SharedHandsSystem
|
||||
return true;
|
||||
|
||||
var userXform = Transform(uid);
|
||||
var isInContainer = ContainerSystem.IsEntityInContainer(uid);
|
||||
var isInContainer = ContainerSystem.IsEntityOrParentInContainer(uid, xform: userXform);
|
||||
|
||||
if (targetDropLocation == null || isInContainer)
|
||||
{
|
||||
// If user is in a container, drop item into that container. Otherwise, attach to grid or map.\
|
||||
// TODO recursively check upwards for containers
|
||||
|
||||
if (!isInContainer
|
||||
|| !ContainerSystem.TryGetContainingContainer(userXform.ParentUid, uid, out var container, skipExistCheck: true)
|
||||
|| !ContainerSystem.Insert((entity, itemXform), container))
|
||||
TransformSystem.AttachToGridOrMap(entity, itemXform);
|
||||
// If user is in a container, drop item into that container. Otherwise, attach to grid or map.
|
||||
TransformSystem.DropNextTo((entity, itemXform), (uid, userXform));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,10 +108,10 @@ public abstract partial class SharedHandsSystem : EntitySystem
|
||||
var xform = Transform(uid);
|
||||
var coordinateEntity = xform.ParentUid.IsValid() ? xform.ParentUid : uid;
|
||||
var itemXform = Transform(entity);
|
||||
var itemPos = itemXform.MapPosition;
|
||||
var itemPos = TransformSystem.GetMapCoordinates(entity, xform: itemXform);
|
||||
|
||||
if (itemPos.MapId == xform.MapID
|
||||
&& (itemPos.Position - xform.MapPosition.Position).Length() <= MaxAnimationRange
|
||||
&& (itemPos.Position - TransformSystem.GetMapCoordinates(uid, xform: xform).Position).Length() <= MaxAnimationRange
|
||||
&& MetaData(entity).VisibilityMask == MetaData(uid).VisibilityMask) // Don't animate aghost pickups.
|
||||
{
|
||||
var initialPosition = EntityCoordinates.FromMap(coordinateEntity, itemPos, TransformSystem, EntityManager);
|
||||
|
||||
23
Content.Shared/HealthExaminable/HealthExaminableComponent.cs
Normal file
23
Content.Shared/HealthExaminable/HealthExaminableComponent.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.HealthExaminable;
|
||||
|
||||
[RegisterComponent, Access(typeof(HealthExaminableSystem))]
|
||||
public sealed partial class HealthExaminableComponent : Component
|
||||
{
|
||||
public List<FixedPoint2> Thresholds = new()
|
||||
{ FixedPoint2.New(10), FixedPoint2.New(25), FixedPoint2.New(50), FixedPoint2.New(75) };
|
||||
|
||||
[DataField(required: true)]
|
||||
public HashSet<ProtoId<DamageTypePrototype>> ExaminableTypes = default!;
|
||||
|
||||
/// <summary>
|
||||
/// Health examine text is automatically generated through creating loc string IDs, in the form:
|
||||
/// `health-examine-[prefix]-[type]-[threshold]`
|
||||
/// This part determines the prefix.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string LocPrefix = "carbon";
|
||||
}
|
||||
116
Content.Shared/HealthExaminable/HealthExaminableSystem.cs
Normal file
116
Content.Shared/HealthExaminable/HealthExaminableSystem.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.HealthExaminable;
|
||||
|
||||
public sealed class HealthExaminableSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HealthExaminableComponent, GetVerbsEvent<ExamineVerb>>(OnGetExamineVerbs);
|
||||
}
|
||||
|
||||
private void OnGetExamineVerbs(EntityUid uid, HealthExaminableComponent component, GetVerbsEvent<ExamineVerb> args)
|
||||
{
|
||||
if (!TryComp<DamageableComponent>(uid, out var damage))
|
||||
return;
|
||||
|
||||
var detailsRange = _examineSystem.IsInDetailsRange(args.User, uid);
|
||||
|
||||
var verb = new ExamineVerb()
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
var markup = CreateMarkup(uid, component, damage);
|
||||
_examineSystem.SendExamineTooltip(args.User, uid, markup, false, false);
|
||||
},
|
||||
Text = Loc.GetString("health-examinable-verb-text"),
|
||||
Category = VerbCategory.Examine,
|
||||
Disabled = !detailsRange,
|
||||
Message = detailsRange ? null : Loc.GetString("health-examinable-verb-disabled"),
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/rejuvenate.svg.192dpi.png"))
|
||||
};
|
||||
|
||||
args.Verbs.Add(verb);
|
||||
}
|
||||
|
||||
public FormattedMessage CreateMarkup(EntityUid uid, HealthExaminableComponent component, DamageableComponent damage)
|
||||
{
|
||||
var msg = new FormattedMessage();
|
||||
|
||||
var first = true;
|
||||
foreach (var type in component.ExaminableTypes)
|
||||
{
|
||||
if (!damage.Damage.DamageDict.TryGetValue(type, out var dmg))
|
||||
continue;
|
||||
|
||||
if (dmg == FixedPoint2.Zero)
|
||||
continue;
|
||||
|
||||
FixedPoint2 closest = FixedPoint2.Zero;
|
||||
|
||||
string chosenLocStr = string.Empty;
|
||||
foreach (var threshold in component.Thresholds)
|
||||
{
|
||||
var str = $"health-examinable-{component.LocPrefix}-{type}-{threshold}";
|
||||
var tempLocStr = Loc.GetString($"health-examinable-{component.LocPrefix}-{type}-{threshold}", ("target", Identity.Entity(uid, EntityManager)));
|
||||
|
||||
// i.e., this string doesn't exist, because theres nothing for that threshold
|
||||
if (tempLocStr == str)
|
||||
continue;
|
||||
|
||||
if (dmg > threshold && threshold > closest)
|
||||
{
|
||||
chosenLocStr = tempLocStr;
|
||||
closest = threshold;
|
||||
}
|
||||
}
|
||||
|
||||
if (closest == FixedPoint2.Zero)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
{
|
||||
msg.PushNewline();
|
||||
}
|
||||
else
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
msg.AddMarkup(chosenLocStr);
|
||||
}
|
||||
|
||||
if (msg.IsEmpty)
|
||||
{
|
||||
msg.AddMarkup(Loc.GetString($"health-examinable-{component.LocPrefix}-none"));
|
||||
}
|
||||
|
||||
// Anything else want to add on to this?
|
||||
RaiseLocalEvent(uid, new HealthBeingExaminedEvent(msg), true);
|
||||
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class raised on an entity whose health is being examined
|
||||
/// in order to add special text that is not handled by the
|
||||
/// damage thresholds.
|
||||
/// </summary>
|
||||
public sealed class HealthBeingExaminedEvent
|
||||
{
|
||||
public FormattedMessage Message;
|
||||
|
||||
public HealthBeingExaminedEvent(FormattedMessage message)
|
||||
{
|
||||
Message = message;
|
||||
}
|
||||
}
|
||||
@@ -9,8 +9,29 @@ namespace Content.Shared.Humanoid;
|
||||
|
||||
[DataDefinition]
|
||||
[Serializable, NetSerializable]
|
||||
public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
|
||||
public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, IEquatable<HumanoidCharacterAppearance>
|
||||
{
|
||||
[DataField("hair")]
|
||||
public string HairStyleId { get; set; } = HairStyles.DefaultHairStyle;
|
||||
|
||||
[DataField]
|
||||
public Color HairColor { get; set; } = Color.Black;
|
||||
|
||||
[DataField("facialHair")]
|
||||
public string FacialHairStyleId { get; set; } = HairStyles.DefaultFacialHairStyle;
|
||||
|
||||
[DataField]
|
||||
public Color FacialHairColor { get; set; } = Color.Black;
|
||||
|
||||
[DataField]
|
||||
public Color EyeColor { get; set; } = Color.Black;
|
||||
|
||||
[DataField]
|
||||
public Color SkinColor { get; set; } = Humanoid.SkinColor.ValidHumanSkinTone;
|
||||
|
||||
[DataField]
|
||||
public List<Marking> Markings { get; set; } = new();
|
||||
|
||||
public HumanoidCharacterAppearance(string hairStyleId,
|
||||
Color hairColor,
|
||||
string facialHairStyleId,
|
||||
@@ -28,26 +49,11 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
|
||||
Markings = markings;
|
||||
}
|
||||
|
||||
[DataField("hair")]
|
||||
public string HairStyleId { get; private set; }
|
||||
public HumanoidCharacterAppearance(HumanoidCharacterAppearance other) :
|
||||
this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings))
|
||||
{
|
||||
|
||||
[DataField("hairColor")]
|
||||
public Color HairColor { get; private set; }
|
||||
|
||||
[DataField("facialHair")]
|
||||
public string FacialHairStyleId { get; private set; }
|
||||
|
||||
[DataField("facialHairColor")]
|
||||
public Color FacialHairColor { get; private set; }
|
||||
|
||||
[DataField("eyeColor")]
|
||||
public Color EyeColor { get; private set; }
|
||||
|
||||
[DataField("skinColor")]
|
||||
public Color SkinColor { get; private set; }
|
||||
|
||||
[DataField("markings")]
|
||||
public List<Marking> Markings { get; private set; }
|
||||
}
|
||||
|
||||
public HumanoidCharacterAppearance WithHairStyleName(string newName)
|
||||
{
|
||||
@@ -84,18 +90,6 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
|
||||
return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings);
|
||||
}
|
||||
|
||||
public HumanoidCharacterAppearance() : this(
|
||||
HairStyles.DefaultHairStyle,
|
||||
Color.Black,
|
||||
HairStyles.DefaultFacialHairStyle,
|
||||
Color.Black,
|
||||
Color.Black,
|
||||
Humanoid.SkinColor.ValidHumanSkinTone,
|
||||
new ()
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public static HumanoidCharacterAppearance DefaultWithSpecies(string species)
|
||||
{
|
||||
var speciesPrototype = IoCManager.Resolve<IPrototypeManager>().Index<SpeciesPrototype>(species);
|
||||
@@ -245,4 +239,32 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
|
||||
if (!Markings.SequenceEqual(other.Markings)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Equals(HumanoidCharacterAppearance? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return HairStyleId == other.HairStyleId &&
|
||||
HairColor.Equals(other.HairColor) &&
|
||||
FacialHairStyleId == other.FacialHairStyleId &&
|
||||
FacialHairColor.Equals(other.FacialHairColor) &&
|
||||
EyeColor.Equals(other.EyeColor) &&
|
||||
SkinColor.Equals(other.SkinColor) &&
|
||||
Markings.SequenceEqual(other.Markings);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is HumanoidCharacterAppearance other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings);
|
||||
}
|
||||
|
||||
public HumanoidCharacterAppearance Clone()
|
||||
{
|
||||
return new(this);
|
||||
}
|
||||
}
|
||||
|
||||
19
Content.Shared/Humanoid/HumanoidProfileExport.cs
Normal file
19
Content.Shared/Humanoid/HumanoidProfileExport.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Content.Shared.Preferences;
|
||||
|
||||
namespace Content.Shared.Humanoid;
|
||||
|
||||
/// <summary>
|
||||
/// Holds all of the data for importing / exporting character profiles.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed partial class HumanoidProfileExport
|
||||
{
|
||||
[DataField]
|
||||
public string ForkId;
|
||||
|
||||
[DataField]
|
||||
public int Version = 1;
|
||||
|
||||
[DataField(required: true)]
|
||||
public HumanoidCharacterProfile Profile = default!;
|
||||
}
|
||||
@@ -1,13 +1,22 @@
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Decals;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.GameObjects.Components.Localization;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Serialization.Markdown;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
namespace Content.Shared.Humanoid;
|
||||
|
||||
@@ -22,8 +31,10 @@ namespace Content.Shared.Humanoid;
|
||||
/// </summary>
|
||||
public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly ISerializationManager _serManager = default!;
|
||||
[Dependency] private readonly MarkingManager _markingManager = default!;
|
||||
|
||||
[ValidatePrototypeId<SpeciesPrototype>]
|
||||
@@ -37,6 +48,37 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
|
||||
SubscribeLocalEvent<HumanoidAppearanceComponent, ExaminedEvent>(OnExamined);
|
||||
}
|
||||
|
||||
public DataNode ToDataNode(HumanoidCharacterProfile profile)
|
||||
{
|
||||
var export = new HumanoidProfileExport()
|
||||
{
|
||||
ForkId = _cfgManager.GetCVar(CVars.BuildForkId),
|
||||
Profile = profile,
|
||||
};
|
||||
|
||||
var dataNode = _serManager.WriteValue(export, alwaysWrite: true, notNullableOverride: true);
|
||||
return dataNode;
|
||||
}
|
||||
|
||||
public HumanoidCharacterProfile FromStream(Stream stream, ICommonSession session)
|
||||
{
|
||||
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
|
||||
var yamlStream = new YamlStream();
|
||||
yamlStream.Load(reader);
|
||||
|
||||
var root = yamlStream.Documents[0].RootNode;
|
||||
var export = _serManager.Read<HumanoidProfileExport>(root.ToDataNode(), notNullableOverride: true);
|
||||
|
||||
/*
|
||||
* Add custom handling here for forks / version numbers if you care.
|
||||
*/
|
||||
|
||||
var profile = export.Profile;
|
||||
var collection = IoCManager.Instance;
|
||||
profile.EnsureValid(session, collection!);
|
||||
return profile;
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, HumanoidAppearanceComponent humanoid, ComponentInit args)
|
||||
{
|
||||
if (string.IsNullOrEmpty(humanoid.Species) || _netManager.IsClient && !IsClientSide(uid))
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace Content.Shared.Interaction
|
||||
if (!Resolve(user, ref xform))
|
||||
return false;
|
||||
|
||||
var diff = coordinates - xform.MapPosition.Position;
|
||||
var diff = coordinates - _transform.GetMapCoordinates(user, xform: xform).Position;
|
||||
if (diff.LengthSquared() <= 0.01f)
|
||||
return true;
|
||||
|
||||
|
||||
@@ -665,14 +665,14 @@ namespace Content.Shared.Interaction
|
||||
else
|
||||
{
|
||||
// We'll still do the raycast from the centres but we'll bump the range as we know they're in range.
|
||||
originPos = xformA.MapPosition;
|
||||
originPos = _transform.GetMapCoordinates(origin, xform: xformA);
|
||||
range = (originPos.Position - targetPos.Position).Length();
|
||||
}
|
||||
}
|
||||
// No fixtures, e.g. wallmounts.
|
||||
else
|
||||
{
|
||||
originPos = Transform(origin).MapPosition;
|
||||
originPos = _transform.GetMapCoordinates(origin);
|
||||
var otherParent = Transform(other).ParentUid;
|
||||
targetRot = otherParent.IsValid() ? Transform(otherParent).LocalRotation + otherAngle : otherAngle;
|
||||
}
|
||||
@@ -826,7 +826,7 @@ namespace Content.Shared.Interaction
|
||||
bool popup = false)
|
||||
{
|
||||
Ignored combinedPredicate = e => e == origin || (predicate?.Invoke(e) ?? false);
|
||||
var originPosition = Transform(origin).MapPosition;
|
||||
var originPosition = _transform.GetMapCoordinates(origin);
|
||||
var inRange = InRangeUnobstructed(originPosition, other, range, collisionMask, combinedPredicate, ShouldCheckAccess(origin));
|
||||
|
||||
if (!inRange && popup && _gameTiming.IsFirstTimePredicted)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Armor;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Hands;
|
||||
@@ -114,7 +115,7 @@ public abstract partial class InventorySystem
|
||||
if (!_handsSystem.CanDropHeld(actor, hands.ActiveHand!, checkActionBlocker: false))
|
||||
return;
|
||||
|
||||
RaiseLocalEvent(held.Value, new HandDeselectedEvent(actor), false);
|
||||
RaiseLocalEvent(held.Value, new HandDeselectedEvent(actor));
|
||||
|
||||
TryEquip(actor, actor, held.Value, ev.Slot, predicted: true, inventory: inventory, force: true, checkDoafter:true);
|
||||
}
|
||||
@@ -243,8 +244,16 @@ public abstract partial class InventorySystem
|
||||
return false;
|
||||
|
||||
DebugTools.Assert(slotDefinition.Name == slot);
|
||||
if (slotDefinition.DependsOn != null && !TryGetSlotEntity(target, slotDefinition.DependsOn, out _, inventory))
|
||||
return false;
|
||||
if (slotDefinition.DependsOn != null)
|
||||
{
|
||||
if (!TryGetSlotEntity(target, slotDefinition.DependsOn, out EntityUid? slotEntity, inventory))
|
||||
return false;
|
||||
|
||||
if (slotDefinition.DependsOnComponents is { } componentRegistry)
|
||||
foreach (var (_, entry) in componentRegistry)
|
||||
if (!HasComp(slotEntity, entry.Component.GetType()))
|
||||
return false;
|
||||
}
|
||||
|
||||
var fittingInPocket = slotDefinition.SlotFlags.HasFlag(SlotFlags.POCKET) &&
|
||||
item != null &&
|
||||
@@ -301,7 +310,6 @@ public abstract partial class InventorySystem
|
||||
reason = itemAttemptEvent.Reason ?? reason;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public partial class InventorySystem : EntitySystem
|
||||
private void InitializeSlots()
|
||||
{
|
||||
SubscribeLocalEvent<InventoryComponent, ComponentInit>(OnInit);
|
||||
SubscribeNetworkEvent<OpenSlotStorageNetworkMessage>(OnOpenSlotStorage);
|
||||
SubscribeAllEvent<OpenSlotStorageNetworkMessage>(OnOpenSlotStorage);
|
||||
|
||||
_vvm.GetTypeHandler<InventoryComponent>()
|
||||
.AddHandler(HandleViewVariablesSlots, ListViewVariablesSlots);
|
||||
|
||||
@@ -34,6 +34,8 @@ public sealed partial class SlotDefinition
|
||||
|
||||
[DataField("dependsOn")] public string? DependsOn { get; private set; }
|
||||
|
||||
[DataField("dependsOnComponents")] public ComponentRegistry? DependsOnComponents { get; private set; }
|
||||
|
||||
[DataField("displayName", required: true)]
|
||||
public string DisplayName { get; private set; } = string.Empty;
|
||||
|
||||
|
||||
@@ -21,12 +21,18 @@ public sealed partial class LockComponent : Component
|
||||
public bool Locked = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the lock is toggled by simply clicking.
|
||||
/// Whether or not the lock is locked by simply clicking.
|
||||
/// </summary>
|
||||
[DataField("lockOnClick"), ViewVariables(VVAccess.ReadWrite)]
|
||||
[AutoNetworkedField]
|
||||
public bool LockOnClick;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the lock is unlocked by simply clicking.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool UnlockOnClick = true;
|
||||
|
||||
/// <summary>
|
||||
/// The sound played when unlocked.
|
||||
/// </summary>
|
||||
|
||||
@@ -64,12 +64,12 @@ public sealed class LockSystem : EntitySystem
|
||||
//CrystallPunk LockSystem Adapt
|
||||
|
||||
// Only attempt an unlock by default on Activate
|
||||
//if (lockComp.Locked)
|
||||
//if (lockComp.Locked && lockComp.UnlockOnClick)
|
||||
//{
|
||||
// TryUnlock(uid, args.User, lockComp);
|
||||
// args.Handled = true;
|
||||
//}
|
||||
//else if (lockComp.LockOnClick)
|
||||
//else if (!lockComp.Locked && lockComp.LockOnClick)
|
||||
//{
|
||||
// TryLock(uid, args.User, lockComp);
|
||||
// args.Handled = true;
|
||||
@@ -228,6 +228,18 @@ public sealed class LockSystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the entity is locked.
|
||||
/// Entities with no lock component are considered unlocked.
|
||||
/// </summary>
|
||||
public bool IsLocked(Entity<LockComponent?> ent)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp, false))
|
||||
return false;
|
||||
|
||||
return ent.Comp.Locked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises an event for other components to check whether or not
|
||||
/// the entity can be locked in its current state.
|
||||
|
||||
39
Content.Shared/Magic/Components/MagicComponent.cs
Normal file
39
Content.Shared/Magic/Components/MagicComponent.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Magic.Components;
|
||||
|
||||
// TODO: Rename to MagicActionComponent or MagicRequirementsComponent
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedMagicSystem))]
|
||||
public sealed partial class MagicComponent : Component
|
||||
{
|
||||
// TODO: Split into different components?
|
||||
// This could be the MagicRequirementsComp - which just is requirements for the spell
|
||||
// Magic comp could be on the actual entities itself
|
||||
// Could handle lifetime, ignore caster, etc?
|
||||
// Magic caster comp would be on the caster, used for what I'm not sure
|
||||
|
||||
// TODO: Do After here or in actions
|
||||
|
||||
// TODO: Spell requirements
|
||||
// A list of requirements to cast the spell
|
||||
// Hands
|
||||
// Any item in hand
|
||||
// Spell takes up an inhand slot
|
||||
// May be an action toggle or something
|
||||
|
||||
// TODO: List requirements in action desc
|
||||
/// <summary>
|
||||
/// Does this spell require Wizard Robes & Hat?
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool RequiresClothes;
|
||||
|
||||
/// <summary>
|
||||
/// Does this spell require the user to speak?
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool RequiresSpeech;
|
||||
|
||||
// TODO: FreeHand - should check if toggleable action
|
||||
// Check which hand is free to toggle action in
|
||||
}
|
||||
36
Content.Shared/Magic/Components/SpellbookComponent.cs
Normal file
36
Content.Shared/Magic/Components/SpellbookComponent.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Magic.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Spellbooks can grant one or more spells to the user. If marked as <see cref="LearnPermanently"/> it will teach
|
||||
/// the performer the spells and wipe the book.
|
||||
/// Default behavior requires the book to be held in hand
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(SpellbookSystem))]
|
||||
public sealed partial class SpellbookComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// List of spells that this book has. This is a combination of the WorldSpells, EntitySpells, and InstantSpells.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public readonly List<EntityUid> Spells = new();
|
||||
|
||||
/// <summary>
|
||||
/// The three fields below is just used for initialization.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public Dictionary<EntProtoId, int> SpellActions = new();
|
||||
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float LearnTime = .75f;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the spell action stays even after the book is removed
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool LearnPermanently;
|
||||
}
|
||||
10
Content.Shared/Magic/Components/WizardClothesComponent.cs
Normal file
10
Content.Shared/Magic/Components/WizardClothesComponent.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Magic.Components;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SharedMagicSystem"/> checks this if a spell requires wizard clothes
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
[Access(typeof(SharedMagicSystem))]
|
||||
public sealed partial class WizardClothesComponent : Component;
|
||||
12
Content.Shared/Magic/Events/BeforeCastSpellEvent.cs
Normal file
12
Content.Shared/Magic/Events/BeforeCastSpellEvent.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Content.Shared.Magic.Events;
|
||||
|
||||
[ByRefEvent]
|
||||
public struct BeforeCastSpellEvent(EntityUid performer)
|
||||
{
|
||||
/// <summary>
|
||||
/// The Performer of the event, to check if they meet the requirements.
|
||||
/// </summary>
|
||||
public EntityUid Performer = performer;
|
||||
|
||||
public bool Cancelled;
|
||||
}
|
||||
18
Content.Shared/Magic/Events/ChargeSpellEvent.cs
Normal file
18
Content.Shared/Magic/Events/ChargeSpellEvent.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Content.Shared.Actions;
|
||||
|
||||
namespace Content.Shared.Magic.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Adds provided Charge to the held wand
|
||||
/// </summary>
|
||||
public sealed partial class ChargeSpellEvent : InstantActionEvent, ISpeakSpell
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public int Charge;
|
||||
|
||||
[DataField]
|
||||
public string WandTag = "WizardWand";
|
||||
|
||||
[DataField]
|
||||
public string? Speech { get; private set; }
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.Magic.Events;
|
||||
|
||||
@@ -9,17 +8,18 @@ public sealed partial class InstantSpawnSpellEvent : InstantActionEvent, ISpeakS
|
||||
/// <summary>
|
||||
/// What entity should be spawned.
|
||||
/// </summary>
|
||||
[DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string Prototype = default!;
|
||||
[DataField(required: true)]
|
||||
public EntProtoId Prototype;
|
||||
|
||||
[DataField("preventCollide")]
|
||||
[DataField]
|
||||
public bool PreventCollideWithCaster = true;
|
||||
|
||||
[DataField("speech")]
|
||||
[DataField]
|
||||
public string? Speech { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the targeted spawn positons; may lead to multiple entities being spawned.
|
||||
/// </summary>
|
||||
[DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos();
|
||||
[DataField]
|
||||
public MagicInstantSpawnData PosData = new TargetCasterPos();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Shared.Magic.Events;
|
||||
|
||||
@@ -7,20 +6,12 @@ public sealed partial class KnockSpellEvent : InstantActionEvent, ISpeakSpell
|
||||
{
|
||||
/// <summary>
|
||||
/// The range this spell opens doors in
|
||||
/// 4f is the default
|
||||
/// 10f is the default
|
||||
/// Should be able to open all doors/lockers in visible sight
|
||||
/// </summary>
|
||||
[DataField("range")]
|
||||
public float Range = 4f;
|
||||
[DataField]
|
||||
public float Range = 10f;
|
||||
|
||||
[DataField("knockSound")]
|
||||
public SoundSpecifier KnockSound = new SoundPathSpecifier("/Audio/Magic/knock.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Volume control for the spell.
|
||||
/// </summary>
|
||||
[DataField("knockVolume")]
|
||||
public float KnockVolume = 5f;
|
||||
|
||||
[DataField("speech")]
|
||||
[DataField]
|
||||
public string? Speech { get; private set; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.Magic.Events;
|
||||
|
||||
@@ -9,14 +8,9 @@ public sealed partial class ProjectileSpellEvent : WorldTargetActionEvent, ISpea
|
||||
/// <summary>
|
||||
/// What entity should be spawned.
|
||||
/// </summary>
|
||||
[DataField("prototype", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string Prototype = default!;
|
||||
[DataField(required: true)]
|
||||
public EntProtoId Prototype;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the targeted spawn positions; may lead to multiple entities being spawned.
|
||||
/// </summary>
|
||||
[DataField("posData")] public MagicSpawnData Pos = new TargetCasterPos();
|
||||
|
||||
[DataField("speech")]
|
||||
[DataField]
|
||||
public string? Speech { get; private set; }
|
||||
}
|
||||
|
||||
@@ -4,12 +4,13 @@ namespace Content.Shared.Magic.Events;
|
||||
|
||||
public sealed partial class SmiteSpellEvent : EntityTargetActionEvent, ISpeakSpell
|
||||
{
|
||||
// TODO: Make part of gib method
|
||||
/// <summary>
|
||||
/// Should this smite delete all parts/mechanisms gibbed except for the brain?
|
||||
/// Should this smite delete all parts/mechanisms gibbed except for the brain?
|
||||
/// </summary>
|
||||
[DataField("deleteNonBrainParts")]
|
||||
[DataField]
|
||||
public bool DeleteNonBrainParts = true;
|
||||
|
||||
[DataField("speech")]
|
||||
[DataField]
|
||||
public string? Speech { get; private set; }
|
||||
}
|
||||
|
||||
8
Content.Shared/Magic/Events/SpeakSpellEvent.cs
Normal file
8
Content.Shared/Magic/Events/SpeakSpellEvent.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Content.Shared.Magic.Events;
|
||||
|
||||
[ByRefEvent]
|
||||
public readonly struct SpeakSpellEvent(EntityUid performer, string speech)
|
||||
{
|
||||
public readonly EntityUid Performer = performer;
|
||||
public readonly string Speech = speech;
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Shared.Magic.Events;
|
||||
|
||||
// TODO: Can probably just be an entity or something
|
||||
public sealed partial class TeleportSpellEvent : WorldTargetActionEvent, ISpeakSpell
|
||||
{
|
||||
[DataField("blinkSound")]
|
||||
public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg");
|
||||
|
||||
[DataField("speech")]
|
||||
[DataField]
|
||||
public string? Speech { get; private set; }
|
||||
|
||||
// TODO: Move to magic component
|
||||
// TODO: Maybe not since sound specifier is a thing
|
||||
// Keep here to remind what the volume was set as
|
||||
/// <summary>
|
||||
/// Volume control for the spell.
|
||||
/// </summary>
|
||||
[DataField("blinkVolume")]
|
||||
[DataField]
|
||||
public float BlinkVolume = 5f;
|
||||
}
|
||||
|
||||
@@ -4,29 +4,31 @@ using Content.Shared.Storage;
|
||||
|
||||
namespace Content.Shared.Magic.Events;
|
||||
|
||||
// TODO: This class needs combining with InstantSpawnSpellEvent
|
||||
|
||||
public sealed partial class WorldSpawnSpellEvent : WorldTargetActionEvent, ISpeakSpell
|
||||
{
|
||||
// TODO:This class needs combining with InstantSpawnSpellEvent
|
||||
|
||||
/// <summary>
|
||||
/// The list of prototypes this spell will spawn
|
||||
/// </summary>
|
||||
[DataField("prototypes")]
|
||||
public List<EntitySpawnEntry> Contents = new();
|
||||
[DataField]
|
||||
public List<EntitySpawnEntry> Prototypes = new();
|
||||
|
||||
// TODO: This offset is liable for deprecation.
|
||||
// TODO: Target tile via code instead?
|
||||
/// <summary>
|
||||
/// The offset the prototypes will spawn in on relative to the one prior.
|
||||
/// Set to 0,0 to have them spawn on the same tile.
|
||||
/// </summary>
|
||||
[DataField("offset")]
|
||||
[DataField]
|
||||
public Vector2 Offset;
|
||||
|
||||
/// <summary>
|
||||
/// Lifetime to set for the entities to self delete
|
||||
/// </summary>
|
||||
[DataField("lifetime")] public float? Lifetime;
|
||||
[DataField]
|
||||
public float? Lifetime;
|
||||
|
||||
[DataField("speech")]
|
||||
[DataField]
|
||||
public string? Speech { get; private set; }
|
||||
}
|
||||
|
||||
25
Content.Shared/Magic/MagicInstantSpawnData.cs
Normal file
25
Content.Shared/Magic/MagicInstantSpawnData.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace Content.Shared.Magic;
|
||||
|
||||
// TODO: If still needed, move to magic component
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract partial class MagicInstantSpawnData;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns underneath caster.
|
||||
/// </summary>
|
||||
public sealed partial class TargetCasterPos : MagicInstantSpawnData;
|
||||
|
||||
/// <summary>
|
||||
/// Spawns 3 tiles wide in front of the caster.
|
||||
/// </summary>
|
||||
public sealed partial class TargetInFront : MagicInstantSpawnData
|
||||
{
|
||||
[DataField]
|
||||
public int Width = 3;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Spawns 1 tile in front of caster
|
||||
/// </summary>
|
||||
public sealed partial class TargetInFrontSingle : MagicInstantSpawnData;
|
||||
@@ -1,20 +0,0 @@
|
||||
namespace Content.Shared.Magic;
|
||||
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public abstract partial class MagicSpawnData
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Spawns 1 at the caster's feet.
|
||||
/// </summary>
|
||||
public sealed partial class TargetCasterPos : MagicSpawnData {}
|
||||
|
||||
/// <summary>
|
||||
/// Targets the 3 tiles in front of the caster.
|
||||
/// </summary>
|
||||
public sealed partial class TargetInFront : MagicSpawnData
|
||||
{
|
||||
[DataField("width")] public int Width = 3;
|
||||
}
|
||||
519
Content.Shared/Magic/SharedMagicSystem.cs
Normal file
519
Content.Shared/Magic/SharedMagicSystem.cs
Normal file
@@ -0,0 +1,519 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Body.Components;
|
||||
using Content.Shared.Body.Systems;
|
||||
using Content.Shared.Coordinates.Helpers;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Lock;
|
||||
using Content.Shared.Magic.Components;
|
||||
using Content.Shared.Magic.Events;
|
||||
using Content.Shared.Maps;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Speech.Muting;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Content.Shared.Weapons.Ranged.Systems;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
using Robust.Shared.Spawners;
|
||||
|
||||
namespace Content.Shared.Magic;
|
||||
|
||||
/// <summary>
|
||||
/// Handles learning and using spells (actions)
|
||||
/// </summary>
|
||||
public abstract class SharedMagicSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ISerializationManager _seriMan = default!;
|
||||
[Dependency] private readonly IComponentFactory _compFact = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedGunSystem _gunSystem = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly INetManager _net = default!;
|
||||
[Dependency] private readonly SharedBodySystem _body = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
[Dependency] private readonly SharedDoorSystem _door = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly LockSystem _lock = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<MagicComponent, BeforeCastSpellEvent>(OnBeforeCastSpell);
|
||||
|
||||
SubscribeLocalEvent<InstantSpawnSpellEvent>(OnInstantSpawn);
|
||||
SubscribeLocalEvent<TeleportSpellEvent>(OnTeleportSpell);
|
||||
SubscribeLocalEvent<WorldSpawnSpellEvent>(OnWorldSpawn);
|
||||
SubscribeLocalEvent<ProjectileSpellEvent>(OnProjectileSpell);
|
||||
SubscribeLocalEvent<ChangeComponentsSpellEvent>(OnChangeComponentsSpell);
|
||||
SubscribeLocalEvent<SmiteSpellEvent>(OnSmiteSpell);
|
||||
SubscribeLocalEvent<KnockSpellEvent>(OnKnockSpell);
|
||||
SubscribeLocalEvent<ChargeSpellEvent>(OnChargeSpell);
|
||||
|
||||
// Spell wishlist
|
||||
// A wishlish of spells that I'd like to implement or planning on implementing in a future PR
|
||||
|
||||
// TODO: InstantDoAfterSpell and WorldDoafterSpell
|
||||
// Both would be an action that take in an event, that passes an event to trigger once the doafter is done
|
||||
// This would be three events:
|
||||
// 1 - Event that triggers from the action that starts the doafter
|
||||
// 2 - The doafter event itself, which passes the event with it
|
||||
// 3 - The event to trigger once the do-after finishes
|
||||
|
||||
// TODO: Inanimate objects to life ECS
|
||||
// AI sentience
|
||||
|
||||
// TODO: Flesh2Stone
|
||||
// Entity Target spell
|
||||
// Synergy with Inanimate object to life (detects player and allows player to move around)
|
||||
|
||||
// TODO: Lightning Spell
|
||||
// Should just fire lightning, try to prevent arc back to caster
|
||||
|
||||
// TODO: Magic Missile (homing projectile ecs)
|
||||
// Instant action, target any player (except self) on screen
|
||||
|
||||
// TODO: Random projectile ECS for magic-carp, wand of magic
|
||||
|
||||
// TODO: Recall Spell
|
||||
// mark any item in hand to recall
|
||||
// ItemRecallComponent
|
||||
// Event adds the component if it doesn't exist and the performer isn't stored in the comp
|
||||
// 2nd firing of the event checks to see if the recall comp has this uid, and if it does it calls it
|
||||
// if no free hands, summon at feet
|
||||
// if item deleted, clear stored item
|
||||
|
||||
// TODO: Jaunt (should be its own ECS)
|
||||
// Instant action
|
||||
// When clicked, disappear/reappear (goes to paused map)
|
||||
// option to restrict to tiles
|
||||
// option for requiring entry/exit (blood jaunt)
|
||||
// speed option
|
||||
|
||||
// TODO: Summon Events
|
||||
// List of wizard events to add into the event pool that frequently activate
|
||||
// floor is lava
|
||||
// change places
|
||||
// ECS that when triggered, will periodically trigger a random GameRule
|
||||
// Would need a controller/controller entity?
|
||||
|
||||
// TODO: Summon Guns
|
||||
// Summon a random gun at peoples feet
|
||||
// Get every alive player (not in cryo, not a simplemob)
|
||||
// TODO: After Antag Rework - Rare chance of giving gun collector status to people
|
||||
|
||||
// TODO: Summon Magic
|
||||
// Summon a random magic wand at peoples feet
|
||||
// Get every alive player (not in cryo, not a simplemob)
|
||||
// TODO: After Antag Rework - Rare chance of giving magic collector status to people
|
||||
|
||||
// TODO: Bottle of Blood
|
||||
// Summons Slaughter Demon
|
||||
// TODO: Slaughter Demon
|
||||
// Also see Jaunt
|
||||
|
||||
// TODO: Field Spells
|
||||
// Should be able to specify a grid of tiles (3x3 for example) that it effects
|
||||
// Timed despawn - so it doesn't last forever
|
||||
// Ignore caster - for spells that shouldn't effect the caster (ie if timestop should effect the caster)
|
||||
|
||||
// TODO: Touch toggle spell
|
||||
// 1 - When toggled on, show in hand
|
||||
// 2 - Block hand when toggled on
|
||||
// - Require free hand
|
||||
// 3 - use spell event when toggled & click
|
||||
}
|
||||
|
||||
private void OnBeforeCastSpell(Entity<MagicComponent> ent, ref BeforeCastSpellEvent args)
|
||||
{
|
||||
var comp = ent.Comp;
|
||||
var hasReqs = true;
|
||||
|
||||
if (comp.RequiresClothes)
|
||||
{
|
||||
var enumerator = _inventory.GetSlotEnumerator(args.Performer, SlotFlags.OUTERCLOTHING | SlotFlags.HEAD);
|
||||
while (enumerator.MoveNext(out var containerSlot))
|
||||
{
|
||||
if (containerSlot.ContainedEntity is { } item)
|
||||
hasReqs = HasComp<WizardClothesComponent>(item);
|
||||
else
|
||||
hasReqs = false;
|
||||
|
||||
if (!hasReqs)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (comp.RequiresSpeech && HasComp<MutedComponent>(args.Performer))
|
||||
hasReqs = false;
|
||||
|
||||
if (hasReqs)
|
||||
return;
|
||||
|
||||
args.Cancelled = true;
|
||||
_popup.PopupClient(Loc.GetString("spell-requirements-failed"), args.Performer, args.Performer);
|
||||
|
||||
// TODO: Pre-cast do after, either here or in SharedActionsSystem
|
||||
}
|
||||
|
||||
private bool PassesSpellPrerequisites(EntityUid spell, EntityUid performer)
|
||||
{
|
||||
var ev = new BeforeCastSpellEvent(performer);
|
||||
RaiseLocalEvent(spell, ref ev);
|
||||
return !ev.Cancelled;
|
||||
}
|
||||
|
||||
#region Spells
|
||||
#region Instant Spawn Spells
|
||||
/// <summary>
|
||||
/// Handles the instant action (i.e. on the caster) attempting to spawn an entity.
|
||||
/// </summary>
|
||||
private void OnInstantSpawn(InstantSpawnSpellEvent args)
|
||||
{
|
||||
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
|
||||
return;
|
||||
|
||||
var transform = Transform(args.Performer);
|
||||
|
||||
foreach (var position in GetInstantSpawnPositions(transform, args.PosData))
|
||||
{
|
||||
SpawnSpellHelper(args.Prototype, position, args.Performer, preventCollide: args.PreventCollideWithCaster);
|
||||
}
|
||||
|
||||
Speak(args);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets spawn positions listed on <see cref="InstantSpawnSpellEvent"/>
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
private List<EntityCoordinates> GetInstantSpawnPositions(TransformComponent casterXform, MagicInstantSpawnData data)
|
||||
{
|
||||
switch (data)
|
||||
{
|
||||
case TargetCasterPos:
|
||||
return new List<EntityCoordinates>(1) {casterXform.Coordinates};
|
||||
case TargetInFrontSingle:
|
||||
{
|
||||
var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
|
||||
|
||||
if (!TryComp<MapGridComponent>(casterXform.GridUid, out var mapGrid))
|
||||
return new List<EntityCoordinates>();
|
||||
if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
|
||||
return new List<EntityCoordinates>();
|
||||
|
||||
var tileIndex = tileReference.Value.GridIndices;
|
||||
return new List<EntityCoordinates>(1) { _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex) };
|
||||
}
|
||||
case TargetInFront:
|
||||
{
|
||||
var directionPos = casterXform.Coordinates.Offset(casterXform.LocalRotation.ToWorldVec().Normalized());
|
||||
|
||||
if (!TryComp<MapGridComponent>(casterXform.GridUid, out var mapGrid))
|
||||
return new List<EntityCoordinates>();
|
||||
|
||||
if (!directionPos.TryGetTileRef(out var tileReference, EntityManager, _mapManager))
|
||||
return new List<EntityCoordinates>();
|
||||
|
||||
var tileIndex = tileReference.Value.GridIndices;
|
||||
var coords = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex);
|
||||
EntityCoordinates coordsPlus;
|
||||
EntityCoordinates coordsMinus;
|
||||
|
||||
var dir = casterXform.LocalRotation.GetCardinalDir();
|
||||
switch (dir)
|
||||
{
|
||||
case Direction.North:
|
||||
case Direction.South:
|
||||
{
|
||||
coordsPlus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (1, 0));
|
||||
coordsMinus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (-1, 0));
|
||||
return new List<EntityCoordinates>(3)
|
||||
{
|
||||
coords,
|
||||
coordsPlus,
|
||||
coordsMinus,
|
||||
};
|
||||
}
|
||||
case Direction.East:
|
||||
case Direction.West:
|
||||
{
|
||||
coordsPlus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (0, 1));
|
||||
coordsMinus = _mapSystem.GridTileToLocal(casterXform.GridUid.Value, mapGrid, tileIndex + (0, -1));
|
||||
return new List<EntityCoordinates>(3)
|
||||
{
|
||||
coords,
|
||||
coordsPlus,
|
||||
coordsMinus,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return new List<EntityCoordinates>();
|
||||
}
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
// End Instant Spawn Spells
|
||||
#endregion
|
||||
#region World Spawn Spells
|
||||
/// <summary>
|
||||
/// Spawns entities from a list within range of click.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It will offset entities after the first entity based on the OffsetVector2.
|
||||
/// </remarks>
|
||||
/// <param name="args"> The Spawn Spell Event args.</param>
|
||||
private void OnWorldSpawn(WorldSpawnSpellEvent args)
|
||||
{
|
||||
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
|
||||
return;
|
||||
|
||||
var targetMapCoords = args.Target;
|
||||
|
||||
WorldSpawnSpellHelper(args.Prototypes, targetMapCoords, args.Performer, args.Lifetime, args.Offset);
|
||||
Speak(args);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loops through a supplied list of entity prototypes and spawns them
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If an offset of 0, 0 is supplied then the entities will all spawn on the same tile.
|
||||
/// Any other offset will spawn entities starting from the source Map Coordinates and will increment the supplied
|
||||
/// offset
|
||||
/// </remarks>
|
||||
/// <param name="entityEntries"> The list of Entities to spawn in</param>
|
||||
/// <param name="entityCoords"> Map Coordinates where the entities will spawn</param>
|
||||
/// <param name="lifetime"> Check to see if the entities should self delete</param>
|
||||
/// <param name="offsetVector2"> A Vector2 offset that the entities will spawn in</param>
|
||||
private void WorldSpawnSpellHelper(List<EntitySpawnEntry> entityEntries, EntityCoordinates entityCoords, EntityUid performer, float? lifetime, Vector2 offsetVector2)
|
||||
{
|
||||
var getProtos = EntitySpawnCollection.GetSpawns(entityEntries, _random);
|
||||
|
||||
var offsetCoords = entityCoords;
|
||||
foreach (var proto in getProtos)
|
||||
{
|
||||
SpawnSpellHelper(proto, offsetCoords, performer, lifetime);
|
||||
offsetCoords = offsetCoords.Offset(offsetVector2);
|
||||
}
|
||||
}
|
||||
// End World Spawn Spells
|
||||
#endregion
|
||||
#region Projectile Spells
|
||||
private void OnProjectileSpell(ProjectileSpellEvent ev)
|
||||
{
|
||||
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !_net.IsServer)
|
||||
return;
|
||||
|
||||
ev.Handled = true;
|
||||
Speak(ev);
|
||||
|
||||
var xform = Transform(ev.Performer);
|
||||
var fromCoords = xform.Coordinates;
|
||||
var toCoords = ev.Target;
|
||||
var userVelocity = _physics.GetMapLinearVelocity(ev.Performer);
|
||||
|
||||
// If applicable, this ensures the projectile is parented to grid on spawn, instead of the map.
|
||||
var fromMap = fromCoords.ToMap(EntityManager, _transform);
|
||||
var spawnCoords = _mapManager.TryFindGridAt(fromMap, out var gridUid, out _)
|
||||
? fromCoords.WithEntityId(gridUid, EntityManager)
|
||||
: new(_mapManager.GetMapEntityId(fromMap.MapId), fromMap.Position);
|
||||
|
||||
var ent = Spawn(ev.Prototype, spawnCoords);
|
||||
var direction = toCoords.ToMapPos(EntityManager, _transform) -
|
||||
spawnCoords.ToMapPos(EntityManager, _transform);
|
||||
_gunSystem.ShootProjectile(ent, direction, userVelocity, ev.Performer, ev.Performer);
|
||||
}
|
||||
// End Projectile Spells
|
||||
#endregion
|
||||
#region Change Component Spells
|
||||
// staves.yml ActionRGB light
|
||||
private void OnChangeComponentsSpell(ChangeComponentsSpellEvent ev)
|
||||
{
|
||||
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
|
||||
return;
|
||||
|
||||
ev.Handled = true;
|
||||
Speak(ev);
|
||||
|
||||
foreach (var toRemove in ev.ToRemove)
|
||||
{
|
||||
if (_compFact.TryGetRegistration(toRemove, out var registration))
|
||||
RemComp(ev.Target, registration.Type);
|
||||
}
|
||||
|
||||
foreach (var (name, data) in ev.ToAdd)
|
||||
{
|
||||
if (HasComp(ev.Target, data.Component.GetType()))
|
||||
continue;
|
||||
|
||||
var component = (Component) _compFact.GetComponent(name);
|
||||
component.Owner = ev.Target;
|
||||
var temp = (object) component;
|
||||
_seriMan.CopyTo(data.Component, ref temp);
|
||||
EntityManager.AddComponent(ev.Target, (Component) temp!);
|
||||
}
|
||||
}
|
||||
// End Change Component Spells
|
||||
#endregion
|
||||
#region Teleport Spells
|
||||
// TODO: Rename to teleport clicked spell?
|
||||
/// <summary>
|
||||
/// Teleports the user to the clicked location
|
||||
/// </summary>
|
||||
/// <param name="args"></param>
|
||||
private void OnTeleportSpell(TeleportSpellEvent args)
|
||||
{
|
||||
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
|
||||
return;
|
||||
|
||||
var transform = Transform(args.Performer);
|
||||
|
||||
if (transform.MapID != args.Target.GetMapId(EntityManager) || !_interaction.InRangeUnobstructed(args.Performer, args.Target, range: 1000F, collisionMask: CollisionGroup.Opaque, popup: true))
|
||||
return;
|
||||
|
||||
_transform.SetCoordinates(args.Performer, args.Target);
|
||||
_transform.AttachToGridOrMap(args.Performer, transform);
|
||||
Speak(args);
|
||||
args.Handled = true;
|
||||
}
|
||||
// End Teleport Spells
|
||||
#endregion
|
||||
#region Spell Helpers
|
||||
private void SpawnSpellHelper(string? proto, EntityCoordinates position, EntityUid performer, float? lifetime = null, bool preventCollide = false)
|
||||
{
|
||||
if (!_net.IsServer)
|
||||
return;
|
||||
|
||||
var ent = Spawn(proto, position.SnapToGrid(EntityManager, _mapManager));
|
||||
|
||||
if (lifetime != null)
|
||||
{
|
||||
var comp = EnsureComp<TimedDespawnComponent>(ent);
|
||||
comp.Lifetime = lifetime.Value;
|
||||
}
|
||||
|
||||
if (preventCollide)
|
||||
{
|
||||
var comp = EnsureComp<PreventCollideComponent>(ent);
|
||||
comp.Uid = performer;
|
||||
}
|
||||
}
|
||||
// End Spell Helpers
|
||||
#endregion
|
||||
#region Smite Spells
|
||||
private void OnSmiteSpell(SmiteSpellEvent ev)
|
||||
{
|
||||
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
|
||||
return;
|
||||
|
||||
ev.Handled = true;
|
||||
Speak(ev);
|
||||
|
||||
var direction = _transform.GetMapCoordinates(ev.Target, Transform(ev.Target)).Position - _transform.GetMapCoordinates(ev.Performer, Transform(ev.Performer)).Position;
|
||||
var impulseVector = direction * 10000;
|
||||
|
||||
_physics.ApplyLinearImpulse(ev.Target, impulseVector);
|
||||
|
||||
if (!TryComp<BodyComponent>(ev.Target, out var body))
|
||||
return;
|
||||
|
||||
_body.GibBody(ev.Target, true, body);
|
||||
}
|
||||
// End Smite Spells
|
||||
#endregion
|
||||
#region Knock Spells
|
||||
/// <summary>
|
||||
/// Opens all doors and locks within range
|
||||
/// </summary>
|
||||
/// <param name="args"></param>
|
||||
private void OnKnockSpell(KnockSpellEvent args)
|
||||
{
|
||||
if (args.Handled || !PassesSpellPrerequisites(args.Action, args.Performer))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
Speak(args);
|
||||
|
||||
var transform = Transform(args.Performer);
|
||||
|
||||
// Look for doors and lockers, and don't open/unlock them if they're already opened/unlocked.
|
||||
foreach (var target in _lookup.GetEntitiesInRange(_transform.GetMapCoordinates(args.Performer, transform), args.Range, flags: LookupFlags.Dynamic | LookupFlags.Static))
|
||||
{
|
||||
if (!_interaction.InRangeUnobstructed(args.Performer, target, range: 0, collisionMask: CollisionGroup.Opaque))
|
||||
continue;
|
||||
|
||||
if (TryComp<DoorBoltComponent>(target, out var doorBoltComp) && doorBoltComp.BoltsDown)
|
||||
_door.SetBoltsDown((target, doorBoltComp), false, predicted: true);
|
||||
|
||||
if (TryComp<DoorComponent>(target, out var doorComp) && doorComp.State is not DoorState.Open)
|
||||
_door.StartOpening(target);
|
||||
|
||||
if (TryComp<LockComponent>(target, out var lockComp) && lockComp.Locked)
|
||||
_lock.Unlock(target, args.Performer, lockComp);
|
||||
}
|
||||
}
|
||||
// End Knock Spells
|
||||
#endregion
|
||||
#region Charge Spells
|
||||
// TODO: Future support to charge other items
|
||||
private void OnChargeSpell(ChargeSpellEvent ev)
|
||||
{
|
||||
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !TryComp<HandsComponent>(ev.Performer, out var handsComp))
|
||||
return;
|
||||
|
||||
EntityUid? wand = null;
|
||||
foreach (var item in _hands.EnumerateHeld(ev.Performer, handsComp))
|
||||
{
|
||||
if (!_tag.HasTag(item, ev.WandTag))
|
||||
continue;
|
||||
|
||||
wand = item;
|
||||
}
|
||||
|
||||
ev.Handled = true;
|
||||
Speak(ev);
|
||||
|
||||
if (wand == null || !TryComp<BasicEntityAmmoProviderComponent>(wand, out var basicAmmoComp) || basicAmmoComp.Count == null)
|
||||
return;
|
||||
|
||||
_gunSystem.UpdateBasicEntityAmmoCount(wand.Value, basicAmmoComp.Count.Value + ev.Charge, basicAmmoComp);
|
||||
}
|
||||
// End Charge Spells
|
||||
#endregion
|
||||
// End Spells
|
||||
#endregion
|
||||
|
||||
// When any spell is cast it will raise this as an event, so then it can be played in server or something. At least until chat gets moved to shared
|
||||
// TODO: Temp until chat is in shared
|
||||
private void Speak(BaseActionEvent args)
|
||||
{
|
||||
if (args is not ISpeakSpell speak || string.IsNullOrWhiteSpace(speak.Speech))
|
||||
return;
|
||||
|
||||
var ev = new SpeakSpellEvent(args.Performer, speak.Speech);
|
||||
RaiseLocalEvent(ref ev);
|
||||
}
|
||||
}
|
||||
96
Content.Shared/Magic/SpellbookSystem.cs
Normal file
96
Content.Shared/Magic/SpellbookSystem.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.DoAfter;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Magic.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Shared.Magic;
|
||||
|
||||
public sealed class SpellbookSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
|
||||
[Dependency] private readonly INetManager _netManager = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<SpellbookComponent, MapInitEvent>(OnInit, before: [typeof(SharedMagicSystem)]);
|
||||
SubscribeLocalEvent<SpellbookComponent, UseInHandEvent>(OnUse);
|
||||
SubscribeLocalEvent<SpellbookComponent, SpellbookDoAfterEvent>(OnDoAfter);
|
||||
}
|
||||
|
||||
private void OnInit(Entity<SpellbookComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
foreach (var (id, charges) in ent.Comp.SpellActions)
|
||||
{
|
||||
var spell = _actionContainer.AddAction(ent, id);
|
||||
if (spell == null)
|
||||
continue;
|
||||
|
||||
int? charge = charges;
|
||||
if (_actions.GetCharges(spell) != null)
|
||||
charge = _actions.GetCharges(spell);
|
||||
|
||||
_actions.SetCharges(spell, charge < 0 ? null : charge);
|
||||
ent.Comp.Spells.Add(spell.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnUse(Entity<SpellbookComponent> ent, ref UseInHandEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
AttemptLearn(ent, args);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnDoAfter<T>(Entity<SpellbookComponent> ent, ref T args) where T : DoAfterEvent // Sometimes i despise this language
|
||||
{
|
||||
if (args.Handled || args.Cancelled)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
if (!ent.Comp.LearnPermanently)
|
||||
{
|
||||
_actions.GrantActions(args.Args.User, ent.Comp.Spells, ent);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_mind.TryGetMind(args.Args.User, out var mindId, out _))
|
||||
{
|
||||
var mindActionContainerComp = EnsureComp<ActionsContainerComponent>(mindId);
|
||||
|
||||
if (_netManager.IsServer)
|
||||
_actionContainer.TransferAllActionsWithNewAttached(ent, mindId, args.Args.User, newContainer: mindActionContainerComp);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var (id, charges) in ent.Comp.SpellActions)
|
||||
{
|
||||
EntityUid? actionId = null;
|
||||
if (_actions.AddAction(args.Args.User, ref actionId, id))
|
||||
_actions.SetCharges(actionId, charges < 0 ? null : charges);
|
||||
}
|
||||
}
|
||||
|
||||
ent.Comp.SpellActions.Clear();
|
||||
}
|
||||
|
||||
private void AttemptLearn(Entity<SpellbookComponent> ent, UseInHandEvent args)
|
||||
{
|
||||
var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, ent.Comp.LearnTime, new SpellbookDoAfterEvent(), ent, target: ent)
|
||||
{
|
||||
BreakOnMove = true,
|
||||
BreakOnDamage = true,
|
||||
NeedHand = true //What, are you going to read with your eyes only??
|
||||
};
|
||||
|
||||
_doAfter.TryStartDoAfter(doAfterEventArgs);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ public abstract class SharedMindSystem : EntitySystem
|
||||
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
|
||||
[Dependency] private readonly SharedPlayerSystem _player = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metadata = default!;
|
||||
[Dependency] private readonly ISharedPlayerManager _playerMan = default!;
|
||||
|
||||
[ViewVariables]
|
||||
protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
|
||||
@@ -383,6 +382,30 @@ public abstract class SharedMindSystem : EntitySystem
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to find an objective that has the same prototype as the argument.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Will not work for objectives that have no prototype, or duplicate objectives with the same prototype.
|
||||
/// <//remarks>
|
||||
public bool TryFindObjective(Entity<MindComponent?> mind, string prototype, [NotNullWhen(true)] out EntityUid? objective)
|
||||
{
|
||||
objective = null;
|
||||
if (!Resolve(mind, ref mind.Comp))
|
||||
return false;
|
||||
|
||||
foreach (var uid in mind.Comp.Objectives)
|
||||
{
|
||||
if (MetaData(uid).EntityPrototype?.ID == prototype)
|
||||
{
|
||||
objective = uid;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetSession(EntityUid? mindId, [NotNullWhen(true)] out ICommonSession? session)
|
||||
{
|
||||
session = null;
|
||||
|
||||
@@ -10,12 +10,15 @@ using Content.Shared.Item;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Pointing;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Pulling.Events;
|
||||
using Content.Shared.Speech;
|
||||
using Content.Shared.Standing;
|
||||
using Content.Shared.Strip.Components;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
|
||||
namespace Content.Shared.Mobs.Systems;
|
||||
|
||||
@@ -43,6 +46,7 @@ public partial class MobStateSystem
|
||||
SubscribeLocalEvent<MobStateComponent, TryingToSleepEvent>(OnSleepAttempt);
|
||||
SubscribeLocalEvent<MobStateComponent, CombatModeShouldHandInteractEvent>(OnCombatModeShouldHandInteract);
|
||||
SubscribeLocalEvent<MobStateComponent, AttemptPacifiedAttackEvent>(OnAttemptPacifiedAttack);
|
||||
SubscribeLocalEvent<MobStateComponent, PreventCollideEvent>(OnPreventCollide);
|
||||
}
|
||||
|
||||
private void OnStateExitSubscribers(EntityUid target, MobStateComponent component, MobState state)
|
||||
@@ -175,5 +179,21 @@ public partial class MobStateSystem
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
private void OnPreventCollide(Entity<MobStateComponent> ent, ref PreventCollideEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
if (IsAlive(ent, ent))
|
||||
return;
|
||||
|
||||
var other = args.OtherEntity;
|
||||
if (HasComp<ProjectileComponent>(other) &&
|
||||
CompOrNull<TargetedProjectileComponent>(other)?.Target != ent.Owner)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -14,6 +14,15 @@ namespace Content.Shared.Movement.Components
|
||||
|
||||
[DataField] public float PushStrength = 600f;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public float StepSoundMoveDistanceRunning = 2;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public float StepSoundMoveDistanceWalking = 1.5f;
|
||||
|
||||
[DataField, AutoNetworkedField]
|
||||
public float FootstepVariation;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public EntityCoordinates LastPosition { get; set; }
|
||||
|
||||
|
||||
@@ -58,11 +58,6 @@ namespace Content.Shared.Movement.Systems
|
||||
protected EntityQuery<CanMoveInAirComponent> CanMoveInAirQuery;
|
||||
protected EntityQuery<NoRotateOnMoveComponent> NoRotateQuery;
|
||||
|
||||
private const float StepSoundMoveDistanceRunning = 2;
|
||||
private const float StepSoundMoveDistanceWalking = 1.5f;
|
||||
|
||||
private const float FootstepVariation = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="CCVars.StopSpeed"/>
|
||||
/// </summary>
|
||||
@@ -258,7 +253,7 @@ namespace Content.Shared.Movement.Systems
|
||||
|
||||
var audioParams = sound.Params
|
||||
.WithVolume(sound.Params.Volume + soundModifier)
|
||||
.WithVariation(sound.Params.Variation ?? FootstepVariation);
|
||||
.WithVariation(sound.Params.Variation ?? mobMover.FootstepVariation);
|
||||
|
||||
// If we're a relay target then predict the sound for all relays.
|
||||
if (relayTarget != null)
|
||||
@@ -404,7 +399,9 @@ namespace Content.Shared.Movement.Systems
|
||||
return false;
|
||||
|
||||
var coordinates = xform.Coordinates;
|
||||
var distanceNeeded = mover.Sprinting ? StepSoundMoveDistanceRunning : StepSoundMoveDistanceWalking;
|
||||
var distanceNeeded = mover.Sprinting
|
||||
? mobMover.StepSoundMoveDistanceRunning
|
||||
: mobMover.StepSoundMoveDistanceWalking;
|
||||
|
||||
// Handle footsteps.
|
||||
if (!weightless)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Shared.Ninja.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Shared.Ninja.Components;
|
||||
|
||||
@@ -35,4 +35,22 @@ public sealed partial class SpaceNinjaComponent : Component
|
||||
/// </summary>
|
||||
[DataField("katana"), AutoNetworkedField]
|
||||
public EntityUid? Katana;
|
||||
|
||||
/// <summary>
|
||||
/// Objective to complete after calling in a threat.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId TerrorObjective = "TerrorObjective";
|
||||
|
||||
/// <summary>
|
||||
/// Objective to complete after setting everyone to arrest.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId MassArrestObjective = "MassArrestObjective";
|
||||
|
||||
/// <summary>
|
||||
/// Objective to complete after the spider charge detonates.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId SpiderChargeObjective = "SpiderChargeObjective";
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ using Content.Shared.Charges.Systems;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Ninja.Components;
|
||||
using Content.Shared.Physics;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Examine;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed class DashAbilitySystem : EntitySystem
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedChargesSystem _charges = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examine = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
|
||||
@@ -79,11 +79,10 @@ public sealed class DashAbilitySystem : EntitySystem
|
||||
_popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user);
|
||||
return;
|
||||
}
|
||||
|
||||
var origin = Transform(user).MapPosition;
|
||||
var origin = _transform.GetMapCoordinates(user);
|
||||
var target = args.Target.ToMap(EntityManager, _transform);
|
||||
// prevent collision with the user duh
|
||||
if (!_interaction.InRangeUnobstructed(origin, target, 0f, CollisionGroup.Opaque, uid => uid == user))
|
||||
if (!_examine.InRangeUnOccluded(origin, target, SharedInteractionSystem.MaxRaycastRange, null))
|
||||
{
|
||||
// can only dash if the destination is visible on screen
|
||||
_popup.PopupClient(Loc.GetString("dash-ability-cant-see", ("item", uid)), user, user);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.CombatMode;
|
||||
using Content.Shared.Communications;
|
||||
using Content.Shared.CriminalRecords.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction;
|
||||
@@ -62,6 +63,7 @@ public abstract class SharedNinjaGlovesSystem : EntitySystem
|
||||
RemComp<StunProviderComponent>(user);
|
||||
RemComp<ResearchStealerComponent>(user);
|
||||
RemComp<CommsHackerComponent>(user);
|
||||
RemComp<CriminalRecordsHackerComponent>(user);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,7 +23,6 @@ public abstract class SharedNavMapSystem : EntitySystem
|
||||
public const int FloorMask = AllDirMask << (int) NavMapChunkType.Floor;
|
||||
|
||||
[Robust.Shared.IoC.Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Robust.Shared.IoC.Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
private readonly string[] _wallTags = ["Wall", "Window"];
|
||||
private EntityQuery<NavMapDoorComponent> _doorQuery;
|
||||
|
||||
@@ -17,8 +17,8 @@ namespace Content.Shared.Plunger.Systems;
|
||||
/// </summary>
|
||||
public sealed class PlungerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] protected readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
|
||||
@@ -5,7 +5,6 @@ using Content.Shared.GameTicking;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Preferences.Loadouts.Effects;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Traits;
|
||||
using Robust.Shared.Collections;
|
||||
@@ -29,16 +28,101 @@ namespace Content.Shared.Preferences
|
||||
public const int MaxNameLength = 32;
|
||||
public const int MaxDescLength = 512;
|
||||
|
||||
private readonly Dictionary<string, JobPriority> _jobPriorities;
|
||||
private readonly List<string> _antagPreferences;
|
||||
private readonly List<string> _traitPreferences;
|
||||
/// <summary>
|
||||
/// Job preferences for initial spawn.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
private Dictionary<string, JobPriority> _jobPriorities = new()
|
||||
{
|
||||
{
|
||||
SharedGameTicker.FallbackOverflowJob, JobPriority.High
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Antags we have opted in to.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
private HashSet<string> _antagPreferences = new();
|
||||
|
||||
/// <summary>
|
||||
/// Enabled traits.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
private HashSet<string> _traitPreferences = new();
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="_loadouts"/>
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts;
|
||||
|
||||
private Dictionary<string, RoleLoadout> _loadouts;
|
||||
[DataField]
|
||||
private Dictionary<string, RoleLoadout> _loadouts = new();
|
||||
|
||||
// What in the lord is happening here.
|
||||
private HumanoidCharacterProfile(
|
||||
[DataField]
|
||||
public string Name { get; set; } = "John Doe";
|
||||
|
||||
/// <summary>
|
||||
/// Detailed text that can appear for the character if <see cref="CCVars.FlavorText"/> is enabled.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string FlavorText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Associated <see cref="SpeciesPrototype"/> for this profile.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
|
||||
|
||||
[DataField]
|
||||
public int Age { get; set; } = 18;
|
||||
|
||||
[DataField]
|
||||
public Sex Sex { get; private set; } = Sex.Male;
|
||||
|
||||
[DataField]
|
||||
public Gender Gender { get; private set; } = Gender.Male;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Appearance"/>
|
||||
/// </summary>
|
||||
public ICharacterAppearance CharacterAppearance => Appearance;
|
||||
|
||||
/// <summary>
|
||||
/// Stores markings, eye colors, etc for the profile.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HumanoidCharacterAppearance Appearance { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// When spawning into a round what's the preferred spot to spawn.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SpawnPriorityPreference SpawnPriority { get; private set; } = SpawnPriorityPreference.None;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="_jobPriorities"/>
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="_antagPreferences"/>
|
||||
/// </summary>
|
||||
public IReadOnlySet<string> AntagPreferences => _antagPreferences;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="_traitPreferences"/>
|
||||
/// </summary>
|
||||
public IReadOnlySet<string> TraitPreferences => _traitPreferences;
|
||||
|
||||
/// <summary>
|
||||
/// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; } =
|
||||
PreferenceUnavailableMode.SpawnAsOverflow;
|
||||
|
||||
public HumanoidCharacterProfile(
|
||||
string name,
|
||||
string flavortext,
|
||||
string species,
|
||||
@@ -49,8 +133,8 @@ namespace Content.Shared.Preferences
|
||||
SpawnPriorityPreference spawnPriority,
|
||||
Dictionary<string, JobPriority> jobPriorities,
|
||||
PreferenceUnavailableMode preferenceUnavailable,
|
||||
List<string> antagPreferences,
|
||||
List<string> traitPreferences,
|
||||
HashSet<string> antagPreferences,
|
||||
HashSet<string> traitPreferences,
|
||||
Dictionary<string, RoleLoadout> loadouts)
|
||||
{
|
||||
Name = name;
|
||||
@@ -68,40 +152,21 @@ namespace Content.Shared.Preferences
|
||||
_loadouts = loadouts;
|
||||
}
|
||||
|
||||
/// <summary>Copy constructor but with overridable references (to prevent useless copies)</summary>
|
||||
private HumanoidCharacterProfile(
|
||||
HumanoidCharacterProfile other,
|
||||
Dictionary<string, JobPriority> jobPriorities,
|
||||
List<string> antagPreferences,
|
||||
List<string> traitPreferences,
|
||||
Dictionary<string, RoleLoadout> loadouts)
|
||||
: this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.SpawnPriority,
|
||||
jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences, loadouts)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Copy constructor</summary>
|
||||
private HumanoidCharacterProfile(HumanoidCharacterProfile other)
|
||||
: this(other, new Dictionary<string, JobPriority>(other.JobPriorities), new List<string>(other.AntagPreferences), new List<string>(other.TraitPreferences), new Dictionary<string, RoleLoadout>(other.Loadouts))
|
||||
{
|
||||
}
|
||||
|
||||
public HumanoidCharacterProfile(
|
||||
string name,
|
||||
string flavortext,
|
||||
string species,
|
||||
int age,
|
||||
Sex sex,
|
||||
Gender gender,
|
||||
HumanoidCharacterAppearance appearance,
|
||||
SpawnPriorityPreference spawnPriority,
|
||||
IReadOnlyDictionary<string, JobPriority> jobPriorities,
|
||||
PreferenceUnavailableMode preferenceUnavailable,
|
||||
IReadOnlyList<string> antagPreferences,
|
||||
IReadOnlyList<string> traitPreferences,
|
||||
Dictionary<string, RoleLoadout> loadouts)
|
||||
: this(name, flavortext, species, age, sex, gender, appearance, spawnPriority, new Dictionary<string, JobPriority>(jobPriorities),
|
||||
preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences), new Dictionary<string, RoleLoadout>(loadouts))
|
||||
public HumanoidCharacterProfile(HumanoidCharacterProfile other)
|
||||
: this(other.Name,
|
||||
other.FlavorText,
|
||||
other.Species,
|
||||
other.Age,
|
||||
other.Sex,
|
||||
other.Gender,
|
||||
other.Appearance.Clone(),
|
||||
other.SpawnPriority,
|
||||
new Dictionary<string, JobPriority>(other.JobPriorities),
|
||||
other.PreferenceUnavailable,
|
||||
new HashSet<string>(other.AntagPreferences),
|
||||
new HashSet<string>(other.TraitPreferences),
|
||||
new Dictionary<string, RoleLoadout>(other.Loadouts))
|
||||
{
|
||||
}
|
||||
|
||||
@@ -110,23 +175,7 @@ namespace Content.Shared.Preferences
|
||||
/// Defaults to <see cref="SharedHumanoidAppearanceSystem.DefaultSpecies"/> for the species.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public HumanoidCharacterProfile() : this(
|
||||
"John Doe",
|
||||
"",
|
||||
SharedHumanoidAppearanceSystem.DefaultSpecies,
|
||||
18,
|
||||
Sex.Male,
|
||||
Gender.Male,
|
||||
new HumanoidCharacterAppearance(),
|
||||
SpawnPriorityPreference.None,
|
||||
new Dictionary<string, JobPriority>
|
||||
{
|
||||
{SharedGameTicker.FallbackOverflowJob, JobPriority.High}
|
||||
},
|
||||
PreferenceUnavailableMode.SpawnAsOverflow,
|
||||
new List<string>(),
|
||||
new List<string>(),
|
||||
new Dictionary<string, RoleLoadout>())
|
||||
public HumanoidCharacterProfile()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -137,23 +186,10 @@ namespace Content.Shared.Preferences
|
||||
/// <returns>Humanoid character profile with default settings.</returns>
|
||||
public static HumanoidCharacterProfile DefaultWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies)
|
||||
{
|
||||
return new(
|
||||
"John Doe",
|
||||
"",
|
||||
species,
|
||||
18,
|
||||
Sex.Male,
|
||||
Gender.Male,
|
||||
HumanoidCharacterAppearance.DefaultWithSpecies(species),
|
||||
SpawnPriorityPreference.None,
|
||||
new Dictionary<string, JobPriority>
|
||||
{
|
||||
{SharedGameTicker.FallbackOverflowJob, JobPriority.High}
|
||||
},
|
||||
PreferenceUnavailableMode.SpawnAsOverflow,
|
||||
new List<string>(),
|
||||
new List<string>(),
|
||||
new Dictionary<string, RoleLoadout>());
|
||||
return new()
|
||||
{
|
||||
Species = species,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: This should eventually not be a visual change only.
|
||||
@@ -198,36 +234,17 @@ namespace Content.Shared.Preferences
|
||||
|
||||
var name = GetName(species, gender);
|
||||
|
||||
return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), SpawnPriorityPreference.None,
|
||||
new Dictionary<string, JobPriority>
|
||||
{
|
||||
{SharedGameTicker.FallbackOverflowJob, JobPriority.High},
|
||||
}, PreferenceUnavailableMode.StayInLobby, new List<string>(), new List<string>(), new Dictionary<string, RoleLoadout>());
|
||||
return new HumanoidCharacterProfile()
|
||||
{
|
||||
Name = name,
|
||||
Sex = sex,
|
||||
Age = age,
|
||||
Gender = gender,
|
||||
Species = species,
|
||||
Appearance = HumanoidCharacterAppearance.Random(species, sex),
|
||||
};
|
||||
}
|
||||
|
||||
public string Name { get; private set; }
|
||||
public string FlavorText { get; private set; }
|
||||
public string Species { get; private set; }
|
||||
|
||||
[DataField("age")]
|
||||
public int Age { get; private set; }
|
||||
|
||||
[DataField("sex")]
|
||||
public Sex Sex { get; private set; }
|
||||
|
||||
[DataField("gender")]
|
||||
public Gender Gender { get; private set; }
|
||||
|
||||
public ICharacterAppearance CharacterAppearance => Appearance;
|
||||
|
||||
[DataField("appearance")]
|
||||
public HumanoidCharacterAppearance Appearance { get; private set; }
|
||||
public SpawnPriorityPreference SpawnPriority { get; private set; }
|
||||
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
|
||||
public IReadOnlyList<string> AntagPreferences => _antagPreferences;
|
||||
public IReadOnlyList<string> TraitPreferences => _traitPreferences;
|
||||
public PreferenceUnavailableMode PreferenceUnavailable { get; private set; }
|
||||
|
||||
public HumanoidCharacterProfile WithName(string name)
|
||||
{
|
||||
return new(this) { Name = name };
|
||||
@@ -271,7 +288,10 @@ namespace Content.Shared.Preferences
|
||||
|
||||
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities)
|
||||
{
|
||||
return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences, _loadouts);
|
||||
return new(this)
|
||||
{
|
||||
_jobPriorities = new Dictionary<string, JobPriority>(jobPriorities),
|
||||
};
|
||||
}
|
||||
|
||||
public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority)
|
||||
@@ -285,7 +305,11 @@ namespace Content.Shared.Preferences
|
||||
{
|
||||
dictionary[jobId] = priority;
|
||||
}
|
||||
return new(this, dictionary, _antagPreferences, _traitPreferences, _loadouts);
|
||||
|
||||
return new(this)
|
||||
{
|
||||
_jobPriorities = dictionary,
|
||||
};
|
||||
}
|
||||
|
||||
public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode)
|
||||
@@ -295,50 +319,47 @@ namespace Content.Shared.Preferences
|
||||
|
||||
public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences)
|
||||
{
|
||||
return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences, _loadouts);
|
||||
return new(this)
|
||||
{
|
||||
_antagPreferences = new HashSet<string>(antagPreferences),
|
||||
};
|
||||
}
|
||||
|
||||
public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref)
|
||||
{
|
||||
var list = new List<string>(_antagPreferences);
|
||||
var list = new HashSet<string>(_antagPreferences);
|
||||
if (pref)
|
||||
{
|
||||
if (!list.Contains(antagId))
|
||||
{
|
||||
list.Add(antagId);
|
||||
}
|
||||
list.Add(antagId);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (list.Contains(antagId))
|
||||
{
|
||||
list.Remove(antagId);
|
||||
}
|
||||
list.Remove(antagId);
|
||||
}
|
||||
|
||||
return new(this, _jobPriorities, list, _traitPreferences, _loadouts);
|
||||
return new(this)
|
||||
{
|
||||
_antagPreferences = list,
|
||||
};
|
||||
}
|
||||
|
||||
public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref)
|
||||
{
|
||||
var list = new List<string>(_traitPreferences);
|
||||
var list = new HashSet<string>(_traitPreferences);
|
||||
|
||||
// TODO: Maybe just refactor this to HashSet? Same with _antagPreferences
|
||||
if (pref)
|
||||
{
|
||||
if (!list.Contains(traitId))
|
||||
{
|
||||
list.Add(traitId);
|
||||
}
|
||||
list.Add(traitId);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (list.Contains(traitId))
|
||||
{
|
||||
list.Remove(traitId);
|
||||
}
|
||||
list.Remove(traitId);
|
||||
}
|
||||
return new(this, _jobPriorities, _antagPreferences, list, _loadouts);
|
||||
|
||||
return new(this)
|
||||
{
|
||||
_traitPreferences = list,
|
||||
};
|
||||
}
|
||||
|
||||
public string Summary =>
|
||||
@@ -497,10 +518,10 @@ namespace Content.Shared.Preferences
|
||||
PreferenceUnavailable = prefsUnavailableMode;
|
||||
|
||||
_antagPreferences.Clear();
|
||||
_antagPreferences.AddRange(antags);
|
||||
_antagPreferences.UnionWith(antags);
|
||||
|
||||
_traitPreferences.Clear();
|
||||
_traitPreferences.AddRange(traits);
|
||||
_traitPreferences.UnionWith(traits);
|
||||
|
||||
// Checks prototypes exist for all loadouts and dump / set to default if not.
|
||||
var toRemove = new ValueList<string>();
|
||||
@@ -513,7 +534,7 @@ namespace Content.Shared.Preferences
|
||||
continue;
|
||||
}
|
||||
|
||||
loadouts.EnsureValid(session, collection);
|
||||
loadouts.EnsureValid(this, session, collection);
|
||||
}
|
||||
|
||||
foreach (var value in toRemove)
|
||||
@@ -539,27 +560,26 @@ namespace Content.Shared.Preferences
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is HumanoidCharacterProfile other && MemberwiseEquals(other);
|
||||
return ReferenceEquals(this, obj) || obj is HumanoidCharacterProfile other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(
|
||||
HashCode.Combine(
|
||||
Name,
|
||||
Species,
|
||||
Age,
|
||||
Sex,
|
||||
Gender,
|
||||
Appearance
|
||||
),
|
||||
SpawnPriority,
|
||||
PreferenceUnavailable,
|
||||
_jobPriorities,
|
||||
_antagPreferences,
|
||||
_traitPreferences,
|
||||
_loadouts
|
||||
);
|
||||
var hashCode = new HashCode();
|
||||
hashCode.Add(_jobPriorities);
|
||||
hashCode.Add(_antagPreferences);
|
||||
hashCode.Add(_traitPreferences);
|
||||
hashCode.Add(_loadouts);
|
||||
hashCode.Add(Name);
|
||||
hashCode.Add(FlavorText);
|
||||
hashCode.Add(Species);
|
||||
hashCode.Add(Age);
|
||||
hashCode.Add((int)Sex);
|
||||
hashCode.Add((int)Gender);
|
||||
hashCode.Add(Appearance);
|
||||
hashCode.Add((int)SpawnPriority);
|
||||
hashCode.Add((int)PreferenceUnavailable);
|
||||
return hashCode.ToHashCode();
|
||||
}
|
||||
|
||||
public void SetLoadout(RoleLoadout loadout)
|
||||
@@ -581,10 +601,12 @@ namespace Content.Shared.Preferences
|
||||
}
|
||||
|
||||
copied[loadout.Role] = loadout.Clone();
|
||||
return new(this, _jobPriorities, _antagPreferences, _traitPreferences, copied);
|
||||
var profile = Clone();
|
||||
profile._loadouts = copied;
|
||||
return profile;
|
||||
}
|
||||
|
||||
public RoleLoadout GetLoadoutOrDefault(string id, IEntityManager entManager, IPrototypeManager protoManager)
|
||||
public RoleLoadout GetLoadoutOrDefault(string id, ProtoId<SpeciesPrototype>? species, IEntityManager entManager, IPrototypeManager protoManager)
|
||||
{
|
||||
if (!_loadouts.TryGetValue(id, out var loadout))
|
||||
{
|
||||
@@ -595,5 +617,10 @@ namespace Content.Shared.Preferences
|
||||
loadout.SetDefault(protoManager);
|
||||
return loadout;
|
||||
}
|
||||
|
||||
public HumanoidCharacterProfile Clone()
|
||||
{
|
||||
return new HumanoidCharacterProfile(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,13 @@ public sealed partial class GroupLoadoutEffect : LoadoutEffect
|
||||
[DataField(required: true)]
|
||||
public ProtoId<LoadoutEffectGroupPrototype> Proto;
|
||||
|
||||
public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
var effectsProto = collection.Resolve<IPrototypeManager>().Index(Proto);
|
||||
|
||||
foreach (var effect in effectsProto.Effects)
|
||||
{
|
||||
if (!effect.Validate(loadout, session, collection, out reason))
|
||||
if (!effect.Validate(profile, loadout, session, collection, out reason))
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect
|
||||
[DataField(required: true)]
|
||||
public JobRequirement Requirement = default!;
|
||||
|
||||
public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
var manager = collection.Resolve<ISharedPlaytimeManager>();
|
||||
var playtimes = manager.GetPlayTimes(session);
|
||||
|
||||
@@ -11,6 +11,7 @@ public abstract partial class LoadoutEffect
|
||||
/// Tries to validate the effect.
|
||||
/// </summary>
|
||||
public abstract bool Validate(
|
||||
HumanoidCharacterProfile profile,
|
||||
RoleLoadout loadout,
|
||||
ICommonSession session,
|
||||
IDependencyCollection collection,
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed partial class PointsCostLoadoutEffect : LoadoutEffect
|
||||
public int Cost = 1;
|
||||
|
||||
public override bool Validate(
|
||||
HumanoidCharacterProfile profile,
|
||||
RoleLoadout loadout,
|
||||
ICommonSession session,
|
||||
IDependencyCollection collection,
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Preferences.Loadouts.Effects;
|
||||
|
||||
public sealed class SpeciesLoadoutEffect
|
||||
public sealed partial class SpeciesLoadoutEffect : LoadoutEffect
|
||||
{
|
||||
|
||||
[DataField(required: true)]
|
||||
public List<ProtoId<SpeciesPrototype>> Species = new();
|
||||
|
||||
public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection,
|
||||
[NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
if (Species.Contains(profile.Species))
|
||||
{
|
||||
reason = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
reason = FormattedMessage.FromUnformatted(Loc.GetString("loadout-group-species-restriction"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ namespace Content.Shared.Preferences.Loadouts;
|
||||
/// <summary>
|
||||
/// Specifies the selected prototype and custom data for a loadout.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class Loadout
|
||||
[Serializable, NetSerializable, DataDefinition]
|
||||
public sealed partial class Loadout
|
||||
{
|
||||
[DataField]
|
||||
public ProtoId<LoadoutPrototype> Prototype;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Random;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Player;
|
||||
@@ -11,11 +13,13 @@ namespace Content.Shared.Preferences.Loadouts;
|
||||
/// <summary>
|
||||
/// Contains all of the selected data for a role's loadout.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RoleLoadout
|
||||
[Serializable, NetSerializable, DataDefinition]
|
||||
public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
|
||||
{
|
||||
public readonly ProtoId<RoleLoadoutPrototype> Role;
|
||||
[DataField]
|
||||
public ProtoId<RoleLoadoutPrototype> Role;
|
||||
|
||||
[DataField]
|
||||
public Dictionary<ProtoId<LoadoutGroupPrototype>, List<Loadout>> SelectedLoadouts = new();
|
||||
|
||||
/*
|
||||
@@ -44,7 +48,7 @@ public sealed class RoleLoadout
|
||||
/// <summary>
|
||||
/// Ensures all prototypes exist and effects can be applied.
|
||||
/// </summary>
|
||||
public void EnsureValid(ICommonSession session, IDependencyCollection collection)
|
||||
public void EnsureValid(HumanoidCharacterProfile profile, ICommonSession session, IDependencyCollection collection)
|
||||
{
|
||||
var groupRemove = new ValueList<string>();
|
||||
var protoManager = collection.Resolve<IPrototypeManager>();
|
||||
@@ -81,7 +85,7 @@ public sealed class RoleLoadout
|
||||
}
|
||||
|
||||
// Validate the loadout can be applied (e.g. points).
|
||||
if (!IsValid(session, loadout.Prototype, collection, out _))
|
||||
if (!IsValid(profile, session, loadout.Prototype, collection, out _))
|
||||
{
|
||||
loadouts.RemoveAt(i);
|
||||
continue;
|
||||
@@ -167,7 +171,7 @@ public sealed class RoleLoadout
|
||||
/// <summary>
|
||||
/// Returns whether a loadout is valid or not.
|
||||
/// </summary>
|
||||
public bool IsValid(ICommonSession session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
public bool IsValid(HumanoidCharacterProfile profile, ICommonSession session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
|
||||
{
|
||||
reason = null;
|
||||
|
||||
@@ -180,7 +184,7 @@ public sealed class RoleLoadout
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!protoManager.TryIndex(Role, out var roleProto))
|
||||
if (!protoManager.HasIndex(Role))
|
||||
{
|
||||
reason = FormattedMessage.FromUnformatted("loadouts-prototype-missing");
|
||||
return false;
|
||||
@@ -190,7 +194,7 @@ public sealed class RoleLoadout
|
||||
|
||||
foreach (var effect in loadoutProto.Effects)
|
||||
{
|
||||
valid = valid && effect.Validate(this, session, collection, out reason);
|
||||
valid = valid && effect.Validate(profile, this, session, collection, out reason);
|
||||
}
|
||||
|
||||
return valid;
|
||||
@@ -257,4 +261,21 @@ public sealed class RoleLoadout
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Equals(RoleLoadout? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return Role.Equals(other.Role) && SelectedLoadouts.SequenceEqual(other.SelectedLoadouts) && Points == other.Points;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is RoleLoadout other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Role, SelectedLoadouts, Points);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Robotics;
|
||||
using Content.Shared.Robotics.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Robotics.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Robotics console for managing borgs.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, Access(typeof(SharedRoboticsConsoleSystem))]
|
||||
[AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
public sealed partial class RoboticsConsoleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Address and data of each cyborg.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<string, CyborgControlData> Cyborgs = new();
|
||||
|
||||
/// <summary>
|
||||
/// After not responding for this length of time borgs are removed from the console.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Timeout = TimeSpan.FromSeconds(10);
|
||||
|
||||
/// <summary>
|
||||
/// Radio channel to send messages on.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<RadioChannelPrototype> RadioChannel = "Science";
|
||||
|
||||
/// <summary>
|
||||
/// Radio message sent when destroying a borg.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId DestroyMessage = "robotics-console-cyborg-destroyed";
|
||||
|
||||
/// <summary>
|
||||
/// Cooldown on destroying borgs to prevent complete abuse.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan DestroyCooldown = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// When a borg can next be destroyed.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
[AutoNetworkedField, AutoPausedField]
|
||||
public TimeSpan NextDestroy = TimeSpan.Zero;
|
||||
}
|
||||
126
Content.Shared/Robotics/RoboticsConsoleUi.cs
Normal file
126
Content.Shared/Robotics/RoboticsConsoleUi.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Robotics;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum RoboticsConsoleUiKey : byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RoboticsConsoleState : BoundUserInterfaceState
|
||||
{
|
||||
/// <summary>
|
||||
/// Map of device network addresses to cyborg data.
|
||||
/// </summary>
|
||||
public Dictionary<string, CyborgControlData> Cyborgs;
|
||||
|
||||
public RoboticsConsoleState(Dictionary<string, CyborgControlData> cyborgs)
|
||||
{
|
||||
Cyborgs = cyborgs;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to disable the selected cyborg.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RoboticsConsoleDisableMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public readonly string Address;
|
||||
|
||||
public RoboticsConsoleDisableMessage(string address)
|
||||
{
|
||||
Address = address;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message to destroy the selected cyborg.
|
||||
/// </summary>
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class RoboticsConsoleDestroyMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public readonly string Address;
|
||||
|
||||
public RoboticsConsoleDestroyMessage(string address)
|
||||
{
|
||||
Address = address;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All data a client needs to render the console UI for a single cyborg.
|
||||
/// Created by <c>BorgTransponderComponent</c> and sent to clients by <c>RoboticsConsoleComponent</c>.
|
||||
/// </summary>
|
||||
[DataRecord, Serializable, NetSerializable]
|
||||
public record struct CyborgControlData
|
||||
{
|
||||
/// <summary>
|
||||
/// Texture of the borg chassis.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public SpriteSpecifier? ChassisSprite;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the borg chassis.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string ChassisName = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the borg's entity, including its silicon id.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string Name = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Battery charge from 0 to 1.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Charge;
|
||||
|
||||
/// <summary>
|
||||
/// How many modules this borg has, just useful information for roboticists.
|
||||
/// Lets them keep track of the latejoin borgs that need new modules and stuff.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int ModuleCount;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the borg has a brain installed or not.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool HasBrain;
|
||||
|
||||
/// <summary>
|
||||
/// When this cyborg's data will be deleted.
|
||||
/// Set by the console when receiving the packet.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan Timeout = TimeSpan.Zero;
|
||||
|
||||
public CyborgControlData(SpriteSpecifier? chassisSprite, string chassisName, string name, float charge, int moduleCount, bool hasBrain)
|
||||
{
|
||||
ChassisSprite = chassisSprite;
|
||||
ChassisName = chassisName;
|
||||
Name = name;
|
||||
Charge = charge;
|
||||
ModuleCount = moduleCount;
|
||||
HasBrain = hasBrain;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RoboticsConsoleConstants
|
||||
{
|
||||
// broadcast by cyborgs on Robotics Console frequency
|
||||
public const string NET_CYBORG_DATA = "cyborg-data";
|
||||
|
||||
// sent by robotics console to cyborgs on Cyborg Control frequency
|
||||
public const string NET_DISABLE_COMMAND = "cyborg-disable";
|
||||
public const string NET_DESTROY_COMMAND = "cyborg-destroy";
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Content.Shared.Robotics.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Does nothing, only exists for access right now.
|
||||
/// </summary>
|
||||
public abstract class SharedRoboticsConsoleSystem : EntitySystem
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Shared.Silicons.Borgs.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Periodically broadcasts borg data to robotics consoles.
|
||||
/// When not emagged, handles disabling and destroying commands as expected.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(SharedBorgSystem))]
|
||||
public sealed partial class BorgTransponderComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Sprite of the chassis to send.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public SpriteSpecifier? Sprite;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the chassis to send.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public string Name = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Popup shown to everyone when a borg is disabled.
|
||||
/// Gets passed a string "name".
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId DisabledPopup = "borg-transponder-disabled-popup";
|
||||
|
||||
/// <summary>
|
||||
/// How long to wait between each broadcast.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan BroadcastDelay = TimeSpan.FromSeconds(5);
|
||||
|
||||
/// <summary>
|
||||
/// When to next broadcast data.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan NextBroadcast = TimeSpan.Zero;
|
||||
}
|
||||
@@ -39,6 +39,13 @@ public partial class SiliconLaw : IComparable<SiliconLaw>
|
||||
return Order.CompareTo(other.Order);
|
||||
}
|
||||
|
||||
public bool Equals(SiliconLaw other)
|
||||
{
|
||||
return LawString == other.LawString
|
||||
&& Order == other.Order
|
||||
&& LawIdentifierOverride == other.LawIdentifierOverride;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return a shallow clone of this law.
|
||||
/// </summary>
|
||||
|
||||
@@ -67,7 +67,7 @@ public sealed class SlipperySystem : EntitySystem
|
||||
&& _statusEffects.CanApplyEffect(toSlip, "Stun"); //Should be KnockedDown instead?
|
||||
}
|
||||
|
||||
private void TrySlip(EntityUid uid, SlipperyComponent component, EntityUid other)
|
||||
public void TrySlip(EntityUid uid, SlipperyComponent component, EntityUid other, bool requiresContact = true)
|
||||
{
|
||||
if (HasComp<KnockedDownComponent>(other) && !component.SuperSlippery)
|
||||
return;
|
||||
@@ -89,7 +89,7 @@ public sealed class SlipperySystem : EntitySystem
|
||||
{
|
||||
_physics.SetLinearVelocity(other, physics.LinearVelocity * component.LaunchForwardsMultiplier, body: physics);
|
||||
|
||||
if (component.SuperSlippery)
|
||||
if (component.SuperSlippery && requiresContact)
|
||||
{
|
||||
var sliding = EnsureComp<SlidingComponent>(other);
|
||||
sliding.CollidingEntities.Add(uid);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
@@ -31,6 +33,34 @@ public abstract class SharedStationSpawningSystem : EntitySystem
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Equips the given starting gears from a `RoleLoadout` onto an entity.
|
||||
/// </summary>
|
||||
public void EquipRoleLoadout(EntityUid entity, RoleLoadout loadout, RoleLoadoutPrototype roleProto)
|
||||
{
|
||||
// Order loadout selections by the order they appear on the prototype.
|
||||
foreach (var group in loadout.SelectedLoadouts.OrderBy(x => roleProto.Groups.FindIndex(e => e == x.Key)))
|
||||
{
|
||||
foreach (var items in group.Value)
|
||||
{
|
||||
if (!PrototypeManager.TryIndex(items.Prototype, out var loadoutProto))
|
||||
{
|
||||
Log.Error($"Unable to find loadout prototype for {items.Prototype}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!PrototypeManager.TryIndex(loadoutProto.Equipment, out var startingGear))
|
||||
{
|
||||
Log.Error($"Unable to find starting gear {loadoutProto.Equipment} for loadout {loadoutProto}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle any extra data here.
|
||||
EquipStartingGear(entity, startingGear, raiseEvent: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="EquipStartingGear(Robust.Shared.GameObjects.EntityUid,System.Nullable{Robust.Shared.Prototypes.ProtoId{Content.Shared.Roles.StartingGearPrototype}},bool)"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -37,17 +37,22 @@ public sealed class GeneralStationRecordConsoleState : BoundUserInterfaceState
|
||||
public readonly GeneralStationRecord? Record;
|
||||
public readonly Dictionary<uint, string>? RecordListing;
|
||||
public readonly StationRecordsFilter? Filter;
|
||||
public readonly bool CanDeleteEntries;
|
||||
|
||||
public GeneralStationRecordConsoleState(uint? key, GeneralStationRecord? record,
|
||||
Dictionary<uint, string>? recordListing, StationRecordsFilter? newFilter)
|
||||
public GeneralStationRecordConsoleState(uint? key,
|
||||
GeneralStationRecord? record,
|
||||
Dictionary<uint, string>? recordListing,
|
||||
StationRecordsFilter? newFilter,
|
||||
bool canDeleteEntries)
|
||||
{
|
||||
SelectedKey = key;
|
||||
Record = record;
|
||||
RecordListing = recordListing;
|
||||
Filter = newFilter;
|
||||
CanDeleteEntries = canDeleteEntries;
|
||||
}
|
||||
|
||||
public GeneralStationRecordConsoleState() : this(null, null, null, null)
|
||||
public GeneralStationRecordConsoleState() : this(null, null, null, null, false)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -69,3 +74,15 @@ public sealed class SelectStationRecord : BoundUserInterfaceMessage
|
||||
SelectedKey = selectedKey;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class DeleteStationRecord : BoundUserInterfaceMessage
|
||||
{
|
||||
public DeleteStationRecord(uint id)
|
||||
{
|
||||
Id = id;
|
||||
}
|
||||
|
||||
public readonly uint Id;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public sealed class MagnetPickupSystem : EntitySystem
|
||||
// the problem is that stack pickups delete the original entity, which is fine, but due to
|
||||
// game state handling we can't show a lerp animation for it.
|
||||
var nearXform = Transform(near);
|
||||
var nearMap = nearXform.MapPosition;
|
||||
var nearMap = _transform.GetMapCoordinates(near, xform: nearXform);
|
||||
var nearCoords = EntityCoordinates.FromMap(moverCoords.EntityId, nearMap, _transform, EntityManager);
|
||||
|
||||
if (!_storage.Insert(uid, near, out var stacked, storageComp: storage, playSound: !playedSound))
|
||||
|
||||
@@ -111,6 +111,7 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
SubscribeLocalEvent<StorageComponent, AfterInteractEvent>(AfterInteract);
|
||||
SubscribeLocalEvent<StorageComponent, DestructionEventArgs>(OnDestroy);
|
||||
SubscribeLocalEvent<StorageComponent, BoundUIOpenedEvent>(OnBoundUIOpen);
|
||||
SubscribeLocalEvent<StorageComponent, LockToggledEvent>(OnLockToggled);
|
||||
SubscribeLocalEvent<MetaDataComponent, StackCountChangedEvent>(OnStackCountChanged);
|
||||
|
||||
SubscribeLocalEvent<StorageComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
|
||||
@@ -1406,6 +1407,25 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
return _nextSmallest[item.Size];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a storage's UI is open by anyone when locked, and closes it unless they're an admin.
|
||||
/// </summary>
|
||||
private void OnLockToggled(EntityUid uid, StorageComponent component, ref LockToggledEvent args)
|
||||
{
|
||||
if (!args.Locked)
|
||||
return;
|
||||
|
||||
// Gets everyone looking at the UI
|
||||
foreach (var actor in _ui.GetActors(uid, StorageComponent.StorageUiKey.Key).ToList())
|
||||
{
|
||||
if (_admin.HasAdminFlag(actor, AdminFlags.Admin))
|
||||
continue;
|
||||
|
||||
// And closes it unless they're an admin
|
||||
_ui.CloseUi(uid, StorageComponent.StorageUiKey.Key, actor);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStackCountChanged(EntityUid uid, MetaDataComponent component, StackCountChangedEvent args)
|
||||
{
|
||||
if (_containerSystem.TryGetContainingContainer(uid, out var container, component) &&
|
||||
@@ -1418,15 +1438,15 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
|
||||
private void HandleOpenBackpack(ICommonSession? session)
|
||||
{
|
||||
HandleOpenSlotUI(session, "back");
|
||||
HandleToggleSlotUI(session, "back");
|
||||
}
|
||||
|
||||
private void HandleOpenBelt(ICommonSession? session)
|
||||
{
|
||||
HandleOpenSlotUI(session, "belt");
|
||||
HandleToggleSlotUI(session, "belt");
|
||||
}
|
||||
|
||||
private void HandleOpenSlotUI(ICommonSession? session, string slot)
|
||||
private void HandleToggleSlotUI(ICommonSession? session, string slot)
|
||||
{
|
||||
if (session is not { } playerSession)
|
||||
return;
|
||||
@@ -1440,7 +1460,14 @@ public abstract class SharedStorageSystem : EntitySystem
|
||||
if (!ActionBlocker.CanInteract(playerEnt, storageEnt))
|
||||
return;
|
||||
|
||||
OpenStorageUI(storageEnt.Value, playerEnt);
|
||||
if (!_ui.IsUiOpen(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt))
|
||||
{
|
||||
OpenStorageUI(storageEnt.Value, playerEnt);
|
||||
}
|
||||
else
|
||||
{
|
||||
_ui.CloseUi(storageEnt.Value, StorageComponent.StorageUiKey.Key, playerEnt);
|
||||
}
|
||||
}
|
||||
|
||||
protected void ClearCantFillReasons()
|
||||
|
||||
@@ -75,14 +75,14 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable
|
||||
public EntProtoId? ProductAction;
|
||||
|
||||
/// <summary>
|
||||
/// The listing ID of the related upgrade listing. Can be used to link a <see cref="ProductAction"/> to an
|
||||
/// upgrade or to use standalone as an upgrade
|
||||
/// The listing ID of the related upgrade listing. Can be used to link a <see cref="ProductAction"/> to an
|
||||
/// upgrade or to use standalone as an upgrade
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<ListingPrototype>? ProductUpgradeID;
|
||||
public ProtoId<ListingPrototype>? ProductUpgradeId;
|
||||
|
||||
/// <summary>
|
||||
/// Keeps track of the current action entity this is tied to, for action upgrades
|
||||
/// Keeps track of the current action entity this is tied to, for action upgrades
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[NonSerialized]
|
||||
@@ -161,7 +161,7 @@ public partial class ListingData : IEquatable<ListingData>, ICloneable
|
||||
Priority = Priority,
|
||||
ProductEntity = ProductEntity,
|
||||
ProductAction = ProductAction,
|
||||
ProductUpgradeID = ProductUpgradeID,
|
||||
ProductUpgradeId = ProductUpgradeId,
|
||||
ProductActionEntity = ProductActionEntity,
|
||||
ProductEvent = ProductEvent,
|
||||
PurchaseAmount = PurchaseAmount,
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Content.Shared.Tabletop
|
||||
[Dependency] protected readonly ActionBlockerSystem ActionBlockerSystem = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transforms = default!;
|
||||
[Dependency] protected readonly SharedTransformSystem Transforms = default!;
|
||||
[Dependency] private readonly IMapManager _mapMan = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -41,8 +41,8 @@ namespace Content.Shared.Tabletop
|
||||
|
||||
// Move the entity and dirty it (we use the map ID from the entity so noone can try to be funny and move the item to another map)
|
||||
var transform = EntityManager.GetComponent<TransformComponent>(moved);
|
||||
_transforms.SetParent(moved, transform, _mapMan.GetMapEntityId(transform.MapID));
|
||||
_transforms.SetLocalPositionNoLerp(transform, msg.Coordinates.Position);
|
||||
Transforms.SetParent(moved, transform, _mapMan.GetMapEntityId(transform.MapID));
|
||||
Transforms.SetLocalPositionNoLerp(transform, msg.Coordinates.Position);
|
||||
}
|
||||
|
||||
private void OnDraggingPlayerChanged(TabletopDraggingPlayerChangedEvent msg, EntitySessionEventArgs args)
|
||||
|
||||
@@ -8,5 +8,7 @@ namespace Content.Shared.Traits.Assorted;
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed partial class PermanentBlindnessComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField]
|
||||
public int Blindness = 0; // How damaged should their eyes be. Set 0 for maximum damage.
|
||||
}
|
||||
|
||||
|
||||
@@ -18,15 +18,14 @@ public sealed class PermanentBlindnessSystem : EntitySystem
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<PermanentBlindnessComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<PermanentBlindnessComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<PermanentBlindnessComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<PermanentBlindnessComponent, EyeDamageChangedEvent>(OnDamageChanged);
|
||||
SubscribeLocalEvent<PermanentBlindnessComponent, ExaminedEvent>(OnExamined);
|
||||
}
|
||||
|
||||
private void OnExamined(Entity<PermanentBlindnessComponent> blindness, ref ExaminedEvent args)
|
||||
{
|
||||
if (args.IsInDetailsRange && !_net.IsClient)
|
||||
if (args.IsInDetailsRange && !_net.IsClient && blindness.Comp.Blindness == 0)
|
||||
{
|
||||
args.PushMarkup(Loc.GetString("permanent-blindness-trait-examined", ("target", Identity.Entity(blindness, EntityManager))));
|
||||
}
|
||||
@@ -37,28 +36,17 @@ public sealed class PermanentBlindnessSystem : EntitySystem
|
||||
_blinding.UpdateIsBlind(blindness.Owner);
|
||||
}
|
||||
|
||||
private void OnStartup(Entity<PermanentBlindnessComponent> blindness, ref ComponentStartup args)
|
||||
private void OnMapInit(Entity<PermanentBlindnessComponent> blindness, ref MapInitEvent args)
|
||||
{
|
||||
if (!_entityManager.TryGetComponent<BlindableComponent>(blindness, out var blindable))
|
||||
return;
|
||||
|
||||
var damageToDeal = (int) BlurryVisionComponent.MaxMagnitude - blindable.EyeDamage;
|
||||
|
||||
if (damageToDeal <= 0)
|
||||
return;
|
||||
|
||||
_blinding.AdjustEyeDamage(blindness.Owner, damageToDeal);
|
||||
}
|
||||
|
||||
private void OnDamageChanged(Entity<PermanentBlindnessComponent> blindness, ref EyeDamageChangedEvent args)
|
||||
{
|
||||
if (args.Damage >= BlurryVisionComponent.MaxMagnitude)
|
||||
return;
|
||||
|
||||
if (!_entityManager.TryGetComponent<BlindableComponent>(blindness, out var blindable))
|
||||
return;
|
||||
|
||||
var damageRestoration = (int) BlurryVisionComponent.MaxMagnitude - args.Damage;
|
||||
_blinding.AdjustEyeDamage(blindness.Owner, damageRestoration);
|
||||
if (blindness.Comp.Blindness != 0)
|
||||
_blinding.SetMinDamage(new Entity<BlindableComponent?>(blindness.Owner, blindable), blindness.Comp.Blindness);
|
||||
else
|
||||
{
|
||||
var maxMagnitudeInt = (int) BlurryVisionComponent.MaxMagnitude;
|
||||
_blinding.SetMinDamage(new Entity<BlindableComponent?>(blindness.Owner, blindable), maxMagnitudeInt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +220,7 @@ public abstract partial class SharedTetherGunSystem : EntitySystem
|
||||
_blocker.UpdateCanMove(target);
|
||||
|
||||
// Invisible tether entity
|
||||
var tether = Spawn("TetherEntity", Transform(target).MapPosition);
|
||||
var tether = Spawn("TetherEntity", TransformSystem.GetMapCoordinates(target));
|
||||
var tetherPhysics = Comp<PhysicsComponent>(tether);
|
||||
component.TetherEntity = tether;
|
||||
_physics.WakeBody(tether);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Weapons.Ranged.Events;
|
||||
using Content.Shared.Weapons.Ranged.Systems;
|
||||
using Robust.Shared.Audio;
|
||||
@@ -139,6 +137,12 @@ public sealed partial class GunComponent : Component
|
||||
[ViewVariables]
|
||||
public EntityCoordinates? ShootCoordinates = null;
|
||||
|
||||
/// <summary>
|
||||
/// Who the gun is being requested to shoot at directly.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public EntityUid? Target = null;
|
||||
|
||||
/// <summary>
|
||||
/// The base value for how many shots to fire per burst.
|
||||
/// </summary>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user