Диверсионный отряд (#985)

* Диверсионный отряд

* фиксы

* фиксы линтера

* Спрайт терминала икаруса

* фикс пинпоинтера

* Доработки ДО

* Фиксы удалённого управления шаттлом

* Не запускать ДО если на может быть меньше 3 глав
This commit is contained in:
Vigers Ray 2025-01-01 07:53:31 +03:00 committed by GitHub
parent dfa7738f1b
commit f84969630d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
140 changed files with 11290 additions and 83 deletions

View file

@ -0,0 +1,23 @@
using Content.Shared._Sunrise.AssaultOps;
using Content.Shared.StatusIcon.Components;
using Robust.Shared.Prototypes;
namespace Content.Client._Sunrise.AssaultOps.AssaultOps;
public sealed class AssaultOpsSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AssaultOperativeComponent, GetStatusIconsEvent>(GetVampireIcon);
}
private void GetVampireIcon(EntityUid uid, AssaultOperativeComponent component, ref GetStatusIconsEvent args)
{
var iconPrototype = _prototype.Index(component.StatusIcon);
args.StatusIcons.Add(iconPrototype);
}
}

View file

@ -0,0 +1,55 @@
using Content.Shared._Sunrise.AssaultOps.Icarus;
namespace Content.Client._Sunrise.AssaultOps.Icarus;
public sealed class IcarusTerminalBoundUserInterface : BoundUserInterface
{
private IcarusTerminalWindow? _window;
public IcarusTerminalBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new IcarusTerminalWindow();
_window.OnClose += Close;
_window.OpenCentered();
_window.FireButtonPressed += OnFireButtonPressed;
}
private void OnFireButtonPressed()
{
if (_window == null)
return;
SendMessage(new IcarusTerminalFireMessage());
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null)
return;
if (state is not IcarusTerminalUiState cast)
return;
_window.UpdateState(cast);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
if (_window != null)
_window.OnClose -= Close;
_window?.Dispose();
}
}

View file

@ -0,0 +1,21 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'icarus-ui-window-title'}"
MinSize="300 120">
<BoxContainer Orientation="Vertical">
<Button Name="FireButton"
Text="{Loc 'icarus-ui-fire-button'}"
StyleClasses="Caution"
MinHeight="50"
Disabled="True" />
<BoxContainer Name="TimerBox" Orientation="Horizontal" Visible="False">
<Label Text="{Loc 'icarus-ui-timer-label'}" />
<Label Text=" " />
<Label Name="TimerValue" Text="-" />
</BoxContainer>
<BoxContainer Name="CooldownBox" Orientation="Horizontal" Visible="False">
<Label Text="{Loc 'icarus-ui-cooldown-label'}" />
<Label Text=" " />
<Label Name="CooldownValue" Text="-" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View file

@ -0,0 +1,36 @@
using Content.Shared._Sunrise.AssaultOps.Icarus;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client._Sunrise.AssaultOps.Icarus;
[GenerateTypedNameReferences]
public sealed partial class IcarusTerminalWindow : DefaultWindow
{
public event Action? FireButtonPressed;
public IcarusTerminalWindow()
{
RobustXamlLoader.Load(this);
FireButton.OnPressed += _ => FireButtonPressed?.Invoke();
}
public void UpdateState(IcarusTerminalUiState state)
{
FireButton.Disabled = state.Status != IcarusTerminalStatus.FIRE_READY;
TimerBox.Visible = state.Status == IcarusTerminalStatus.FIRE_PREPARING;
CooldownBox.Visible = state.Status == IcarusTerminalStatus.COOLDOWN;
switch (state.Status)
{
case IcarusTerminalStatus.FIRE_PREPARING:
TimerValue.Text = state.RemainingTime.ToString();
break;
case IcarusTerminalStatus.COOLDOWN:
CooldownValue.Text = state.CooldownTime.ToString();
break;
}
}
}

View file

@ -0,0 +1,54 @@
using Content.Shared._Sunrise.Interrogator;
using Content.Shared.Verbs;
using Robust.Client.GameObjects;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client._Sunrise.Interrogator;
public sealed class InterrogatorSystem: SharedInterrogatorSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InterrogatorComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<InterrogatorComponent, GetVerbsEvent<AlternativeVerb>>(AddAlternativeVerbs);
SubscribeLocalEvent<InterrogatorComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnAppearanceChange(EntityUid uid, InterrogatorComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
{
return;
}
if (!_appearance.TryGetData<bool>(uid, InterrogatorComponent.InterrogatorVisuals.ContainsEntity, out var isOpen, args.Component)
|| !_appearance.TryGetData<bool>(uid, InterrogatorComponent.InterrogatorVisuals.IsOn, out var isOn, args.Component))
{
return;
}
if (isOpen)
{
args.Sprite.LayerSetState(InterrogatorVisualLayers.Base, "open");
args.Sprite.LayerSetVisible(InterrogatorVisualLayers.Extract, false);
args.Sprite.DrawDepth = (int) DrawDepth.Objects;
}
else
{
args.Sprite.DrawDepth = (int) DrawDepth.Mobs;
args.Sprite.LayerSetState(InterrogatorVisualLayers.Extract, isOn ? "extraction-on" : "extraction-off");
args.Sprite.LayerSetVisible(InterrogatorVisualLayers.Extract, true);
}
}
}
public enum InterrogatorVisualLayers : byte
{
Base,
Extract,
}

View file

@ -1,3 +1,4 @@
using Content.Server._Sunrise.AssaultOps;
using Content.Server.Administration.Commands;
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
@ -179,5 +180,19 @@ public sealed partial class AdminVerbSystem
Message = Loc.GetString("admin-verb-make-vampire"),
};
args.Verbs.Add(vampire);
Verb assaultOperative = new()
{
Text = Loc.GetString("admin-verb-text-make-assault-operative"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster46_contraband"),
Act = () =>
{
_antag.ForceMakeAntag<AssaultOpsRuleComponent>(targetPlayer, "AssaultOps");
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-assault-operative"),
};
args.Verbs.Add(assaultOperative);
}
}

View file

@ -1,11 +0,0 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Tags grid as nuke ops shuttle
/// </summary>
[RegisterComponent]
public sealed partial class NukeOpsShuttleComponent : Component
{
[DataField]
public EntityUid AssociatedRule;
}

View file

@ -1,7 +1,13 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat.Managers;
using Content.Server.Jobs;
using Content.Server.Preferences.Managers;
using Content.Server.Revolutionary.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
@ -13,6 +19,8 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
[Dependency] protected readonly IChatManager ChatManager = default!;
[Dependency] protected readonly GameTicker GameTicker = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
// Not protected, just to be used in utility methods
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
@ -37,6 +45,55 @@ public abstract partial class GameRuleSystem<T> : EntitySystem where T : ICompon
var query = QueryAllRules();
while (query.MoveNext(out var uid, out _, out var gameRule))
{
// Sunrise-Start
if (gameRule.MinCommandStaff > 0)
{
var availableHeads = new List<string>();
foreach (var playerSession in args.Players)
{
var userId = playerSession.UserId;
var preferencesManager = IoCManager.Resolve<IServerPreferencesManager>();
var prefs = preferencesManager.GetPreferences(userId);
var profile = prefs.SelectedCharacter as HumanoidCharacterProfile;
if (profile == null)
continue;
foreach (var profileJobPriority in profile.JobPriorities)
{
if (profileJobPriority.Value == JobPriority.Never)
continue;
if (!_prototype.TryIndex<JobPrototype>(profileJobPriority.Key.Id, out var job))
continue;
foreach (var special in job.Special)
{
if (special is not AddComponentSpecial componentSpecial)
continue;
foreach (var componentSpecialComponent in componentSpecial.Components)
{
var copy = _componentFactory.GetComponent(componentSpecialComponent.Value);
if (copy is CommandStaffComponent)
{
if (availableHeads.Contains(profileJobPriority.Key))
continue;
availableHeads.Add(profileJobPriority.Key);
}
}
}
}
}
if (gameRule.CancelPresetOnTooFewPlayers && availableHeads.Count < gameRule.MinCommandStaff)
{
ChatManager.SendAdminAnnouncement(Loc.GetString("preset-not-enough-ready-command-staff",
("readyCommandStaffCount", args.Players.Length),
("minimumCommandStaff", gameRule.MinCommandStaff),
("presetName", ToPrettyString(uid))));
args.Cancel();
}
}
// Sunrise-Edit
var minPlayers = gameRule.MinPlayers;
if (args.Players.Length >= minPlayers)
continue;

View file

@ -1,3 +1,4 @@
using System.Linq;
using Content.Server.Cuffs;
using Content.Server.Forensics;
using Content.Server.Humanoid;
@ -19,9 +20,11 @@ using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Random;
using System.Numerics;
using Content.Server.Body.Components;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Systems;
using Content.Shared.Store.Components;
using Robust.Server.Containers;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
@ -41,6 +44,7 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
[Dependency] private readonly PullingSystem _pullingSystem = default!;
[Dependency] private readonly EntityLookupSystem _lookupSystem = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly ContainerSystem _container = default!;
private EntityQuery<PhysicsComponent> _physicsQuery;
private HashSet<Entity<MapGridComponent>> _targetGrids = [];
@ -56,7 +60,37 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
SubscribeLocalEvent<SubdermalImplantComponent, ActivateImplantEvent>(OnActivateImplantEvent);
SubscribeLocalEvent<SubdermalImplantComponent, UseScramImplantEvent>(OnScramImplant);
SubscribeLocalEvent<SubdermalImplantComponent, UseDnaScramblerImplantEvent>(OnDnaScramblerImplant);
SubscribeLocalEvent<ImplantedComponent, BeingGibbedEvent>(OnGibbed);
}
private void OnGibbed(EntityUid uid, ImplantedComponent component, BeingGibbedEvent args)
{
if (!_container.TryGetContainer(uid, ImplanterComponent.ImplantSlotId, out var implantContainer))
return;
foreach (var implant in implantContainer.ContainedEntities)
{
if (!TryComp<SubdermalImplantComponent>(implant, out var subdermalImplant))
continue;
if (!subdermalImplant.DropContainerItemsIfGibbed)
continue;
if (!_container.TryGetContainer(implant, BaseStorageId, out var storageImplant))
continue;
var entCoords = Transform(uid).Coordinates;
var containedEntites = storageImplant.ContainedEntities.ToArray();
foreach (var entity in containedEntites)
{
if (Terminating(entity))
continue;
_container.RemoveEntity(storageImplant.Owner, entity, force: true, destination: entCoords);
}
}
}
private void OnStoreRelay(EntityUid uid, StoreComponent store, ImplantRelayEvent<AfterInteractUsingEvent> implantRelay)

View file

@ -29,6 +29,7 @@ public sealed class MindShieldSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<SubdermalImplantComponent, ImplantImplantedEvent>(ImplantCheck);
SubscribeLocalEvent<SubdermalImplantComponent, ImplantEjectEvent>(ImplantCheck);
}
/// <summary>
@ -43,6 +44,16 @@ public sealed class MindShieldSystem : EntitySystem
}
}
// Sunrise-Start
public void ImplantCheck(EntityUid uid, SubdermalImplantComponent comp, ref ImplantEjectEvent ev)
{
if (_tag.HasTag(ev.Implant, MindShieldTag) && ev.Implanted != null)
{
RemCompDeferred<MindShieldComponent>(ev.Implanted.Value);
}
}
// Sunrise-End
/// <summary>
/// Checks if the implanted person was a Rev or Head Rev and remove role or destroy mindshield respectively.
/// </summary>

View file

@ -2,9 +2,11 @@ using Content.Shared.Interaction;
using Content.Shared.Pinpointer;
using System.Linq;
using System.Numerics;
using Content.Server.Popups;
using Robust.Shared.Utility;
using Content.Server.Shuttles.Events;
using Content.Shared.IdentityManagement;
using Content.Shared.Verbs;
using Robust.Shared.Random;
namespace Content.Server.Pinpointer;
@ -12,6 +14,8 @@ public sealed class PinpointerSystem : SharedPinpointerSystem
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PopupSystem _popups = default!;
private EntityQuery<TransformComponent> _xformQuery;
@ -22,6 +26,44 @@ public sealed class PinpointerSystem : SharedPinpointerSystem
SubscribeLocalEvent<PinpointerComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<FTLCompletedEvent>(OnLocateTarget);
SubscribeLocalEvent<PinpointerComponent, GetVerbsEvent<AlternativeVerb>>(AddSwitchVerb);
}
private void AddSwitchVerb(EntityUid uid, PinpointerComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanInteract || !args.CanAccess)
return;
AlternativeVerb verb = new()
{
Act = () =>
{
SwitchTarget(uid, component, args.User);
},
Text = Loc.GetString("pinpointer-switch-target"),
Priority = 2
};
args.Verbs.Add(verb);
}
private void SwitchTarget(EntityUid uid, PinpointerComponent component, EntityUid user)
{
if (component.IsActive && component.Component != null)
{
if (!EntityManager.ComponentFactory.TryGetRegistration(component.Component, out var reg))
{
Logger.Error($"Unable to find component registration for {component.Component} for pinpointer!");
DebugTools.Assert(false);
return;
}
var target = FindTargetFromComponent(uid, reg.Type, component.Target);
SetTarget(uid, target, component);
}
_popups.PopupEntity(Loc.GetString("pinpointer-target-switched"), user, user);
}
public override bool TogglePinpointer(EntityUid uid, PinpointerComponent? pinpointer = null)
@ -85,7 +127,7 @@ public sealed class PinpointerSystem : SharedPinpointerSystem
return;
}
var target = FindTargetFromComponent(uid, reg.Type);
var target = FindTargetFromComponent(uid, reg.Type, component.Target);
SetTarget(uid, target, component);
}
}
@ -107,7 +149,7 @@ public sealed class PinpointerSystem : SharedPinpointerSystem
/// Try to find the closest entity from whitelist on a current map
/// Will return null if can't find anything
/// </summary>
private EntityUid? FindTargetFromComponent(EntityUid uid, Type whitelist, TransformComponent? transform = null)
private EntityUid? FindTargetFromComponent(EntityUid uid, Type whitelist, EntityUid? currentTarget, TransformComponent? transform = null)
{
_xformQuery.Resolve(uid, ref transform, false);
@ -128,6 +170,17 @@ public sealed class PinpointerSystem : SharedPinpointerSystem
l.TryAdd(dist, otherUid);
}
if (l.Count > 1)
{
foreach (var target in l.ToList())
{
if (currentTarget == target.Value)
l.Remove(target.Key);
}
return _random.Pick(l).Value;
}
// return uid with a smallest distance
return l.Count > 0 ? l.First().Value : null;
}

View file

@ -20,5 +20,11 @@ namespace Content.Server.Shuttles.Components
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("whitelistSpecific")]
public List<EntityUid> FTLWhitelist = new List<EntityUid>();
// Sunrise-Start
[ViewVariables]
[DataField("portable")]
public bool Portable;
// Sunrise-End
}
}

View file

@ -18,4 +18,10 @@ public sealed partial class DroneConsoleComponent : Component
/// </summary>
[DataField("entity")]
public EntityUid? Entity;
// Sunrise-Start
[ViewVariables]
[DataField("portable")]
public bool Portable;
// Sunrise-End
}

View file

@ -61,7 +61,7 @@ public sealed partial class ShuttleConsoleSystem
var stationUid = _station.GetOwningStation(uid);
if (stationUid == null)
if (stationUid == null && !component.Portable)
return null;
// I know this sucks but needs device linking or something idunno
@ -69,9 +69,9 @@ public sealed partial class ShuttleConsoleSystem
while (query.MoveNext(out var cUid, out _, out var xform))
{
if (xform.GridUid == null ||
if ((xform.GridUid == null ||
!TryComp<StationMemberComponent>(xform.GridUid, out var member) ||
member.Station != stationUid)
member.Station != stationUid) && !component.Portable)
{
continue;
}

View file

@ -124,6 +124,13 @@ public sealed partial class ShuttleConsoleSystem
{
var consoleUid = GetDroneConsole(ent.Owner);
// Sunrise-Start
if (TryComp<DroneConsoleComponent>(consoleUid, out var droneConsole))
{
consoleUid = droneConsole.Entity;
}
// Sunrise-End
if (consoleUid == null)
return;

View file

@ -171,7 +171,7 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
if (!_tags.HasTag(user, "CanPilot") ||
!TryComp<ShuttleConsoleComponent>(uid, out var component) ||
!this.IsPowered(uid, EntityManager) ||
!Transform(uid).Anchored ||
!Transform(uid).Anchored && !component.Portable || // Sunrise-Edit
!_blocker.CanInteract(user, uid))
{
return false;
@ -252,6 +252,13 @@ public sealed partial class ShuttleConsoleSystem : SharedShuttleConsoleSystem
RaiseLocalEvent(entity.Value, ref getShuttleEv);
entity = getShuttleEv.Console;
// Sunrise-Start
if (TryComp<DroneConsoleComponent>(entity, out var droneConsole))
{
entity = droneConsole.Entity;
}
// Sunrise-End
TryComp(entity, out TransformComponent? consoleXform);
var shuttleGridUid = consoleXform?.GridUid;

View file

@ -0,0 +1,14 @@
using Content.Shared.Roles;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server._Sunrise.AssaultOps;
[RegisterComponent]
public sealed partial class AssaultOperativeSpawnerComponent : Component
{
[DataField("rolePrototype", customTypeSerializer:typeof(PrototypeIdSerializer<AntagPrototype>), required:true)]
public string OperativeRolePrototype = default!;
[DataField("startingGearPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<StartingGearPrototype>), required:true)]
public string OperativeStartingGear = default!;
}

View file

@ -0,0 +1,8 @@
using Content.Shared.Roles;
namespace Content.Server._Sunrise.AssaultOps;
[RegisterComponent]
public sealed partial class AssaultOpsRoleComponent : BaseMindRoleComponent
{
}

View file

@ -0,0 +1,86 @@
using Content.Shared.Dataset;
using Content.Shared.NPC.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Utility;
namespace Content.Server._Sunrise.AssaultOps;
[RegisterComponent, Access(typeof(AssaultOpsRuleSystem))]
public sealed partial class AssaultOpsRuleComponent : Component
{
[DataField("icarusKeyImplant", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string IcarusKeyImplant = "IcarusKey";
[DataField("requiredKeys")] public int RequiredKeys = 3;
[DataField("keysCarrierJobs", customTypeSerializer: typeof(PrototypeIdArraySerializer<JobPrototype>))]
public string[] KeysCarrierJobs =
{
"Captain",
"HeadOfSecurity",
"ChiefEngineer",
"ChiefMedicalOfficer",
"ResearchDirector",
"Quartermaster"
};
[DataField("faction", customTypeSerializer: typeof(PrototypeIdSerializer<NpcFactionPrototype>), required: true)]
public string Faction = default!;
[DataField]
public int TCAmountPerOperative = 50;
[DataField]
public int RoundstartOperatives;
[DataField("greetingSound", customTypeSerializer: typeof(SoundSpecifierTypeSerializer))]
public SoundSpecifier? GreetSoundNotification = new SoundPathSpecifier("/Audio/_Sunrise/AssaultOperatives/assault_operatives_greet.ogg",
AudioParams.Default.WithVolume(-6f));
[DataField("winType")] public WinType WinType = WinType.Stalemate;
[DataField("winConditions")] public List<WinCondition> WinConditions = new ();
public EntityUid? ShuttleGrid;
public EntityUid? TargetStation;
}
public enum WinType : byte
{
/// <summary>
/// Operative major win. Goldeneye activated and all ops alive.
/// </summary>
OpsMajor,
/// <summary>
/// Minor win. Goldeneye was activated and some ops alive.
/// </summary>
OpsMinor,
/// <summary>
/// Hearty. Goldeneye activated but no ops alive.
/// </summary>
Hearty,
/// <summary>
/// Stalemate. Goldeneye not activated and ops still alive.
/// </summary>
Stalemate,
/// <summary>
/// Crew major win. Goldeneye not activated and no ops alive.
/// </summary>
CrewMajor
}
public enum WinCondition
{
IcarusActivated,
AllOpsDead,
SomeOpsAlive,
AllOpsAlive
}

View file

@ -0,0 +1,349 @@
using Content.Server._Sunrise.AssaultOps.Icarus;
using Content.Server.Antag;
using Content.Server.Antag.Components;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.Mind;
using Content.Server.Revolutionary.Components;
using Content.Server.RoundEnd;
using Content.Server.Station.Components;
using Content.Server.Traitor.Uplink;
using Content.Shared._Sunrise.AssaultOps;
using Content.Shared.GameTicking;
using Content.Shared.GameTicking.Components;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Mind;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.NPC.Components;
using Content.Shared.NPC.Systems;
using Content.Shared.Roles;
using Content.Shared.Tag;
using Robust.Shared.Containers;
using Robust.Shared.Random;
namespace Content.Server._Sunrise.AssaultOps;
public sealed class AssaultOpsRuleSystem : GameRuleSystem<AssaultOpsRuleComponent>
{
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly SharedSubdermalImplantSystem _subdermalImplant = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly UplinkSystem _uplinkSystem = default!;
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[ValidatePrototypeId<TagPrototype>]
private const string UplinkTagPrototype = "AssaultOpsUplink";
[ValidatePrototypeId<AntagPrototype>]
private const string CommanderAntagProto = "AssaultCommander";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnPlayerSpawned);
SubscribeLocalEvent<IcarusTerminalSystem.IcarusActivatedEvent>(OnIcarusActivated);
SubscribeLocalEvent<AssaultOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<AssaultOpsRuleComponent, AfterAntagEntitySelectedEvent>(OnAfterAntagEntSelected);
SubscribeLocalEvent<AssaultOpsRuleComponent, AntagSelectionCompleteEvent>(OnAfterAntagSelectionComplete); // Sunrise-Edit
SubscribeLocalEvent<AssaultOpsRuleComponent, RuleLoadedGridsEvent>(OnRuleLoadedGrids);
}
protected override void Started(EntityUid uid,
AssaultOpsRuleComponent component,
GameRuleComponent gameRule,
GameRuleStartedEvent args)
{
var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
var eligibleQuery = EntityQueryEnumerator<StationEventEligibleComponent, NpcFactionMemberComponent>();
while (eligibleQuery.MoveNext(out var eligibleUid, out var eligibleComp, out var member))
{
if (!_npcFaction.IsFactionHostile(component.Faction, (eligibleUid, member)))
continue;
eligible.Add((eligibleUid, eligibleComp, member));
}
if (eligible.Count == 0)
return;
component.TargetStation = RobustRandom.Pick(eligible);
if (GameTicker.RunLevel == GameRunLevel.InRound)
{
InsertIcarusKeys(uid, component);
}
}
private void OnRuleLoadedGrids(Entity<AssaultOpsRuleComponent> ent, ref RuleLoadedGridsEvent args)
{
var query = EntityQueryEnumerator<AssaultOpsShuttleComponent>();
while (query.MoveNext(out var uid, out var shuttle))
{
if (Transform(uid).MapID == args.Map)
{
shuttle.AssociatedRule = ent;
break;
}
}
}
private void OnAfterAntagEntSelected(Entity<AssaultOpsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
var target = (ent.Comp.TargetStation is not null) ? Name(ent.Comp.TargetStation.Value) : "the target";
_antag.SendBriefing(args.Session,
Loc.GetString("assaultops-welcome",
("station", target),
("name", Name(ent))),
Color.Red,
ent.Comp.GreetSoundNotification);
// Sunrise-Start
if (!args.GameRule.Comp.UseSpawners)
return;
ent.Comp.RoundstartOperatives = args.GameRule.Comp.SpawnersCount;
var commander = GetCommander(args.GameRule);
if (commander != null)
SetupUplink(commander.Value, ent.Comp);
// Sunrise-End
}
private void OnAfterAntagSelectionComplete(Entity<AssaultOpsRuleComponent> ent, ref AntagSelectionCompleteEvent args)
{
ent.Comp.RoundstartOperatives = args.GameRule.Comp.SelectedMinds.Count;
var commander = GetCommander(args.GameRule);
if (commander != null)
SetupUplink(commander.Value, ent.Comp);
}
private EntityUid? GetCommander(Entity<AntagSelectionComponent> antagSelection)
{
EntityUid? commander = null;
foreach (var compSelectedMind in antagSelection.Comp.SelectedMinds)
{
if (!TryComp<MindComponent>(compSelectedMind.Item1, out var mindComp))
continue;
foreach (var roleInfo in _roles.MindGetAllRoleInfo((compSelectedMind.Item1, mindComp)))
{
if (roleInfo.Prototype != CommanderAntagProto || mindComp.CurrentEntity == null)
continue;
commander = mindComp.CurrentEntity.Value;
}
}
return commander;
}
private void SetupUplink(EntityUid user, AssaultOpsRuleComponent rule)
{
var uplink = _uplinkSystem.FindUplinkByTag(user, UplinkTagPrototype);
if (uplink != null)
_uplinkSystem.SetUplink(user, uplink.Value, rule.TCAmountPerOperative * rule.RoundstartOperatives, true);
}
private void InsertIcarusKeys(EntityUid uid, AssaultOpsRuleComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var query = EntityQueryEnumerator<CommandStaffComponent>();
while (query.MoveNext(out var ent, out var mind))
{
var haveKey = false;
if (_container.TryGetContainer(ent, ImplanterComponent.ImplantSlotId, out var implantContainer))
{
foreach (var implant in implantContainer.ContainedEntities)
{
if (MetaData(implant).EntityPrototype!.ID == component.IcarusKeyImplant)
haveKey = true;
}
}
if (!haveKey)
InsertKey(ent, component.IcarusKeyImplant);
}
}
private bool InsertKey(EntityUid uid, string icarusKeyImplant)
{
var ownedCoords = Transform(uid).Coordinates;
var implant = Spawn(icarusKeyImplant, ownedCoords);
if (!TryComp<SubdermalImplantComponent>(implant, out var implantComp))
return false;
_subdermalImplant.ForceImplant(uid, implant, implantComp);
return true;
}
private void OnIcarusActivated(IcarusTerminalSystem.IcarusActivatedEvent ev)
{
var query = EntityQueryEnumerator<AssaultOpsRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var assaultops, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
{
Logger.Info("AssaultopsRule not added");
continue;
}
assaultops.WinConditions.Add(WinCondition.IcarusActivated);
}
}
private void OnPlayerSpawned(PlayerSpawnCompleteEvent ev)
{
var query = EntityQueryEnumerator<AssaultOpsRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var assaultops, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
var session = ev.Player;
var mind = _mind.GetMind(session.UserId);
if (mind == null)
continue;
if (HasComp<CommandStaffComponent>(ev.Mob))
InsertKey(ev.Mob, assaultops.IcarusKeyImplant);
}
}
private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
{
if (ev.New is not GameRunLevel.PostRound)
return;
var query = QueryActiveRules();
while (query.MoveNext(out var uid, out _, out var rule, out _))
{
OnRoundEnd(uid, rule);
}
}
private void OnRoundEnd(EntityUid uid, AssaultOpsRuleComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
var total = 0;
var alive = 0;
foreach (var (_, state) in EntityQuery<AssaultOperativeComponent, MobStateComponent>())
{
total++;
if (state.CurrentState != MobState.Alive)
continue;
alive++;
break;
}
var allAlive = alive == total;
if (allAlive)
{
if (component.WinConditions.Contains(WinCondition.IcarusActivated))
{
component.WinType = WinType.OpsMajor;
}
else
{
component.WinType = WinType.OpsMinor;
component.WinConditions.Add(WinCondition.AllOpsAlive);
}
}
else if (alive == 0)
{
if (component.WinConditions.Contains(WinCondition.IcarusActivated))
{
component.WinType = WinType.Hearty;
}
else
{
component.WinType = WinType.CrewMajor;
component.WinConditions.Add(WinCondition.AllOpsDead);
}
}
else
{
if (component.WinConditions.Contains(WinCondition.IcarusActivated))
{
component.WinType = WinType.OpsMinor;
}
else
{
component.WinType = WinType.Stalemate;
component.WinConditions.Add(WinCondition.SomeOpsAlive);
}
}
}
private void OnMobStateChanged(EntityUid uid, AssaultOperativeComponent component, MobStateChangedEvent ev)
{
if(ev.NewMobState == MobState.Dead)
CheckRoundShouldEnd();
}
private void CheckRoundShouldEnd()
{
var query = EntityQueryEnumerator<AssaultOpsRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var assaultops, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
if (assaultops.WinType == WinType.CrewMajor || assaultops.WinType == WinType.OpsMajor)
continue;
var operativesAlive = false;
var operatives = EntityQuery<AssaultOperativeComponent, MobStateComponent>(true);
foreach (var (assaultOp, mobState) in operatives)
{
if (mobState.CurrentState is MobState.Alive)
{
operativesAlive = true;
}
}
if (operativesAlive)
continue;
_roundEndSystem.EndRound();
}
}
protected override void AppendRoundEndText(EntityUid uid,
AssaultOpsRuleComponent component,
GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{
var winText = Loc.GetString($"assaultops-{component.WinType.ToString().ToLower()}");
args.AddLine(winText);
foreach (var cond in component.WinConditions)
{
var text = Loc.GetString($"assaultops-cond-{cond.ToString().ToLower()}");
args.AddLine(text);
}
args.AddLine(Loc.GetString("assaultops-list-start"));
var antags =_antag.GetAntagIdentifiers(uid);
foreach (var (_, sessionData, name) in antags)
{
args.AddLine(Loc.GetString("assaultops-list-name", ("name", name), ("user", sessionData.UserName)));
}
}
}

View file

@ -0,0 +1,51 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.Map.Components;
namespace Content.Server._Sunrise.AssaultOps.Icarus.Commands;
[UsedImplicitly]
[AdminCommand(AdminFlags.Fun)]
public sealed class SpawnIcarusCommand : IConsoleCommand
{
public string Command => "spawnicarus";
public string Description => "Spawn Icarus beam and direct to specified grid center.";
public string Help => "spawnicarus <gridId>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError("Incorrect number of arguments. " + Help);
return;
}
if (!EntityUid.TryParse(args[0], out var uid))
{
shell.WriteError("Not a valid entity ID.");
return;
}
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!entityManager.EntityExists(uid))
{
shell.WriteError("That grid does not exist.");
return;
}
var xformQuery = entityManager.GetEntityQuery<TransformComponent>();
if (entityManager.TryGetComponent<MapGridComponent>(uid, out var grid))
{
var icarusSystem = IoCManager.Resolve<IEntityManager>().System<IcarusTerminalSystem>();
var coords = icarusSystem.FireBeam(xformQuery.GetComponent(uid).WorldMatrix.TransformBox(grid.LocalAABB));
shell.WriteLine($"Icarus was spawned: {coords.ToString()}");
}
else
{
shell.WriteError($"No grid exists with ID {uid}");
}
}
}

View file

@ -0,0 +1,31 @@
namespace Content.Server._Sunrise.AssaultOps.Icarus;
[RegisterComponent]
public sealed partial class IcarusBeamComponent : Component
{
/// <summary>
/// Beam moving speed.
/// </summary>
[DataField("speed")]
public float Speed = 8f;
/// <summary>
/// The beam will be automatically cleaned up after this time.
/// </summary>
[DataField("lifetime")]
public TimeSpan Lifetime = TimeSpan.FromSeconds(200);
/// <summary>
/// With this set to true, beam will automatically set the tiles under them to space.
/// </summary>
[DataField("destroyTiles")]
public bool DestroyTiles = true;
[DataField("destroyRadius")]
public float DestroyRadius = 4f;
[DataField("flameRadius")]
public float FlameRadius = 8f;
public TimeSpan LifetimeEnd;
}

View file

@ -0,0 +1,143 @@
using System.Numerics;
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Ghost;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing;
namespace Content.Server._Sunrise.AssaultOps.Icarus;
public sealed class IcarusBeamSystem : EntitySystem
{
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly FlammableSystem _flammable = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly TransformSystem _transform = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQuery<IcarusBeamComponent, TransformComponent>(true);
foreach (var (comp, xform) in query)
{
DestroyEntities(comp, xform);
BurnEntities(comp.Owner, comp, xform);
if (comp.DestroyTiles)
DestroyTiles(comp, xform);
if (_timing.CurTime > comp.LifetimeEnd)
QueueDel(comp.Owner);
}
}
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<IcarusBeamComponent, ComponentInit>(OnComponentInit);
}
private void OnComponentInit(EntityUid uid, IcarusBeamComponent component, ComponentInit args)
{
component.LifetimeEnd = _timing.CurTime + component.Lifetime;
if (!TryComp(uid, out PhysicsComponent? phys))
return;
_physics.SetLinearDamping(uid, phys, 0f);
_physics.SetFriction(uid, phys, 0f);
_physics.SetAngularDamping(uid, phys, 0f);
}
public void LaunchInDirection(EntityUid uid, Vector2 dir, IcarusBeamComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
if (TryComp(uid, out PhysicsComponent? phys))
{
var impulseVector = dir.Normalized() * comp.Speed * phys.Mass;
_physics.ApplyLinearImpulse(uid, impulseVector, body: phys);
_transform.SetWorldRotation(uid, impulseVector.ToWorldAngle());
}
}
/// <summary>
/// Destroy any grid tiles in beam radius.
/// </summary>
private void DestroyTiles(IcarusBeamComponent component, TransformComponent trans)
{
var radius = component.DestroyRadius;
var worldPos = trans.WorldPosition;
var circle = new Circle(worldPos, radius);
var r = new Vector2(radius, radius);
var box = new Box2(worldPos - r, worldPos + r);
foreach (var grid in _map.FindGridsIntersecting(trans.MapID, box))
{
// Bundle these together so we can use the faster helper to set tiles.
var toDestroy = new List<(Vector2i, Tile)>();
foreach (var tile in grid.GetTilesIntersecting(circle))
{
if (tile.Tile.IsEmpty)
continue;
toDestroy.Add((tile.GridIndices, Tile.Empty));
}
grid.SetTiles(toDestroy);
}
}
/// <summary>
/// Handle deleting entities in beam radius.
/// </summary>
private void DestroyEntities(IcarusBeamComponent component, TransformComponent trans)
{
var radius = component.DestroyRadius - 0.5f;
var entitys = _lookup.GetEntitiesInRange(trans.MapID, trans.WorldPosition, radius);
foreach (var entity in entitys)
{
if (!CanDestroy(component, entity))
continue;
QueueDel(entity);
}
}
/// <summary>
/// Handle igniting flammable entities in beam radius.
/// </summary>
private void BurnEntities(EntityUid beam, IcarusBeamComponent component, TransformComponent trans)
{
var radius = component.FlameRadius * 2;
foreach (var entity in _lookup.GetEntitiesInRange(trans.MapID, trans.WorldPosition, radius))
{
if (!CanDestroy(component, entity))
continue;
if (!TryComp<FlammableComponent>(entity, out var flammable))
continue;
flammable.FireStacks += 1;
if (!flammable.OnFire)
_flammable.Ignite(entity, beam);
}
}
private bool CanDestroy(IcarusBeamComponent component, EntityUid entity)
{
return entity != component.Owner &&
!EntityManager.HasComponent<MapGridComponent>(entity) &&
!EntityManager.HasComponent<GhostComponent>(entity);
}
}

View file

@ -0,0 +1,267 @@
using System.Linq;
using System.Numerics;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.RoundEnd;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared._Sunrise.AssaultOps.Icarus;
using Content.Shared.Containers.ItemSlots;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server._Sunrise.AssaultOps.Icarus;
/// <summary>
/// Handle Icarus activation terminal
/// </summary>
public sealed class IcarusTerminalSystem : EntitySystem
{
private const string IcarusBeamPrototypeId = "IcarusBeam";
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly IcarusBeamSystem _icarusSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
[Dependency] private readonly AudioSystem _audio = default!;
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQuery<IcarusTerminalComponent>();
foreach (var terminal in query)
{
switch (terminal.Status)
{
case IcarusTerminalStatus.FIRE_PREPARING:
TickTimer(terminal, frameTime);
break;
case IcarusTerminalStatus.COOLDOWN:
TickCooldown(terminal, frameTime);
break;
}
if (terminal.Status != IcarusTerminalStatus.AWAIT_DISKS)
{
TickTimerEndRound(terminal, frameTime);
}
}
}
private void TickTimerEndRound(IcarusTerminalComponent component, float frameTime)
{
component.TimerRoundEnd -= frameTime;
if (!(component.TimerRoundEnd <= 0))
return;
component.TimerRoundEnd = 0;
_roundEndSystem.EndRound();
}
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<IcarusTerminalComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<IcarusTerminalComponent, EntInsertedIntoContainerMessage>(OnItemSlotInserted);
SubscribeLocalEvent<IcarusTerminalComponent, EntRemovedFromContainerMessage>(OnItemSlotRemoved);
// UI events
SubscribeLocalEvent<IcarusTerminalComponent, IcarusTerminalFireMessage>(OnFireButtonPressed);
}
private void OnInit(EntityUid uid, IcarusTerminalComponent component, ComponentInit args)
{
component.RemainingTime = component.Timer;
UpdateStatus(component);
UpdateUserInterface(component);
}
private void OnItemSlotInserted(EntityUid uid, IcarusTerminalComponent component, ContainerModifiedMessage args)
{
OnItemSlotChanged(component);
}
private void OnItemSlotRemoved(EntityUid uid, IcarusTerminalComponent component, ContainerModifiedMessage args)
{
OnItemSlotChanged(component);
}
private void OnItemSlotChanged(IcarusTerminalComponent component)
{
UpdateStatus(component);
UpdateUserInterface(component);
}
private void OnFireButtonPressed(EntityUid uid, IcarusTerminalComponent component, IcarusTerminalFireMessage args)
{
Fire(component);
}
private void Fire(IcarusTerminalComponent component)
{
if (component.Status == IcarusTerminalStatus.FIRE_PREPARING)
return;
component.RemainingTime = component.Timer;
component.Status = IcarusTerminalStatus.FIRE_PREPARING;
var stationName = "/NTSS14/";
var targetStation = _stationSystem.GetStations().FirstOrNull();
if (targetStation != null)
{
stationName = Name(targetStation.Value);
}
_chatSystem.DispatchGlobalAnnouncement(
Loc.GetString("icarus-fire-announcement", ("seconds", component.Timer),
("station", stationName)),
Loc.GetString("icarus-announce-sender"),
false,
colorOverride: Color.Red);
_audio.PlayGlobal(component.AlertSound, Filter.Broadcast(), false);
}
private void UpdateStatus(IcarusTerminalComponent component)
{
switch (component.Status)
{
case IcarusTerminalStatus.AWAIT_DISKS:
if (IsAccessGranted(component.Owner))
Authorize(component);
break;
case IcarusTerminalStatus.FIRE_READY:
{
if (!IsAccessGranted(component.Owner))
{
component.Status = IcarusTerminalStatus.AWAIT_DISKS;
}
break;
}
}
}
private void UpdateUserInterface(IcarusTerminalComponent component)
{
_userInterfaceSystem.SetUiState(component.Owner, IcarusTerminalUiKey.Key, new IcarusTerminalUiState(
component.Status,
(int) component.RemainingTime,
(int) component.CooldownTime)
);
}
private bool IsAccessGranted(EntityUid uid)
{
return TryComp<ItemSlotsComponent>(uid, out var itemSlotsComponent) && itemSlotsComponent.Slots.Values.All(v => v.HasItem);
}
private void Authorize(IcarusTerminalComponent component)
{
component.Status = IcarusTerminalStatus.FIRE_READY;
if (!component.AuthorizationNotified)
{
_chatSystem.DispatchGlobalAnnouncement(Loc.GetString("icarus-authorized-announcement"),
Loc.GetString("icarus-announce-sender"),
false,
component.ActiveGoldenEyeAlertSound);
component.AuthorizationNotified = true;
RaiseLocalEvent(new IcarusActivatedEvent()
{
OwningStation = Transform(component.Owner).GridUid,
});
}
}
private void TickCooldown(IcarusTerminalComponent component, float frameTime)
{
component.CooldownTime -= frameTime;
if (component.CooldownTime <= 0)
{
component.CooldownTime = 0;
component.Status = IcarusTerminalStatus.AWAIT_DISKS;
UpdateStatus(component);
}
UpdateUserInterface(component);
}
private void TickTimer(IcarusTerminalComponent component, float frameTime)
{
component.RemainingTime -= frameTime;
if (component.RemainingTime <= 0)
{
component.RemainingTime = 0;
ActivateBeamOnStation(component);
}
UpdateUserInterface(component);
}
private void ActivateBeamOnStation(IcarusTerminalComponent component)
{
component.Status = IcarusTerminalStatus.COOLDOWN;
component.CooldownTime = component.Cooldown;
_audio.PlayGlobal(component.FireSound, Filter.Broadcast(), false);
FireBeam(GetStationArea());
}
public MapCoordinates FireBeam(Box2 area)
{
TryGetBeamSpawnLocation(area, out var coords, out var offset);
Logger.DebugS("icarus", $"Try spawn beam on coords: {coords.ToString()}");
var entUid = Spawn(IcarusBeamPrototypeId, coords);
_icarusSystem.LaunchInDirection(entUid, -offset.Normalized());
return coords;
}
private void TryGetBeamSpawnLocation(Box2 area, out MapCoordinates coords,
out Vector2 offset)
{
coords = MapCoordinates.Nullspace;
offset = Vector2.Zero;
var center = area.Center;
var distance = (area.TopRight - center).Length();
var angle = new Angle(_robustRandom.NextFloat() * MathF.Tau);
offset = angle.RotateVec(new Vector2(distance + 50f, 0));
coords = new MapCoordinates(center + offset, _gameTicker.DefaultMap);
}
/// <summary>
/// Determine box of all stations and all of they grids. (copy-paste from pirate gamerule)
/// </summary>
/// <returns>Box of all station grids</returns>
private Box2 GetStationArea()
{
var xformQuery = GetEntityQuery<TransformComponent>();
var areas = _stationSystem.GetStations().SelectMany(s =>
Comp<StationDataComponent>(s).Grids.Select(g =>
xformQuery.GetComponent(g).WorldMatrix.TransformBox(Comp<MapGridComponent>(g).LocalAABB))).ToArray();
var stationArea = areas[0];
for (var i = 1; i < areas.Length; i++)
stationArea.Union(areas[i]);
return stationArea;
}
public sealed class IcarusActivatedEvent : EntityEventArgs
{
public EntityUid? OwningStation;
}
}

View file

@ -0,0 +1,201 @@
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Power.EntitySystems;
using Content.Shared._Sunrise.Interrogator;
using Content.Shared.Climbing.Systems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.Implants.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Power;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
namespace Content.Server._Sunrise.Interrogator
{
public sealed class InterrogatorSystem : SharedInterrogatorSystem
{
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly PowerReceiverSystem _powerReceiverSystem = default!;
[Dependency] private readonly ClimbSystem _climbSystem = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InterrogatorComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<InterrogatorComponent, GetVerbsEvent<AlternativeVerb>>(AddAlternativeVerbs);
SubscribeLocalEvent<InterrogatorComponent, DragDropTargetEvent>(HandleDragDropOn);
SubscribeLocalEvent<InterrogatorComponent, InterrogatorDragFinished>(OnDragFinished);
SubscribeLocalEvent<InterrogatorComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<InterrogatorComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<InterrogatorComponent, EntRemovedFromContainerMessage>(OnEjected);
SubscribeLocalEvent<ActiveInterrogatorComponent, ComponentStartup>(OnExtractStart);
SubscribeLocalEvent<ActiveInterrogatorComponent, ComponentShutdown>(OnExtractStop);
}
private void OnAnchorChanged(EntityUid uid, InterrogatorComponent component, ref AnchorStateChangedEvent args)
{
if (!args.Anchored)
_container.EmptyContainer(component.BodyContainer);
}
private void StopExtracting(Entity<InterrogatorComponent> ent)
{
RemCompDeferred<ActiveInterrogatorComponent>(ent);
}
private void OnExtractStart(Entity<ActiveInterrogatorComponent> ent, ref ComponentStartup args)
{
if (!TryComp<InterrogatorComponent>(ent, out var interrogatorComponent))
return;
//SetAppearance(ent.Owner, MicrowaveVisualState.Cooking, microwaveComponent);
interrogatorComponent.ExtractionProgress = 0;
interrogatorComponent.PlayingStream =
_audio.PlayPvs(interrogatorComponent.ExtractingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5))?.Entity;
UpdateAppearance(ent);
}
private void OnExtractStop(Entity<ActiveInterrogatorComponent> ent, ref ComponentShutdown args)
{
if (!TryComp<InterrogatorComponent>(ent, out var interrogatorComponent))
return;
interrogatorComponent.ExtractionProgress = 0;
//SetAppearance(ent.Owner, MicrowaveVisualState.Idle, microwaveComponent);
interrogatorComponent.PlayingStream = _audio.Stop(interrogatorComponent.PlayingStream);
_audio.PlayPvs(interrogatorComponent.ExtractDoneSound, ent);
}
private void OnEjected(Entity<InterrogatorComponent> interrogator, ref EntRemovedFromContainerMessage args)
{
StopExtracting(interrogator);
}
private void OnPowerChanged(Entity<InterrogatorComponent> entity, ref PowerChangedEvent args)
{
// Needed to avoid adding/removing components on a deleted entity
if (Terminating(entity))
{
return;
}
if (!args.Powered)
{
StopExtracting(entity);
EjectBody(entity.Owner, entity.Comp);
if (_light.TryGetLight(entity.Owner, out var light))
{
_light.SetEnabled(entity.Owner, false, light);
}
}
UpdateAppearance(entity.Owner, entity.Comp);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveInterrogatorComponent, InterrogatorComponent>();
while (query.MoveNext(out var uid, out var _, out var interrogator))
{
if (!_powerReceiverSystem.IsPowered(uid))
continue;
if (interrogator.BodyContainer.ContainedEntity == null)
continue;
if (_mobState.IsDead(interrogator.BodyContainer.ContainedEntity.Value))
{
StopExtracting((uid, interrogator));
EjectBody(uid, interrogator);
continue;
}
if (interrogator.BodyContainer.ContainedEntity == null)
continue;
interrogator.ExtractionProgress += frameTime;
if (interrogator.ExtractionProgress < interrogator.ExtractionTime)
continue;
EjectImplants(interrogator.BodyContainer.ContainedEntity.Value);
StopExtracting((uid, interrogator));
EjectBody(uid, interrogator);
}
}
private void EjectImplants(EntityUid target)
{
if (_container.TryGetContainer(target, ImplanterComponent.ImplantSlotId, out var implantContainer))
{
var implantCompQuery = GetEntityQuery<SubdermalImplantComponent>();
// Create a copy of the ContainedEntities list
var implants = implantContainer.ContainedEntities.ToList();
foreach (var implant in implants)
{
if (!implantCompQuery.TryGetComponent(implant, out var implantComp))
continue;
// Don't remove a permanent implant and look for the next that can be drawn
if (!_container.CanRemove(implant, implantContainer))
{
continue;
}
_container.Remove(implant, implantContainer);
}
}
}
protected override EntityUid? EjectBody(EntityUid uid, InterrogatorComponent? component)
{
if (!Resolve(uid, ref component))
return null;
if (component.BodyContainer.ContainedEntity is not { Valid: true } contained)
return null;
base.EjectBody(uid, component);
_climbSystem.ForciblySetClimbing(contained, uid);
return contained;
}
private void OnDragFinished(Entity<InterrogatorComponent> entity, ref InterrogatorDragFinished args)
{
if (args.Cancelled || args.Handled || args.Args.Target == null)
return;
if (InsertBody(entity.Owner, args.Args.Target.Value, entity.Comp))
{
_adminLogger.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(args.User)} inserted {ToPrettyString(args.Args.Target.Value)} into {ToPrettyString(entity.Owner)}");
}
args.Handled = true;
}
private void HandleDragDropOn(Entity<InterrogatorComponent> entity, ref DragDropTargetEvent args)
{
if (entity.Comp.BodyContainer.ContainedEntity != null)
return;
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, entity.Comp.EntryDelay, new InterrogatorDragFinished(), entity, target: args.Dragged, used: entity)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = false,
};
_doAfterSystem.TryStartDoAfter(doAfterArgs);
args.Handled = true;
}
}
}

View file

@ -103,7 +103,7 @@ public sealed partial class CCVars
/// 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);
CVarDef.Create("shuttle.mass_limit", 0f, CVar.SERVERONLY); // Sunrise-Edit
/// <summary>
/// How long to knock down entities for if they aren't buckled when FTL starts and stops.

View file

@ -23,6 +23,9 @@ public sealed partial class GameRuleComponent : Component
[DataField]
public int MinPlayers;
[DataField]
public int MinCommandStaff;
/// <summary>
/// If true, this rule not having enough players will cancel the preset selection.
/// If false, it will simply not run silently.

View file

@ -55,7 +55,7 @@ public sealed partial class ImplanterComponent : Component
/// Good for single-use injectors
/// </summary>
[DataField, AutoNetworkedField]
public bool ImplantOnly;
public bool ImplantOnly = true; // Sunrsie-Edit
/// <summary>
/// The current mode of the implanter

View file

@ -49,6 +49,16 @@ public sealed partial class SubdermalImplantComponent : Component
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
// Sunrise-Start
[ViewVariables(VVAccess.ReadWrite)]
[DataField("dropContainerItemsIfGibbed"), AutoNetworkedField]
public bool DropContainerItemsIfGibbed = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("deleteWhenDraw"), AutoNetworkedField]
public bool DeleteWhenDraw;
// Sunrise-End
}
/// <summary>

View file

@ -28,6 +28,7 @@ public abstract class SharedImplanterSystem : EntitySystem
SubscribeLocalEvent<ImplanterComponent, ComponentInit>(OnImplanterInit);
SubscribeLocalEvent<ImplanterComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<ImplanterComponent, EntRemovedFromContainerMessage>(OnEntEjected); // Sunrise-Edit
SubscribeLocalEvent<ImplanterComponent, ExaminedEvent>(OnExamine);
}
@ -43,8 +44,19 @@ public abstract class SharedImplanterSystem : EntitySystem
{
var implantData = EntityManager.GetComponent<MetaDataComponent>(args.Entity);
component.ImplantData = (implantData.EntityName, implantData.EntityDescription);
ChangeOnImplantVisualizer(uid, component); // Sunrise-Edit
Dirty(uid, component); // Sunrise-Edit
}
// Sunrise-Start
private void OnEntEjected(EntityUid uid, ImplanterComponent component, EntRemovedFromContainerMessage args)
{
component.ImplantData = ("", "");
ChangeOnImplantVisualizer(uid, component);
Dirty(uid, component);
}
// Sunrise-End
private void OnExamine(EntityUid uid, ImplanterComponent component, ExaminedEvent args)
{
if (!component.ImplanterSlot.HasItem || !args.IsInDetailsRange)

View file

@ -36,6 +36,11 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
if (component.ImplantedEntity == null || _net.IsClient)
return;
// Sunrise-Start
if (args.Container.ID != "implant")
return;
// Sunrise-End
if (!string.IsNullOrWhiteSpace(component.ImplantAction))
{
_actionsSystem.AddAction(component.ImplantedEntity.Value, ref component.Action, component.ImplantAction, uid);
@ -60,6 +65,11 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
private void OnRemoveAttempt(EntityUid uid, SubdermalImplantComponent component, ContainerGettingRemovedAttemptEvent args)
{
// Sunrise-Start
if (args.Container.ID != "implant")
return;
// Sunrise-End
if (component.Permanent && component.ImplantedEntity != null)
args.Cancel();
}
@ -69,6 +79,11 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
if (component.ImplantedEntity == null || Terminating(component.ImplantedEntity.Value))
return;
// Sunrise-Start
if (args.Container.ID != "implant")
return;
// Sunrise-End
if (component.ImplantAction != null)
_actionsSystem.RemoveProvidedActions(component.ImplantedEntity.Value, uid);
@ -86,6 +101,11 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
_container.RemoveEntity(storageImplant.Owner, entity, force: true, destination: entCoords);
}
// Sunrsie-Start
var ev = new ImplantEjectEvent(uid, component.ImplantedEntity.Value);
RaiseLocalEvent(uid, ref ev);
// Sunrsie-End
}
/// <summary>
@ -221,3 +241,18 @@ public readonly struct ImplantImplantedEvent
Implanted = implanted;
}
}
// Sunrise-Start
[ByRefEvent]
public readonly struct ImplantEjectEvent
{
public readonly EntityUid Implant;
public readonly EntityUid? Implanted;
public ImplantEjectEvent(EntityUid implant, EntityUid? implanted)
{
Implant = implant;
Implanted = implanted;
}
}
// Sunrise-End

View file

@ -33,6 +33,9 @@ namespace Content.Shared.PDA
[DataField("id", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? IdCard;
[DataField("pen", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? Pen;
[ViewVariables] public EntityUid? ContainedId;
[ViewVariables] public bool FlashlightOn;

View file

@ -25,6 +25,8 @@ namespace Content.Shared.PDA
{
if (pda.IdCard != null)
pda.IdSlot.StartingItem = pda.IdCard;
if (pda.Pen != null)
pda.PenSlot.StartingItem = pda.Pen;
ItemSlotsSystem.AddItemSlot(uid, PdaComponent.PdaIdSlotId, pda.IdSlot);
ItemSlotsSystem.AddItemSlot(uid, PdaComponent.PdaPenSlotId, pda.PenSlot);

View file

@ -0,0 +1,13 @@
using Content.Shared.StatusIcon;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared._Sunrise.AssaultOps
{
[RegisterComponent, NetworkedComponent]
public sealed partial class AssaultOperativeComponent : Component
{
[DataField("statusIcon")]
public ProtoId<FactionIconPrototype> StatusIcon { get; set; } = "SyndicateFaction";
}
}

View file

@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Server._Sunrise.AssaultOps;
[RegisterComponent, NetworkedComponent]
public sealed partial class AssaultOpsShuttleComponent : Component
{
[DataField]
public EntityUid AssociatedRule;
}

View file

@ -0,0 +1,7 @@
namespace Content.Shared._Sunrise.AssaultOps.Icarus;
/// <summary>
/// Used for Icarus terminal activation
/// </summary>
[RegisterComponent]
public sealed partial class IcarusKeyComponent : Component {}

View file

@ -0,0 +1,68 @@
using Robust.Shared.Audio;
using Robust.Shared.Containers;
namespace Content.Shared._Sunrise.AssaultOps.Icarus;
/// <summary>
/// Used for Icarus terminal activation
/// </summary>
[RegisterComponent]
public sealed partial class IcarusTerminalComponent : Component
{
/// <summary>
/// Default fire timer value in seconds.
/// </summary>
[DataField("timer")]
[ViewVariables(VVAccess.ReadWrite)]
public int Timer = 25;
/// <summary>
/// How long until the beam can arm again after fire.
/// </summary>
[DataField("cooldown")]
[ViewVariables(VVAccess.ReadWrite)]
public int Cooldown = 240;
/// <summary>
/// Current status of a terminal.
/// </summary>
[ViewVariables]
public IcarusTerminalStatus Status = IcarusTerminalStatus.AWAIT_DISKS;
/// <summary>
/// Time until beam will be spawned in seconds.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float RemainingTime;
/// <summary>
/// Time until beam cooldown will expire in seconds.
/// </summary>
[ViewVariables]
public float CooldownTime;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("keySlots")]
public int KeySlots = 3;
[ViewVariables]
public Container KeyContainer = default!;
public const string KeyContainerName = "key_slots";
[ViewVariables]
public float TimerRoundEnd = 1200;
[DataField("activeGoldenEyeAlertSound")]
public SoundSpecifier ActiveGoldenEyeAlertSound = new SoundPathSpecifier("/Audio/_Sunrise/AssaultOperatives/golden_eye_alarm.ogg");
[DataField("alertSound")]
public SoundSpecifier AlertSound = new SoundPathSpecifier("/Audio/_Sunrise/AssaultOperatives/icarus_alarm.ogg");
[DataField("fireSound")]
public SoundSpecifier FireSound = new SoundPathSpecifier("/Audio/_Sunrise/AssaultOperatives/sunbeam_fire.ogg");
/// <summary>
/// Check if already notified about system authorization
/// </summary>
public bool AuthorizationNotified = false;
}

View file

@ -0,0 +1,37 @@
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.AssaultOps.Icarus;
[Serializable, NetSerializable]
public enum IcarusTerminalUiKey
{
Key,
}
public enum IcarusTerminalStatus : byte
{
AWAIT_DISKS,
FIRE_READY,
FIRE_PREPARING,
COOLDOWN
}
[Serializable, NetSerializable]
public sealed class IcarusTerminalUiState : BoundUserInterfaceState
{
public IcarusTerminalStatus Status { get; }
public int RemainingTime { get; }
public int CooldownTime { get; }
public IcarusTerminalUiState(IcarusTerminalStatus status, int remainingTime, int cooldownTime)
{
Status = status;
RemainingTime = remainingTime;
CooldownTime = cooldownTime;
}
}
[Serializable, NetSerializable]
public sealed class IcarusTerminalFireMessage : BoundUserInterfaceMessage
{
}

View file

@ -0,0 +1,6 @@
namespace Content.Shared._Sunrise.Interrogator;
[RegisterComponent]
public sealed partial class ActiveInterrogatorComponent : Component
{
}

View file

@ -0,0 +1,38 @@
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.Interrogator;
[RegisterComponent]
public sealed partial class InterrogatorComponent : Component
{
[ViewVariables]
public ContainerSlot BodyContainer = default!;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float ExtractionTime = 30f;
[ViewVariables]
public float ExtractionProgress = 0;
public EntityUid? PlayingStream;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("entryDelay")]
public float EntryDelay = 2f;
// SUNRISE-TODO: Более подходящий звук работы, мейби взять из сс13
[DataField("extractingSound")]
public SoundSpecifier ExtractingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg");
[DataField("extractDoneSound")]
public SoundSpecifier ExtractDoneSound = new SoundPathSpecifier("/Audio/_Sunrise/Interrogator/ding.ogg");
[Serializable, NetSerializable]
public enum InterrogatorVisuals : byte
{
ContainsEntity,
IsOn
}
}

View file

@ -0,0 +1,169 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Components;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Standing;
using Content.Shared.Stunnable;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Serialization;
namespace Content.Shared._Sunrise.Interrogator;
public abstract partial class SharedInterrogatorSystem: EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedStandingStateSystem _standingStateSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InterrogatorComponent, CanDropTargetEvent>(OnInterrogatorCanDropOn);
}
protected void OnComponentInit(EntityUid uid, InterrogatorComponent interrogatorComponent, ComponentInit args)
{
interrogatorComponent.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(uid, "body_container");
}
private void OnInterrogatorCanDropOn(EntityUid uid, InterrogatorComponent component, ref CanDropTargetEvent args)
{
if (args.Handled)
return;
args.CanDrop = HasComp<BodyComponent>(args.Dragged);
args.Handled = true;
}
protected void UpdateAppearance(EntityUid uid, InterrogatorComponent? component = null, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref component))
return;
var interrogatorEnabled = HasComp<ActiveInterrogatorComponent>(uid);
if (_light.TryGetLight(uid, out var light))
{
_light.SetEnabled(uid, interrogatorEnabled && component.BodyContainer.ContainedEntity != null, light);
}
if (!Resolve(uid, ref appearance))
return;
_appearanceSystem.SetData(uid, InterrogatorComponent.InterrogatorVisuals.ContainsEntity, component.BodyContainer.ContainedEntity == null, appearance);
_appearanceSystem.SetData(uid, InterrogatorComponent.InterrogatorVisuals.IsOn, interrogatorEnabled, appearance);
}
public bool InsertBody(EntityUid uid, EntityUid target, InterrogatorComponent component)
{
if (component.BodyContainer.ContainedEntity != null)
return false;
if (!HasComp<MobStateComponent>(target))
return false;
var xform = Transform(target);
_containerSystem.Insert((target, xform), component.BodyContainer);
_standingStateSystem.Stand(target, force: true); // Force-stand the mob so that the cryo pod sprite overlays it fully
UpdateAppearance(uid, component);
return true;
}
private void TryEjectBody(EntityUid uid, EntityUid userId, InterrogatorComponent? component)
{
if (!Resolve(uid, ref component))
{
return;
}
var ejected = EjectBody(uid, component);
if (ejected != null)
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ejected.Value)} ejected from {ToPrettyString(uid)} by {ToPrettyString(userId)}");
}
protected virtual EntityUid? EjectBody(EntityUid uid, InterrogatorComponent? component)
{
if (!Resolve(uid, ref component))
return null;
if (component.BodyContainer.ContainedEntity is not {Valid: true} contained)
return null;
_containerSystem.Remove(contained, component.BodyContainer);
// Insidecomponent is removed automatically in its EntGotRemovedFromContainerMessage listener
// RemComp<Insidecomponent>(contained);
// Restore the correct position of the patient. Checking the components manually feels hacky, but I did not find a better way for now.
if (HasComp<KnockedDownComponent>(contained) || _mobStateSystem.IsIncapacitated(contained))
{
_standingStateSystem.Down(contained);
}
else
{
_standingStateSystem.Stand(contained);
}
UpdateAppearance(uid, component);
return contained;
}
private void TryStartExtract(EntityUid uid, EntityUid userId, InterrogatorComponent? interrogatorComponent)
{
if (!Resolve(uid, ref interrogatorComponent))
{
return;
}
if (interrogatorComponent.BodyContainer.ContainedEntity == null)
return;
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(uid)} start extract impants from {ToPrettyString(interrogatorComponent.BodyContainer.ContainedEntity)} by {ToPrettyString(userId)}");
EnsureComp<ActiveInterrogatorComponent>(uid);
}
protected void AddAlternativeVerbs(EntityUid uid, InterrogatorComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
// Eject verb
if (component.BodyContainer.ContainedEntity != null)
{
args.Verbs.Add(new AlternativeVerb
{
Text = Loc.GetString("interrogator-verb-noun-occupant"),
Category = VerbCategory.Eject,
Priority = 1,
Act = () => TryEjectBody(uid, args.User, component)
});
}
// Extract verb
if (component.BodyContainer.ContainedEntity != null && !HasComp<ActiveInterrogatorComponent>(uid))
{
args.Verbs.Add(new AlternativeVerb
{
Text = Loc.GetString("interrogator-verb-start-extract"),
Priority = 2,
Act = () => TryStartExtract(uid, args.User, component)
});
}
}
[Serializable, NetSerializable]
public sealed partial class InterrogatorDragFinished : SimpleDoAfterEvent
{
}
}

View file

@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class NukeOpsShuttleComponent : Component
{
[DataField]
public EntityUid AssociatedRule;
}

Binary file not shown.

View file

@ -0,0 +1,2 @@
ent-Interrogator = Экстрактор имплантов
.desc = Устройство предназначеное для извлечения имплантов из гуманоидов.

View file

@ -1,6 +1,9 @@
ent-EnergySwordDoubleBiocode = { ent-EnergySwordDouble }
.suffix = БИОКОД
.desc = { ent-EnergySwordDouble.desc }
ent-EnergyDaggerBiocode = { ent-EnergyDagger }
.suffix = БИОКОД
.desc = { ent-EnergyDagger.desc }
ent-EnergySwordBiocode = { ent-EnergySword }
.suffix = БИОКОД
.desc = { ent-EnergySword.desc }

View file

@ -153,3 +153,6 @@ ent-PiratePDA = КПК с Адамовой головой
ent-SyndiAgentPDA = медицинский ало-красный КПК
.suffix = КПК оперативника медика Синдиката, Ядерный Оперативник
.desc = Смотря на этот КПК, ваше сердцебиение учащается... словно его владелец проводил немыслимые и ужасные медицинские эксперименты.
ent-AssaultOpsPDA = { ent-PassengerPDA }
.desc = { ent-PassengerPDA }

View file

@ -0,0 +1,2 @@
admin-verb-text-make-assault-operative = Сделать цель членом ДО.
admin-verb-make-assault-operative = Сделать цель членом диверсионного отряда.

View file

@ -0,0 +1,5 @@
roles-antag-assault-operative-name = Член диверсионного отряда
roles-antag-assault-operative-objective = Похищайте глав станции, извлекайте из голов ключи и обратите оружие Nanotrasen против них.
roles-antag-assault-commander-name = Командир диверсионного отряда
roles-antag-assault-commander-objective = Руководите секретной операцией по похищению глав станции с целью достать ключи доступа Икарус.

View file

@ -0,0 +1,20 @@
# Announce
icarus-announce-sender = Оборонная сеть "Икарус"
icarus-authorized-announcement = ОБНАРУЖЕНА НЕСАНКЦИОНИРОВАННАЯ ЗАГРУЗКА КЛЮЧ-КАРТ. ВСЕ КЛЮЧИ ЗАГРУЖЕНЫ!
icarus-fire-announcement = /// ВЗЛОМ ЗАЩИЩЕННОЙ СЕТИ "ИКАРУС" ///
Обнаружен несанкционированный доступ к оборонной сеть "Икарус"
ИКАРУС онлайн.
Обнаружено переопределение системы нацеливания...
Новая цель: { $station }
Активированы протоколы стрельбы ИКАРУС.
РАСЧЕТНОЕ ВРЕМЯ выстрела: { $seconds } { $seconds ->
[one] секунду
[few] секунды
*[other] секунд
}.
# UI
icarus-ui-window-title = Терминал Icarus
icarus-ui-fire-button = Огонь
icarus-ui-timer-label = Время до выстрела:
icarus-ui-cooldown-label = Перезарядка:

View file

@ -0,0 +1,15 @@
assault-ops-document-goal = ####### СОВЕРШЕННО СЕКРЕТНО ########
Вы - специальный диверсионный отряд синдиката.
Ваша цель - взлом системы обритального оружия Icarus в секторе Алеф.
Для взлома системы вам необходимо добыть 3 ключа доступа Golden Key, которые помещены в тела руководящего состава станции.
Вам был предоставлен шатл, одежда хамелеон и аплинки со всем необходимым для выполнения задачи. Так же у вас есть навигаторы которые указывают местонахождение ближайшего ключа. Навигатор покажет новую цель только после установки целевого ключа в терминал, учтите это.
Запомните, ваша задача действовать скрытно, не дайте персоналу обнаружить свой шатл или раскрыть ваши цели.
После взлома системы у вас есть возможность открыть огонь по станции, используйте данную возможность по собственному желанию.
####### СОВЕРШЕННО СЕКРЕТНО ########

View file

@ -0,0 +1,25 @@
assaultops-title = Диверсионный отряд
assaultops-description = На станцию нацелился диверсионный отряд с целью завладеть доступом к секретному оружию NanoTransen Icarus.
assaultops-welcome =
Вы - член элитного диверсионного отряда. Ваша задача - напасть на { $station } и завладеть всеми ключами доступа GoldenEye, расположенных в головах руководства объекта. Ваше руководство, Синдикат, снабдило вас всем необходимым для выполнения этой задачи.
Удачи агент!
assaultops-opsmajor = [bold][color=crimson]Крупная победа Синдиката![/color][/bold]
assaultops-opsminor = [bold][color=crimson]Малая победа Синдиката![/color][/bold]
assaultops-hearty = [bold][color=crimson]Посмертная победа![/color][/bold]
assaultops-stalemate = [bold][color=yellow]Ничейный исход![/color][/bold]
assaultops-crewmajor = [bold][color=green]Разгромная победа экипажа![/color][/bold]
assaultops-cond-icarusactivated = Икарус был активирован.
assaultops-cond-allopsdead = Все члены диверсионного отряда погибли.
assaultops-cond-someopssalive = Несколько членов диверсионного отряда погибли.
assaultops-cond-allopsalive = Все члены диверсионного отряда выжили.
assaultops-list-start = Членами диверсионного отряда были:
assaultops-list-name = - [color=white]{ $name }[/color] ([color=gray]{ $user }[/color])
assaultops-not-enough-ready-players = Недостаточно игроков готовы к игре! { $readyPlayersCount } игроков из необходимых { $minimumPlayers } готовы. Нельзя начать Диверсионный отряд.
assaultops-not-enough-keys = Недостаточно ключей! { $keys } игроков из необходимых { $requiredKeys } являются главами. Нельзя начать Диверсионный отряд.
assaultops-no-one-ready = Нет готовых игроков! Нельзя начать Диверсионный отряд.
assaultops-role-agent = Агент

View file

@ -0,0 +1,2 @@
interrogator-verb-noun-occupant = Пациент
interrogator-verb-start-extract = Начать извлечение

View file

@ -44,6 +44,7 @@ latejoin-arrivals-direction-time = Шаттл, который доставит
latejoin-arrivals-dumped-from-shuttle = Таинственная сила не позволяет вам улететь на шаттле прибытия.
latejoin-arrivals-teleport-to-spawn = Таинственная сила телепортирует вас с шаттла прибытия. Удачной смены!
preset-not-enough-ready-players = Не удалось запустить пресет { $presetName }. Требуется { $minimumPlayers } игроков, но готовы только { $readyPlayersCount }.
preset-not-enough-ready-command-staff = Не удалось запустить пресет { $presetName }. Требуется { $minimumCommandStaff } членов командного состава, но может быть только { $readyCommandStaffCount }.
preset-no-one-ready = Не удалось запустить режим { $presetName }. Нет готовых игроков.
game-run-level-PreRoundLobby = Лобби до начала раунда
game-run-level-InRound = В раунде

View file

@ -1 +1,4 @@
examine-pinpointer-linked = Он отслеживает: { $target }
pinpointer-target-switched = Цель переключена
pinpointer-switch-target = Сменить цель

File diff suppressed because it is too large Load diff

View file

@ -79,6 +79,13 @@
Telecrystal: 8
categories:
- UplinkWeaponry
#Sunrise-start
conditions:
- !type:StoreWhitelistCondition
whitelist:
tags:
- AssaultOpsUplink
#Sunrise-end
- type: listing
id: UplinkEnergyDagger
@ -1267,6 +1274,7 @@
blacklist:
tags:
- NukeOpsUplink
- AssaultOpsUplink
- type: listing
id: UplinkReinforcementRadioSyndicateNukeops # Version for Nukeops that spawns another nuclear operative without the uplink.

View file

@ -20,11 +20,11 @@
- Guardian # no holoparasite macrobomb wombo combo
tags:
- Unimplantable
currentMode: Draw
currentMode: Inject #Sunrise-Edit
implanterSlot:
name: Implant
locked: True
priority: 0
ejectOnBreak: true #Sunrise-Edit
swap: false #Sunrise-Edit
whitelist:
tags:
- SubdermalImplant

View file

@ -1,5 +1,6 @@
- type: entity
id: BaseSubdermalImplant
parent: BaseItem
name: implant
description: A microscopic chip that's injected under the skin.
abstract: true
@ -8,7 +9,13 @@
- type: Tag
tags:
- SubdermalImplant
- HideContextMenu
# Sunrsie-Start
- type: Sprite
sprite: _Sunrise/Entities/Objects/Implants/implants.rsi
state: reviver_implant
- type: Item
size: Tiny
# Sunrsie-End
#Fun implants
@ -17,7 +24,7 @@
id: SadTromboneImplant
name: sad trombone implant
description: This implant plays a sad tune when the user dies.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
whitelist:
@ -37,7 +44,7 @@
id: LightImplant
name: light implant
description: This implant emits light from the user's skin on activation.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
implantAction: ActionToggleLight
@ -50,7 +57,6 @@
- type: Tag
tags:
- SubdermalImplant
- HideContextMenu
- Flashlight
- type: UnpoweredFlashlight
@ -59,7 +65,7 @@
id: BikeHornImplant
name: bike horn implant
description: This implant lets the user honk anywhere at any time.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
implantAction: ActionActivateHonkImplant
@ -72,6 +78,7 @@
- type: Tag
tags:
- BikeHorn
- SubdermalImplant # Sunrsie-Edit
#Security implants
@ -80,7 +87,7 @@
id: TrackingImplant
name: tracking implant
description: This implant has a tracking device attached to the suit sensor network, as well as a condition monitor for the Security radio channel.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
whitelist:
@ -109,7 +116,7 @@
# id: UplinkImplantErt
# name: uplink implant
# description: This implant lets the user access a hidden Syndicate uplink at will.
# categories: [ HideSpawnMenu ]
# categories: [ HideSpawnMenu ] Sunrsie-Edit
# components:
# - type: SubdermalImplant
# implantAction: ActionOpenUplinkImplantErt
@ -127,13 +134,14 @@
# - type: Tag
# tags:
# - NTUplink
# - SubdermalImplant
- type: entity
parent: BaseSubdermalImplant
id: TrackingImplantErt
name: tracking ERT implant
description: This implant has a tracking device attached to the suit sensor network, as well as a condition monitor for the Centcom radio channel.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
whitelist:
@ -153,7 +161,7 @@
id: StorageImplant
name: storage implant
description: This implant grants hidden storage within a person's body using bluespace technology.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
implantAction: ActionOpenStorageImplant
@ -177,7 +185,7 @@
id: FreedomImplant
name: freedom implant
description: This implant lets the user break out of hand restraints up to three times before ceasing to function anymore.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
implantAction: ActionActivateFreedomImplant
@ -190,7 +198,7 @@
id: UplinkImplant
name: uplink implant
description: This implant lets the user access a hidden Syndicate uplink at will.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
implantAction: ActionOpenUplinkImplant
@ -208,6 +216,7 @@
- type: Tag
tags:
- SyndieAgentUplink
- SubdermalImplant
#Sunrise-end
- type: entity
@ -215,7 +224,7 @@
id: EmpImplant
name: EMP implant
description: This implant creates an electromagnetic pulse when activated.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
implantAction: ActionActivateEmpImplant
@ -230,7 +239,7 @@
id: ScramImplant
name: scram implant
description: This implant randomly teleports the user within a large radius when activated.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
implantAction: ActionActivateScramImplant
@ -242,7 +251,7 @@
id: DnaScramblerImplant
name: DNA scrambler implant
description: This implant lets the user randomly change their appearance and name once.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
implantAction: ActionActivateDnaScramblerImplant
@ -257,10 +266,10 @@
id: MicroBombImplant
name: micro-bomb implant
description: This implant detonates the user upon activation or upon death.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
permanent: true
#permanent: true Sunrsie-Edit
implantAction: ActionActivateMicroBomb
- type: TriggerOnMobstateChange
mobState:
@ -278,7 +287,6 @@
- type: Tag
tags:
- SubdermalImplant
- HideContextMenu
- MicroBomb
@ -287,10 +295,10 @@
id: MacroBombImplant
name: macro-bomb implant
description: This implant creates a large explosion on death after a preprogrammed countdown.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
permanent: true
#permanent: true Sunrsie-Edit
- type: TriggerOnMobstateChange #Chains with OnUseTimerTrigger
mobState:
- Dead
@ -314,7 +322,6 @@
- type: Tag
tags:
- SubdermalImplant
- HideContextMenu
- MacroBomb
- type: entity
@ -322,10 +329,10 @@
id: DeathAcidifierImplant
name: death-acidifier implant
description: This implant melts the user and their equipment upon death.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
permanent: true
#permanent: true Sunrsie-Edit
implantAction: ActionActivateDeathAcidifier
- type: TriggerOnMobstateChange
mobState:
@ -338,7 +345,6 @@
- type: Tag
tags:
- SubdermalImplant
- HideContextMenu
- DeathAcidifier
- type: entity
@ -346,10 +352,10 @@
id: DeathRattleImplant
name: death rattle implant
description: This implant will inform the Syndicate radio channel should the user fall into critical condition or die.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
permanent: true
#permanent: true Sunrsie-Edit
whitelist:
components:
- MobState # admeme implanting a chair with rattle implant needs to give the chair mobstate so it can die first
@ -366,10 +372,11 @@
id: MindShieldImplant
name: mindshield implant
description: This implant will ensure loyalty to Nanotrasen and prevent mind control devices.
categories: [ HideSpawnMenu ]
#categories: [ HideSpawnMenu ] Sunrsie-Edit
components:
- type: SubdermalImplant
permanent: true
#permanent: true Sunrsie-Edit
- type: Tag
tags:
- MindShield
- SubdermalImplant

View file

@ -99,6 +99,7 @@
pocket1: BaseUplinkRadio0TC
inhand:
- NukeOpsDeclarationOfWar
- RemoteNukeOpShuttleController # Sunrise-Edit
#Nuclear Operative Medic Gear
- type: startingGear

View file

@ -0,0 +1,25 @@
- type: antag
id: AssaultOperative
name: roles-antag-assault-operative-name
antagonist: true
setPreference: true
objective: roles-antag-assault-operative-objective
requirements:
- !type:OverallPlaytimeRequirement
time: 7200 # 2h
- !type:DepartmentTimeRequirement
department: Security
time: 3600 # 1h
- type: antag
id: AssaultCommander
name: roles-antag-assault-commander-name
antagonist: true
setPreference: true
objective: roles-antag-assault-commander-objective
requirements:
- !type:OverallPlaytimeRequirement
time: 7200 # 2h
- !type:DepartmentTimeRequirement
department: Security
time: 7200 # 2h

View file

@ -0,0 +1,53 @@
- type: entity
id: IcarusBeam
name: icarus
categories: [ HideSpawnMenu ]
description: A beam of light from the sun.
components:
- type: Clickable
- type: MovementIgnoreGravity
- type: Sprite
sprite: _Sunrise/AssaultOperatives/sunray.rsi
drawdepth: Effects
noRot: false
netsync: false
scale: 6, 6
layers:
- state: sunray_splash
- state: sunray
offset: 0, 1
- state: sunray_muzzle
offset: 0, 2
- type: IcarusBeam
- type: AmbientSound
range: 14
sound:
path: /Audio/_Sunrise/AssaultOperatives/sunbeam_loop.ogg
- type: Physics
bodyType: Dynamic
bodyStatus: InAir
linearDamping: 0
angularDamping: 0
- type: PointLight
radius: 12
color: yellow
energy: 10.0
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeCircle
radius: 2
density: 1
hard: false
mask:
- Impassable
- BulletImpassable
layer:
- Impassable
- MidImpassable
- HighImpassable
- LowImpassable
- type: WarpPoint
follow: true
location: Icarus beam

View file

@ -0,0 +1,12 @@
- type: entity
parent: ClothingBackpackChameleon
id: ClothingBackpackAssaultOpsFill
suffix: Fill
components:
- type: StorageFill
contents:
- id: BoxSurvivalSyndicate
- id: NocturineChemistryBottle
- id: FreedomImplanter
- id: PinpointerIcarus
- id: EncryptionKeySyndie

View file

@ -0,0 +1,15 @@
- type: gamePreset
id: AssaultOps
alias:
- assaultops
- assops # yeeeeah
name: assaultops-title
description: assaultops-description
showInVote: true
rules:
- AssaultOps
- LiteSubGamemodesRule
- BasicStationEventScheduler
- MeteorSwarmScheduler
- SpaceTrafficControlEventScheduler
- BasicRoundstartVariation

View file

@ -0,0 +1,35 @@
- type: entity
categories: [ HideSpawnMenu, Spawner ]
parent: BaseAntagSpawner
id: SpawnPointAssaultOpsCommander
components:
- type: GhostRole
name: Командир диверсионного отряда
description: Вы являетесь оперативником синдиката, назначенным на уничтожение станции. Вашей задачей как антагониста является выполнение всего необходимого для достижения этой цели.
rules: ghost-role-information-rules-default-team-antagonist
raffle:
settings: default
- type: Sprite
sprite: Markers/jobs.rsi
layers:
- state: green
- sprite: _Sunrise/Interface/Misc/antag_preview.rsi
state: test
- type: entity
categories: [ HideSpawnMenu, Spawner ]
parent: BaseAntagSpawner
id: SpawnPointAssaultOpsOperative
components:
- type: GhostRole
name: Оперативник диверсионного отряда
description: Вы являетесь оперативником синдиката, назначенным на уничтожение станции. Вашей задачей как антагониста является выполнение всего необходимого для достижения этой цели.
rules: ghost-role-information-rules-default-team-antagonist
raffle:
settings: default
- type: Sprite
sprite: Markers/jobs.rsi
layers:
- state: green
- sprite: _Sunrise/Interface/Misc/antag_preview.rsi
state: test

View file

@ -0,0 +1,9 @@
# Assault Operative
- type: entity
categories: [ HideSpawnMenu ]
name: Assault Operative
parent: MobHuman
id: MobHumanAssaultOp
components:
- type: AssaultOperative
- type: RandomHumanoidAppearance

View file

@ -0,0 +1 @@


View file

@ -0,0 +1,7 @@
- type: entity
id: IcarusKeyImplanter
name: имплантер ключа Икарус
parent: BaseImplantOnlyImplanter
components:
- type: Implanter
implant: IcarusKey

View file

@ -0,0 +1,10 @@
- type: entity
parent: ClothingUniformBase
id: ClothingUniformJumpsuitTactical
name: tactical turtleneck suit
description: A double seamed tactical turtleneck disguised as a civilian grade silk suit. Intended for the most formal operator. The collar is really sharp.
components:
- type: Sprite
sprite: _Sunrise/AssaultOperatives/tactical_suit.rsi
- type: Clothing
sprite: _Sunrise/AssaultOperatives/tactical_suit.rsi

View file

@ -0,0 +1,20 @@
- type: entity
parent: BaseItem
id: IcarusKey
name: icarus authentication keycard
description: A high profile authentication keycard to Nanotrasen's Icarus secured network.
components:
- type: Sprite
sprite: _Sunrise/AssaultOperatives/goldeneye_key.rsi
layers:
- state: key
- type: IcarusKey
- type: Item
size: Tiny
- type: WarpPoint
follow: true
location: Icarus key
- type: SubdermalImplant
- type: Tag
tags:
- SubdermalImplant

View file

@ -0,0 +1,13 @@
- type: entity
parent: ClothingMaskPullableBase
id: ClothingMaskGaiter
name: neck gaiter
description: For the agent wanting to keep a low profile whilst concealing their identity. Has a small respirator to be used with internals.
components:
- type: Sprite
sprite: _Sunrise/AssaultOperatives/gaiter.rsi
- type: Clothing
sprite: _Sunrise/AssaultOperatives/gaiter.rsi
- type: BreathMask
- type: IngestionBlocker
- type: IdentityBlocker

View file

@ -0,0 +1,17 @@
- type: entity
parent: BaseMindRoleAntag
id: MindRoleAssaultOperative
name: Assault Operative Role
components:
- type: MindRole
exclusiveAntag: true
antagPrototype: AssaultOperative
- type: AssaultOpsRole
- type: entity
parent: MindRoleNukeops
id: MindRoleAssaultCommander
name: Assault Commander Role
components:
- type: MindRole
antagPrototype: AssaultCommander

View file

@ -0,0 +1,11 @@
- type: entity
name: цель
parent: Paper
id: DocumentAssaultOpsGoal
components:
- type: Paper
content: assault-ops-document-goal
stampState: paper_stamp-syndicate
stampedBy:
- stampedColor: '#850000FF'
stampedName: stamp-component-stamped-name-syndicate

View file

@ -0,0 +1,10 @@
- type: entity
parent: PassengerPDA
id: AssaultOpsPDA
name: passenger PDA
description: Why isn't it gray?
components:
- type: Pda
id: AgentIDCard
pen: Hypopen
state: pda

View file

@ -0,0 +1,16 @@
- type: entity
name: pinpointer
id: PinpointerIcarus
parent: PinpointerBase
components:
- type: Pinpointer
component: IcarusKey
- type: Sprite
sprite: _Sunrise/AssaultOperatives/pinpointer.rsi
layers:
- state: pinpointer_icarus
map: ["enum.PinpointerLayers.Base"]
- state: pinonnull
map: ["enum.PinpointerLayers.Screen"]
shader: unshaded
visible: false

View file

@ -0,0 +1,65 @@
- type: entity
id: AssaultOps
parent: BaseGameRule
components:
- type: GameRule
minPlayers: 20
minCommandStaff: 3
- type: AssaultOpsRule
faction: Syndicate
- type: LoadMapRule
mapPath: /Maps/_Sunrise/Shuttles/assaultops.yml
- type: RuleGrids
- type: RandomMetadata
nameSegments:
- names_first
- names_last
- type: AntagLoadProfileRule
speciesOverride: Human
speciesOverrideBlacklist:
- Vox
- Felinid
- Diona
- HumanoidXeno
- Predator
- type: AntagSelection
selectionTime: PrePlayerSpawn
definitions:
- prefRoles: [ AssaultCommander ]
fallbackRoles: [ AssaultOperative ]
spawnerPrototype: SpawnPointAssaultOpsCommander
startingGear: AssaultCommanderGear
roleLoadout:
- RoleSurvivalAssaultOps
components:
- type: AssaultOperative
# SUNRISE-TODO: Рандомное имя подходящее под под и расу
- type: RandomMetadata
nameSegments:
- names_first
- names_last
- type: NpcFactionMember
factions:
- Syndicate
mindRoles:
- MindRoleAssaultCommander
- prefRoles: [ AssaultOperative ]
fallbackRoles: [ AssaultCommander ]
spawnerPrototype: SpawnPointAssaultOpsOperative
max: 5
playerRatio: 15
startingGear: AssaultOperativeGear
roleLoadout:
- RoleSurvivalAssaultOps
components:
- type: AssaultOperative
# SUNRISE-TODO: Рандомное имя подходящее под под и расу
- type: RandomMetadata
nameSegments:
- names_first
- names_last
- type: NpcFactionMember
factions:
- Syndicate
mindRoles:
- MindRoleAssaultOperative

View file

@ -0,0 +1,11 @@
- type: entity
id: SpawnPointAssaultOps
parent: MarkerBase
name: assaultops
components:
- type: SpawnPoint
- type: Sprite
layers:
- state: green
- sprite: Objects/Fun/toys.rsi
state: base

View file

@ -0,0 +1,41 @@
- type: startingGear
id: AssaultOperativeGear
equipment:
jumpsuit: ClothingUniformJumpsuitTactical
back: ClothingBackpackAssaultOpsFill
mask: ClothingMaskGaiter
eyes: ClothingEyesGlassesSunglasses
shoes: ClothingShoesColorBlack
id: AssaultOpsPDA
# SUNRISE-TODO: Лоадаут для ДОшников
#innerclothingskirt: ClothingUniformJumpsuitTactical
- type: startingGear
id: LoneAssaultOperativeGear
equipment:
jumpsuit: ClothingUniformJumpsuitTactical
back: ClothingBackpackAssaultOpsFill
mask: ClothingMaskGaiter
eyes: ClothingEyesGlassesSunglasses
shoes: ClothingShoesColorBlack
id: AssaultOpsPDA
pocket2: UplinkRadioAssaultOperatives40TC
# SUNRISE-TODO: Лоадаут для ДОшников
#innerclothingskirt: ClothingUniformJumpsuitTactical
- type: startingGear
id: AssaultCommanderGear
equipment:
jumpsuit: ClothingUniformJumpsuitTactical
back: ClothingBackpackAssaultOpsFill
mask: ClothingMaskGaiter
eyes: ClothingEyesGlassesSunglasses
shoes: ClothingShoesColorBlack
id: AssaultOpsPDA
pocket1: RemoteAssaultOpsShuttleController
pocket2: UplinkRadioAssaultOperatives0TC
# SUNRISE-TODO: Лоадаут для ДОшников
#innerclothingskirt: ClothingUniformJumpsuitTactical
inhand:
- FlippoLighter
- DocumentAssaultOpsGoal

View file

@ -0,0 +1,2 @@
- type: Tag
id: AssaultOpsUplink

View file

@ -0,0 +1,70 @@
- type: entity
parent: BaseStructureComputer
id: ComputerIcarus
name: icarus terminal
description: An ominous terminal with some ports and keypads, the screen is scrolling with illegible nonsense. It has a strange marking on the side, a red ring with a gold circle within.
placement:
mode: SnapgridCenter
components:
- type: Sprite
sprite: _Sunrise/AssaultOperatives/goldeneye_terminal.rsi
state: terminal
- type: Computer
- type: ApcPowerReceiver
powerLoad: 200
- type: LitOnPowered
- type: ExtensionCableReceiver
- type: ActivatableUIRequiresPower
- type: LightningTarget
priority: 1
- type: RequireProjectileTarget
- type: Electrified
enabled: false
usesApcPower: true
- type: PointLight
radius: 1.5
energy: 1.6
enabled: false
mask: /Textures/Effects/LightMasks/cone.png
autoRot: true
color: "#b5a13c"
offset: "0, 0.4" # shine from the top, not bottom of the computer
castShadows: false
- type: ItemSlots
slots:
firstKeySlot:
ejectSound: /Audio/Machines/id_swipe.ogg
insertSound: /Audio/Machines/Nuke/general_beep.ogg
ejectOnBreak: true
swap: false
whitelist:
components:
- IcarusKey
secondKeySlot:
ejectSound: /Audio/Machines/id_swipe.ogg
insertSound: /Audio/Machines/Nuke/general_beep.ogg
ejectOnBreak: true
swap: false
whitelist:
components:
- IcarusKey
thirdKeySlot:
ejectSound: /Audio/Machines/id_swipe.ogg
insertSound: /Audio/Machines/Nuke/general_beep.ogg
ejectOnBreak: true
swap: false
whitelist:
components:
- IcarusKey
- type: ContainerContainer
containers:
firstKeySlot: !type:ContainerSlot
secondKeySlot: !type:ContainerSlot
thirdKeySlot: !type:ContainerSlot
- type: IcarusTerminal
- type: UserInterface
interfaces:
enum.IcarusTerminalUiKey.Key:
type: IcarusTerminalBoundUserInterface
- type: ActivatableUI
key: enum.IcarusTerminalUiKey.Key

View file

@ -0,0 +1,24 @@
- type: entity
parent: BaseUplinkRadio
id: UplinkRadioAssaultOperatives0TC
suffix: AssaultOps
components:
- type: Store
balance:
Telecrystal: 0
- type: Tag
tags:
- AssaultOpsUplink
- type: entity
parent: BaseUplinkRadio
id: UplinkRadioAssaultOperatives40TC
suffix: AssaultOps
components:
- type: Store
balance:
Telecrystal: 40
- type: Tag
tags:
- AssaultOpsUplink

View file

@ -0,0 +1,87 @@
- type: entity
id: RemoteShuttleControllerBase
parent: BaseItem
abstract: true
components:
- type: Sprite
sprite: _Sunrise/Entities/Objects/Devices/remote_controller.rsi
- type: Item
size: Normal
- type: ApcPowerReceiver
needsPower: false
powerLoad: 0
- type: ShuttleConsole
portable: true
- type: RadarConsole
maxRange: 256
- type: ActivatableUI
key: enum.ShuttleConsoleUiKey.Key
- type: UserInterface
interfaces:
enum.ShuttleConsoleUiKey.Key:
type: ShuttleConsoleBoundUserInterface
- type: entity
id: RemoteAssaultOpsShuttleController
parent: RemoteShuttleControllerBase
name: Удаленный контроллер управления шаттлом (Диверсионный отряд)
description: Теперь вам не нужен пилот, ведь пилот это вы!
components:
- type: Sprite
state: syndicate
- type: DroneConsole
portable: true
components:
- type: AssaultOpsShuttle
- type: entity
id: RemoteNukeOpShuttleController
parent: RemoteShuttleControllerBase
name: Удаленный контроллер управления шаттлом (Ядерные оперативки)
description: Теперь вам не нужен пилот, ведь пилот это вы!
components:
- type: Sprite
state: syndicate
- type: DroneConsole
portable: true
components:
- type: NukeOpsShuttle
- type: entity
id: RemoteSecurityShuttleController
parent: RemoteShuttleControllerBase
name: Удаленный контроллер управления шаттлом (Шаттл службы безопастности)
description: Теперь вам не нужен пилот, ведь пилот это вы!
components:
- type: Sprite
state: nanotrasen
- type: DroneConsole
portable: true
components:
- type: SecurityShuttle
- type: entity
id: RemoteCargoShuttleController
parent: RemoteShuttleControllerBase
name: Удаленный контроллер управления шаттлом (Шаттл карго)
description: Теперь вам не нужен пилот, ведь пилот это вы!
components:
- type: Sprite
state: nanotrasen
- type: DroneConsole
portable: true
components:
- type: CargoShuttle
- type: entity
id: RemotePrisonShuttleController
parent: RemoteShuttleControllerBase
name: Удаленный контроллер управления шаттлом (Тюремный шаттл)
description: Теперь вам не нужен пилот, ведь пилот это вы!
components:
- type: Sprite
state: nanotrasen
- type: DroneConsole
portable: true
components:
- type: PrisonShuttle

View file

@ -80,6 +80,14 @@
- type: AccessReader
access: [["SyndicateAgent"]]
- type: entity
parent: AirlockMaint
id: AirlockMaintSyndicateLocked
suffix: Syndicate, Locked
components:
- type: AccessReader
access: [["SyndicateAgent"]]
#DoubleAirlocks
- type: entity

View file

@ -336,3 +336,10 @@
- Bra
- Pants
- Socks
- type: roleLoadout
id: RoleSurvivalAssaultOps
groups:
- SurvivalSyndicate
- GroupSpeciesBreathTool
- GroupPocketTankDouble

View file

@ -0,0 +1,77 @@
- type: entity
id: Interrogator
parent: BaseMachinePowered
name: interrogator
description: apchy
components:
- type: Interrogator
- type: Sprite
sprite: _Sunrise/AssaultOperatives/interrogator.rsi
snapCardinals: true
layers:
- state: base
map: [ "enum.InterrogatorVisualLayers.Base" ]
- state: extraction-on
map: [ "enum.InterrogatorVisualLayers.Extract" ]
visible: false
- type: PointLight
color: "#3a807f"
radius: 2
energy: 10
enabled: false
- type: Physics
bodyType: Static
- type: Construction
graph: Machine
node: machine
containers:
- machine_board
- machine_parts
- body_container
- type: EmptyOnMachineDeconstruct
containers:
- body_container
- type: Damageable
damageContainer: StructuralInorganic
damageModifierSet: StrongMetallic
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:PlaySoundBehavior
sound:
collection: MetalGlassBreak
- !type:ChangeConstructionNodeBehavior
node: machineFrame
- !type:DoActsBehavior
acts: ["Destruction"]
- type: Machine
board: InterrogatorMachineCircuitboard
- type: WiresPanel
- type: ApcPowerReceiver
powerLoad: 200
- type: Appearance
- type: ContainerContainer
containers:
body_container: !type:ContainerSlot
machine_board: !type:Container
machine_parts: !type:Container
- type: entity
id: InterrogatorMachineCircuitboard
parent: BaseMachineCircuitboard
name: interrogator machine board
description: A machine printed circuit board for a interrogator.
components:
- type: Sprite
state: medical
- type: MachineBoard
prototype: Interrogator
stackRequirements:
MatterBin: 2
Manipulator: 2
Glass: 1
Cable: 1

View file

@ -0,0 +1,33 @@
- type: entity
id: ThrusterSyndicate
parent: [ BaseThruster, ConstructibleMachine ]
components:
- type: Thruster
baseThrust: 150
- type: Machine
board: ThrusterSyndicateMachineCircuitboard
- type: Sprite
sprite: _Sunrise/Structures/Shuttles/syndicate_thruster.rsi
layers:
- state: base
map: ["enum.ThrusterVisualLayers.Base"]
- state: red_thrust
map: ["enum.ThrusterVisualLayers.ThrustOn"]
shader: unshaded
visible: false
- state: red_thrust_burn_unshaded
map: ["enum.ThrusterVisualLayers.ThrustingUnshaded"]
shader: unshaded
visible: false
offset: 0, 1
- type: entity
id: ThrusterSyndicateMachineCircuitboard
parent: BaseMachineCircuitboard
name: thruster syndie machine board
components:
- type: MachineBoard
prototype: ThrusterSyndicate
stackRequirements:
Capacitor: 4
Steel: 5

View file

@ -31,9 +31,6 @@
- type: Tag
id: BorgModuleERT
- type: Tag
id: AssaultOpsUplink
- type: Tag
id: SyndieAgentUplink

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

View file

@ -0,0 +1,30 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from Skyrat-tg at commit https://github.com/Skyrat-SS13/Skyrat-tg/commit/fe1f0bd9bb30996d09f17dacbd8b72dee7a4dfd7",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "equipped-MASK",
"directions": 4
},
{
"name": "up-equipped-MASK",
"directions": 4
},
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

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