Compare commits
12 Commits
master
...
assaultops
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11c5ecf880 | ||
|
|
9c6a393617 | ||
|
|
eba81123e7 | ||
|
|
d6afbb30bb | ||
|
|
8434c9cd9f | ||
|
|
335ad04647 | ||
|
|
1521dff5e3 | ||
|
|
b6fc63a61d | ||
|
|
bd18265088 | ||
|
|
da7170e600 | ||
|
|
e9b053c856 | ||
|
|
d5afa731cf |
@@ -0,0 +1,56 @@
|
||||
using Content.Shared.Corvax.Icarus;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Corvax.Icarus;
|
||||
|
||||
public sealed class IcarusTerminalBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private IcarusTerminalWindow? _window;
|
||||
|
||||
public IcarusTerminalBoundUserInterface(ClientUserInterfaceComponent 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();
|
||||
}
|
||||
}
|
||||
21
Content.Client/Corvax/Icarus/IcarusTerminalWindow.xaml
Normal 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>
|
||||
36
Content.Client/Corvax/Icarus/IcarusTerminalWindow.xaml.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Content.Shared.Corvax.Icarus;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Corvax.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Content.Server.Corvax.AssaultOps;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for tagging a mob as a assault operative.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class AssaultOperativeComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using Content.Server.GameTicking.Rules.Configurations;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Shared.Audio;
|
||||
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.Corvax.AssaultOps;
|
||||
|
||||
public sealed class AssaultopsRuleConfiguration : GameRuleConfiguration
|
||||
{
|
||||
public override string Id => "Assaultops";
|
||||
|
||||
[DataField("minPlayers")]
|
||||
public int MinPlayers = 20;
|
||||
|
||||
/// <summary>
|
||||
/// This INCLUDES the operatives. So a value of 3 is satisfied by 2 players & 1 operative
|
||||
/// </summary>
|
||||
[DataField("playersPerOperative")]
|
||||
public int PlayersPerOperative = 5;
|
||||
|
||||
[DataField("maxOps")]
|
||||
public int MaxOperatives = 5;
|
||||
|
||||
[DataField("requiredKeys")]
|
||||
public int RequiredKeys = 3;
|
||||
|
||||
[DataField("keysCarrierJobs", customTypeSerializer: typeof(PrototypeIdArraySerializer<JobPrototype>))]
|
||||
public string[] KeysCarrierJobs = { "Captain", "HeadOfSecurity", "ChiefEngineer", "ChiefMedicalOfficer", "ResearchDirector", "Quartermaster" };
|
||||
|
||||
[DataField("randomHumanoidSettings", customTypeSerializer: typeof(PrototypeIdSerializer<RandomHumanoidSettingsPrototype>))]
|
||||
public string RandomHumanoidSettingsPrototype = "AssaultOp";
|
||||
|
||||
[DataField("spawnPointProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
public string SpawnPointPrototype = "SpawnPointAssaultops";
|
||||
|
||||
[DataField("operativeRoleProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
public string OperativeRoleProto = "Assaultops";
|
||||
|
||||
[DataField("operativeStartGearProto", customTypeSerializer: typeof(PrototypeIdSerializer<StartingGearPrototype>))]
|
||||
public string OperativeStartGearPrototype = "AssaultOperativeGear";
|
||||
|
||||
[DataField("normalNames", customTypeSerializer: typeof(PrototypeIdSerializer<DatasetPrototype>))]
|
||||
public string OperativeNames = "SyndicateNamesNormal";
|
||||
|
||||
[DataField("outpostMap", customTypeSerializer: typeof(ResourcePathSerializer))]
|
||||
public ResourcePath? OutpostMap = new("/Maps/corvax_assaultopsplanet.yml");
|
||||
|
||||
[DataField("shuttleMap", customTypeSerializer: typeof(ResourcePathSerializer))]
|
||||
public ResourcePath? ShuttleMap = new("/Maps/infiltrator.yml"); // TODO: Create custom shuttle
|
||||
|
||||
[DataField("greetingSound", customTypeSerializer: typeof(SoundSpecifierTypeSerializer))]
|
||||
public SoundSpecifier? GreetSound = new SoundPathSpecifier("/Audio/Corvax/Misc/assaultops.ogg");
|
||||
}
|
||||
448
Content.Server/Corvax/AssaultOps/AssaultopsRuleSystem.cs
Normal file
@@ -0,0 +1,448 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.Humanoid.Systems;
|
||||
using Content.Server.Mind.Components;
|
||||
using Content.Server.NPC.Systems;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Server.Traitor;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.MobState;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Maps;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Corvax.AssaultOps;
|
||||
|
||||
public sealed class AssaultopsRuleSystem : GameRuleSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly RandomHumanoidSystem _randomHumanoid = default!;
|
||||
[Dependency] private readonly StationSpawningSystem _stationSpawningSystem = default!;
|
||||
[Dependency] private readonly ShuttleSystem _shuttleSystem = default!;
|
||||
[Dependency] private readonly FactionSystem _faction = default!;
|
||||
[Dependency] private readonly MapLoaderSystem _mapLoader = default!;
|
||||
|
||||
private enum WinType
|
||||
{
|
||||
/// <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
|
||||
}
|
||||
|
||||
private enum WinCondition
|
||||
{
|
||||
IcarusActivated,
|
||||
AllOpsDead,
|
||||
SomeOpsAlive,
|
||||
AllOpsAlive
|
||||
}
|
||||
|
||||
private WinType _winType = WinType.Stalemate;
|
||||
private readonly List<WinCondition> _winConditions = new();
|
||||
|
||||
private MapId? _outpostMap;
|
||||
|
||||
// TODO: use components, don't just cache entity UIDs
|
||||
// There have been (and probably still are) bugs where these refer to deleted entities from old rounds.
|
||||
private EntityUid? _outpostGrid;
|
||||
private EntityUid? _shuttleGrid;
|
||||
private EntityUid? _targetStation;
|
||||
|
||||
public override string Prototype => "Assaultops";
|
||||
|
||||
private AssaultopsRuleConfiguration _ruleConfig = new();
|
||||
|
||||
/// <summary>
|
||||
/// Cached operator name prototypes.
|
||||
/// </summary>
|
||||
private readonly List<string> _operativeNames = new();
|
||||
|
||||
/// <summary>
|
||||
/// Players who played as an operative at some point in the round.
|
||||
/// Stores the session as well as the entity name
|
||||
/// </summary>
|
||||
private readonly Dictionary<string, IPlayerSession> _operativePlayers = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
|
||||
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayersSpawning);
|
||||
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnRunLevelChanged);
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEndText);
|
||||
SubscribeLocalEvent<AssaultOperativeComponent, ComponentInit>(OnComponentInit);
|
||||
}
|
||||
|
||||
private void OnStartAttempt(RoundStartAttemptEvent ev)
|
||||
{
|
||||
if (!RuleAdded || Configuration is not AssaultopsRuleConfiguration assaultOpsConfig)
|
||||
return;
|
||||
|
||||
_ruleConfig = assaultOpsConfig;
|
||||
var minPlayers = assaultOpsConfig.MinPlayers;
|
||||
if (!ev.Forced && ev.Players.Length < minPlayers)
|
||||
{
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("assaultops-not-enough-ready-players", ("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
|
||||
ev.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ev.Players.Length != 0)
|
||||
return;
|
||||
|
||||
_chatManager.DispatchServerAnnouncement(Loc.GetString("assaultops-no-one-ready"));
|
||||
ev.Cancel();
|
||||
}
|
||||
|
||||
private void OnPlayersSpawning(RulePlayerSpawningEvent ev)
|
||||
{
|
||||
if (!RuleAdded)
|
||||
return;
|
||||
|
||||
// Basically copied verbatim from traitor code
|
||||
var playersPerOperative = _ruleConfig.PlayersPerOperative;
|
||||
var maxOperatives = _ruleConfig.MaxOperatives;
|
||||
var numOps = MathHelper.Clamp(ev.PlayerPool.Count / playersPerOperative, 1, maxOperatives);
|
||||
|
||||
var opsPool = FindPotentialOperatives(ev);
|
||||
var selectedOps = PickOperatives(numOps, opsPool);
|
||||
|
||||
SpawnOperatives(selectedOps);
|
||||
|
||||
foreach (var session in selectedOps)
|
||||
{
|
||||
ev.PlayerPool.Remove(session);
|
||||
GameTicker.PlayerJoinGame(session);
|
||||
var name = session.AttachedEntity == null
|
||||
? string.Empty
|
||||
: MetaData(session.AttachedEntity.Value).EntityName;
|
||||
// TODO: Fix this being able to have duplicates
|
||||
_operativePlayers[name] = session;
|
||||
}
|
||||
}
|
||||
|
||||
private List<IPlayerSession> FindPotentialOperatives(RulePlayerSpawningEvent ev)
|
||||
{
|
||||
var eligible = new List<IPlayerSession>(ev.PlayerPool).Where(p =>
|
||||
ev.Profiles.TryGetValue(p.UserId, out var profile) &&
|
||||
profile.AntagPreferences.Contains(_ruleConfig.OperativeRoleProto)
|
||||
).ToList();
|
||||
|
||||
if (eligible.Count == 0)
|
||||
{
|
||||
Logger.InfoS("preset", "Insufficient preferred assaultops, create pool from everyone.");
|
||||
return ev.PlayerPool;
|
||||
}
|
||||
|
||||
return eligible;
|
||||
}
|
||||
|
||||
private List<IPlayerSession> PickOperatives(int count, List<IPlayerSession> pool)
|
||||
{
|
||||
var selected = new List<IPlayerSession>(count);
|
||||
if (pool.Count == 0)
|
||||
{
|
||||
Logger.InfoS("preset", "Insufficient ready players to fill up with assaultops, stopping the selection");
|
||||
return selected;
|
||||
}
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
selected.Add(_random.PickAndTake(pool));
|
||||
Logger.InfoS("preset", "Selected a assaultop.");
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
private void SpawnOperatives(List<IPlayerSession> operatives)
|
||||
{
|
||||
if (_outpostGrid == null)
|
||||
return;
|
||||
|
||||
var spawnpoints = GetSpawnpoints(_outpostGrid.Value);
|
||||
foreach (var session in operatives)
|
||||
{
|
||||
var spawnpoint = _random.Pick(spawnpoints);
|
||||
SpawnOperative(session, spawnpoint);
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnOperative(IPlayerSession session, EntityCoordinates spawnpoint)
|
||||
{
|
||||
var name = Loc.GetString("nukeops-role-operator") + " " +
|
||||
_random.PickAndTake(_operativeNames);
|
||||
var mob = _randomHumanoid.SpawnRandomHumanoid(_ruleConfig.RandomHumanoidSettingsPrototype, spawnpoint, name);
|
||||
var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
|
||||
|
||||
// EntityManager.EnsureComponent<RandomHumanoidAppearanceComponent>(mob);
|
||||
EnsureComp<AssaultOperativeComponent>(mob);
|
||||
|
||||
var gearProto = _prototypeManager.Index<StartingGearPrototype>(_ruleConfig.OperativeStartGearPrototype);
|
||||
_stationSpawningSystem.EquipStartingGear(mob, gearProto, profile);
|
||||
|
||||
_faction.RemoveFaction(mob, "NanoTrasen", false);
|
||||
_faction.AddFaction(mob, "Syndicate");
|
||||
|
||||
var newMind = new Mind.Mind(session.UserId) { CharacterName = name };
|
||||
newMind.ChangeOwningPlayer(session.UserId);
|
||||
|
||||
var antagProto = _prototypeManager.Index<AntagPrototype>(_ruleConfig.OperativeRoleProto);
|
||||
newMind.AddRole(new TraitorRole(newMind, antagProto));
|
||||
|
||||
newMind.TransferTo(mob);
|
||||
}
|
||||
|
||||
private List<EntityCoordinates> GetSpawnpoints(EntityUid outpostUid)
|
||||
{
|
||||
var spawns = new List<EntityCoordinates>();
|
||||
|
||||
// Forgive sloth for hardcoding prototypes
|
||||
foreach (var (_, meta, xform) in EntityManager.EntityQuery<SpawnPointComponent, MetaDataComponent, TransformComponent>(true))
|
||||
{
|
||||
if (meta.EntityPrototype?.ID != _ruleConfig.SpawnPointPrototype)
|
||||
continue;
|
||||
|
||||
if (xform.ParentUid != _outpostGrid)
|
||||
continue;
|
||||
|
||||
spawns.Add(xform.Coordinates);
|
||||
break;
|
||||
}
|
||||
|
||||
if (spawns.Count == 0)
|
||||
{
|
||||
spawns.Add(EntityManager.GetComponent<TransformComponent>(outpostUid).Coordinates);
|
||||
Logger.WarningS("assaultops", $"Fell back to default spawn for assaultops!");
|
||||
}
|
||||
|
||||
return spawns;
|
||||
}
|
||||
|
||||
private void OnComponentInit(EntityUid uid, AssaultOperativeComponent component, ComponentInit args)
|
||||
{
|
||||
// If entity has a prior mind attached, add them to the players list.
|
||||
if (!TryComp<MindComponent>(uid, out var mindComponent) || !RuleAdded)
|
||||
return;
|
||||
|
||||
var session = mindComponent.Mind?.Session;
|
||||
var name = MetaData(uid).EntityName;
|
||||
if (session != null)
|
||||
_operativePlayers.Add(name, session);
|
||||
}
|
||||
|
||||
private void OnRunLevelChanged(GameRunLevelChangedEvent ev)
|
||||
{
|
||||
switch (ev.New)
|
||||
{
|
||||
case GameRunLevel.InRound:
|
||||
OnRoundStart();
|
||||
break;
|
||||
case GameRunLevel.PostRound:
|
||||
OnRoundEnd();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundStart()
|
||||
{
|
||||
// TODO: This needs to try and target a Nanotrasen station. At the very least,
|
||||
// we can only currently guarantee that NT stations are the only station to
|
||||
// exist in the base game.
|
||||
|
||||
_targetStation = _stationSystem.Stations.FirstOrNull();
|
||||
if (_targetStation == null)
|
||||
return;
|
||||
|
||||
var filter = Filter.Empty();
|
||||
foreach (var op in EntityQuery<AssaultOperativeComponent>())
|
||||
{
|
||||
if (!TryComp<ActorComponent>(op.Owner, out var actor))
|
||||
continue;
|
||||
|
||||
_chatManager.DispatchServerMessage(actor.PlayerSession, Loc.GetString("assaultops-welcome", ("station", _targetStation.Value)));
|
||||
filter.AddPlayer(actor.PlayerSession);
|
||||
}
|
||||
|
||||
_audioSystem.PlayGlobal(_ruleConfig.GreetSound, filter, recordReplay: false);
|
||||
}
|
||||
|
||||
private void OnRoundEnd()
|
||||
{
|
||||
var total = 0;
|
||||
var alive = 0;
|
||||
foreach (var (_, state) in EntityQuery<AssaultOperativeComponent, MobStateComponent>())
|
||||
{
|
||||
total++;
|
||||
if (state.CurrentState is DamageState.Alive)
|
||||
continue;
|
||||
|
||||
alive++;
|
||||
break;
|
||||
}
|
||||
|
||||
var allAlive = alive == total;
|
||||
if (allAlive)
|
||||
{
|
||||
_winType = WinType.OpsMinor;
|
||||
_winConditions.Add(WinCondition.AllOpsAlive);
|
||||
}
|
||||
else if (alive == 0)
|
||||
{
|
||||
_winConditions.Add(WinCondition.AllOpsDead);
|
||||
}
|
||||
else
|
||||
{
|
||||
_winConditions.Add(WinCondition.SomeOpsAlive);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundEndText(RoundEndTextAppendEvent ev)
|
||||
{
|
||||
if (!RuleAdded)
|
||||
return;
|
||||
|
||||
ev.AddLine(Loc.GetString($"assaultops-{_winType.ToString().ToLower()}"));
|
||||
|
||||
foreach (var cond in _winConditions)
|
||||
ev.AddLine(Loc.GetString($"assaultops-cond-{cond.ToString().ToLower()}"));
|
||||
|
||||
ev.AddLine(Loc.GetString("assaultops-list-start"));
|
||||
foreach (var (name, session) in _operativePlayers)
|
||||
{
|
||||
var listing = Loc.GetString("assaultops-list-name", ("name", name), ("user", session.Name));
|
||||
ev.AddLine(listing);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Started()
|
||||
{
|
||||
_winType = WinType.Stalemate;
|
||||
_winConditions.Clear();
|
||||
_outpostGrid = null;
|
||||
|
||||
_operativePlayers.Clear();
|
||||
_operativeNames.Clear();
|
||||
|
||||
_operativeNames.AddRange(_prototypeManager.Index<DatasetPrototype>(_ruleConfig.OperativeNames).Values);
|
||||
|
||||
if (!SpawnMap())
|
||||
{
|
||||
Logger.InfoS("assaultops", "Failed to load map for assaultops");
|
||||
return;
|
||||
}
|
||||
|
||||
LoadExistOperatives();
|
||||
}
|
||||
|
||||
private bool SpawnMap()
|
||||
{
|
||||
if (_outpostMap != null)
|
||||
return true; // Map is already loaded
|
||||
|
||||
var outpostMap = _ruleConfig.OutpostMap;
|
||||
if (outpostMap == null)
|
||||
{
|
||||
Logger.ErrorS("assaultops", "No station map specified for assaultops!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var shuttlePath = _ruleConfig.ShuttleMap;
|
||||
if (shuttlePath == null)
|
||||
{
|
||||
Logger.ErrorS("assaultops", "No shuttle map specified for assaultops!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var mapId = _mapManager.CreateMap();
|
||||
var options = new MapLoadOptions() { LoadMap = true };
|
||||
|
||||
if (!_mapLoader.TryLoad(mapId, outpostMap.ToString(), out var outpostGrids, options) || outpostGrids.Count == 0)
|
||||
{
|
||||
Logger.ErrorS("assaultops", $"Error loading map {outpostMap} for assaultops!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Assume the first grid is the outpost grid.
|
||||
_outpostGrid = outpostGrids[0];
|
||||
|
||||
// Listen I just don't want it to overlap.
|
||||
if (!_mapLoader.TryLoad(mapId, shuttlePath.ToString(), out var grids, new MapLoadOptions {Offset = Vector2.One*1000f}) || !grids.Any())
|
||||
{
|
||||
Logger.ErrorS("assaultops", $"Error loading grid {shuttlePath} for assaultops!");
|
||||
return false;
|
||||
}
|
||||
|
||||
var shuttleId = grids.First();
|
||||
|
||||
// Naughty, someone saved the shuttle as a map.
|
||||
if (Deleted(shuttleId))
|
||||
{
|
||||
Logger.ErrorS("assaultops", $"Tried to load assaultops shuttle as a map, aborting.");
|
||||
_mapManager.DeleteMap(mapId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (TryComp<ShuttleComponent>(shuttleId, out var shuttle))
|
||||
{
|
||||
_shuttleSystem.TryFTLDock(shuttle, _outpostGrid.Value);
|
||||
}
|
||||
|
||||
_outpostMap = mapId;
|
||||
_shuttleGrid = shuttleId;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void LoadExistOperatives()
|
||||
{
|
||||
// Add pre-existing nuke operatives to the credit list.
|
||||
var query = EntityQuery<AssaultOperativeComponent, MindComponent>(true);
|
||||
foreach (var (_, mindComp) in query)
|
||||
{
|
||||
if (mindComp.Mind == null || !mindComp.Mind.TryGetSession(out var session))
|
||||
continue;
|
||||
var name = MetaData(mindComp.Owner).EntityName;
|
||||
_operativePlayers.Add(name, session);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Ended() { }
|
||||
}
|
||||
50
Content.Server/Corvax/Icarus/Commands/SpawnIcarusCommand.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Content.Server.Administration;
|
||||
using Content.Shared.Administration;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Corvax.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 mapManager = IoCManager.Resolve<IMapManager>();
|
||||
if (mapManager.TryGetGrid(uid, out var grid))
|
||||
{
|
||||
var icarusSystem = IoCManager.Resolve<IEntityManager>().System<IcarusTerminalSystem>();
|
||||
var coords = icarusSystem.FireBeam(grid.LocalAABB);
|
||||
shell.WriteLine($"Icarus was spawned: {coords.ToString()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.WriteError($"No grid exists with ID {uid}");
|
||||
}
|
||||
}
|
||||
}
|
||||
31
Content.Server/Corvax/Icarus/IcarusBeamComponent.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace Content.Server.Corvax.Icarus;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed class IcarusBeamComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Beam moving speed.
|
||||
/// </summary>
|
||||
[DataField("speed")]
|
||||
public float Speed = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// The beam will be automatically cleaned up after this time.
|
||||
/// </summary>
|
||||
[DataField("lifetime")]
|
||||
public TimeSpan Lifetime = TimeSpan.FromSeconds(240);
|
||||
|
||||
/// <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 = 2f;
|
||||
|
||||
[DataField("flameRadius")]
|
||||
public float FlameRadius = 4f;
|
||||
|
||||
public TimeSpan LifetimeEnd;
|
||||
}
|
||||
133
Content.Server/Corvax/Icarus/IcarusBeamSystem.cs
Normal file
@@ -0,0 +1,133 @@
|
||||
using Content.Server.Atmos.Components;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Ghost.Components;
|
||||
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.Corvax.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!;
|
||||
|
||||
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, 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))
|
||||
{
|
||||
phys.LinearDamping = 0f;
|
||||
phys.Friction = 0f;
|
||||
phys.BodyStatus = BodyStatus.InAir;
|
||||
}
|
||||
}
|
||||
|
||||
public void LaunchInDirection(EntityUid uid, Vector2 dir, IcarusBeamComponent? comp = null)
|
||||
{
|
||||
if (!Resolve(uid, ref comp))
|
||||
return;
|
||||
|
||||
if (TryComp(comp.Owner, out PhysicsComponent? phys))
|
||||
_physics.ApplyLinearImpulse(phys, dir * comp.Speed);
|
||||
}
|
||||
|
||||
/// <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 box = new Box2(worldPos - radius, worldPos + radius);
|
||||
|
||||
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;
|
||||
foreach (var entity in _lookup.GetEntitiesInRange(trans.MapID, trans.WorldPosition, radius))
|
||||
{
|
||||
if (!CanDestroy(component, entity))
|
||||
continue;
|
||||
|
||||
QueueDel(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle igniting flammable entities in beam radius.
|
||||
/// </summary>
|
||||
private void BurnEntities(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);
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanDestroy(IcarusBeamComponent component, EntityUid entity)
|
||||
{
|
||||
return entity != component.Owner &&
|
||||
!EntityManager.HasComponent<MapGridComponent>(entity) &&
|
||||
!EntityManager.HasComponent<GhostComponent>(entity);
|
||||
}
|
||||
}
|
||||
227
Content.Server/Corvax/Icarus/IcarusTerminalSystem.cs
Normal file
@@ -0,0 +1,227 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Corvax.Icarus;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
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.Corvax.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 IMapManager _mapManager = 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!;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<IcarusTerminalComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<IcarusTerminalComponent, EntInsertedIntoContainerMessage>(OnItemSlotChanged);
|
||||
SubscribeLocalEvent<IcarusTerminalComponent, EntRemovedFromContainerMessage>(OnItemSlotChanged);
|
||||
|
||||
// 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 OnItemSlotChanged(EntityUid uid, IcarusTerminalComponent component, ContainerModifiedMessage args)
|
||||
{
|
||||
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;
|
||||
|
||||
_chatSystem.DispatchStationAnnouncement(component.Owner,
|
||||
Loc.GetString("icarus-fire-announcement", ("seconds", component.Timer)),
|
||||
Loc.GetString("icarus-announce-sender"),
|
||||
false,
|
||||
colorOverride: Color.Red);
|
||||
SoundSystem.Play(component.AlertSound.GetSound(), Filter.Broadcast());
|
||||
}
|
||||
|
||||
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.TrySetUiState(component.Owner, IcarusTerminalUiKey.Key, new IcarusTerminalUiState(
|
||||
component.Status,
|
||||
(int) component.RemainingTime,
|
||||
(int) component.CooldownTime)
|
||||
);
|
||||
}
|
||||
|
||||
private bool IsAccessGranted(EntityUid uid)
|
||||
{
|
||||
return Comp<ItemSlotsComponent>(uid).Slots.Values.All(v => v.HasItem);
|
||||
}
|
||||
|
||||
private bool CanFire(EntityUid uid, IcarusTerminalComponent component)
|
||||
{
|
||||
return IsAccessGranted(uid) &&
|
||||
component.Status == IcarusTerminalStatus.FIRE_READY;
|
||||
}
|
||||
|
||||
private void Authorize(IcarusTerminalComponent component)
|
||||
{
|
||||
component.Status = IcarusTerminalStatus.FIRE_READY;
|
||||
|
||||
SoundSystem.Play(component.AccessGrantedSound.GetSound(), Filter.Pvs(component.Owner), component.Owner);
|
||||
|
||||
if (!component.AuthorizationNotified)
|
||||
{
|
||||
_chatSystem.DispatchStationAnnouncement(component.Owner, Loc.GetString("icarus-authorized-announcement"),
|
||||
playDefaultSound: false); // TODO: Just pass custom sound path after PR accepting
|
||||
SoundSystem.Play("/Audio/Misc/notice1.ogg",
|
||||
Filter.Broadcast());
|
||||
component.AuthorizationNotified = true;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
SoundSystem.Play(component.FireSound.GetSound(), Filter.Broadcast());
|
||||
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, 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.Stations.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;
|
||||
}
|
||||
}
|
||||
7
Content.Shared/Corvax/Icarus/IcarusKeyComponent.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Content.Shared.Corvax.Icarus;
|
||||
|
||||
/// <summary>
|
||||
/// Used for Icarus terminal activation
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class IcarusKeyComponent : Component {}
|
||||
65
Content.Shared/Corvax/Icarus/IcarusTerminalComponent.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Sound;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Shared.Corvax.Icarus;
|
||||
|
||||
/// <summary>
|
||||
/// Used for Icarus terminal activation
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed 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 = 360;
|
||||
|
||||
/// <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;
|
||||
|
||||
[DataField("alertSound")]
|
||||
public SoundSpecifier AlertSound = new SoundPathSpecifier("/Audio/Corvax/AssaultOperatives/icarus_alarm.ogg");
|
||||
|
||||
[DataField("accessGrantedSound")]
|
||||
public SoundSpecifier AccessGrantedSound = new SoundPathSpecifier("/Audio/Machines/Nuke/confirm_beep.ogg");
|
||||
|
||||
[DataField("fireSound")]
|
||||
public SoundSpecifier FireSound = new SoundPathSpecifier("/Audio/Corvax/AssaultOperatives/sunbeam_fire.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Check if already notified about system authorization
|
||||
/// </summary>
|
||||
public bool AuthorizationNotified = false;
|
||||
|
||||
protected override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Owner.EnsureComponentWarn<ItemSlotsComponent>();
|
||||
}
|
||||
}
|
||||
38
Content.Shared/Corvax/Icarus/SharedIcarus.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Content.Shared.Nuke;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Corvax.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
|
||||
{
|
||||
}
|
||||
BIN
Resources/Audio/Corvax/AssaultOperatives/goldeneyealarm.ogg
Normal file
BIN
Resources/Audio/Corvax/AssaultOperatives/icarus_alarm.ogg
Normal file
BIN
Resources/Audio/Corvax/AssaultOperatives/sunbeam_fire.ogg
Normal file
BIN
Resources/Audio/Corvax/AssaultOperatives/sunbeam_loop.ogg
Normal file
BIN
Resources/Audio/Corvax/Misc/assaultops.ogg
Normal file
@@ -0,0 +1,16 @@
|
||||
# Announce
|
||||
icarus-announce-sender = Icarus Defence Network
|
||||
icarus-authorized-announcement = UNAUTHORISED KEYCARD UPLOAD DETECTED. ALL KEYCARDS UPLOADED!
|
||||
icarus-fire-announcement = /// ICARUS DEFENCE NETWORK BREACHED ///
|
||||
Unauthorised Icarus Defence Network access detected.
|
||||
ICARUS online.
|
||||
Targeting system override detected...
|
||||
New target: /NTSS14/
|
||||
ICARUS firing protocols activated.
|
||||
ETA to fire: { $seconds } seconds.
|
||||
|
||||
# UI
|
||||
icarus-ui-window-title = Icarus Terminal
|
||||
icarus-ui-fire-button = Fire
|
||||
icarus-ui-timer-label = Time until the shot:
|
||||
icarus-ui-cooldown-label = Cooldown:
|
||||
@@ -0,0 +1,20 @@
|
||||
# Announce
|
||||
icarus-announce-sender = Оборонная сеть "Икарус"
|
||||
icarus-authorized-announcement = ОБНАРУЖЕНА НЕСАНКЦИОНИРОВАННАЯ ЗАГРУЗКА КЛЮЧ-КАРТ. ВСЕ КЛЮЧИ ЗАГРУЖЕНЫ!
|
||||
icarus-fire-announcement = /// ВЗЛОМ ЗАЩИЩЕННОЙ СЕТИ "ИКАРУС" ///
|
||||
Обнаружен несанкционированный доступ к оборонной сеть "Икарус"
|
||||
ИКАРУС онлайн.
|
||||
Обнаружено переопределение системы нацеливания...
|
||||
Новая цель: /NTSS14/
|
||||
Активированы протоколы стрельбы ИКАРУС.
|
||||
РАСЧЕТНОЕ ВРЕМЯ выстера: { $seconds } { $seconds ->
|
||||
[one] секунду
|
||||
[few] секунды
|
||||
*[other] секунд
|
||||
}.
|
||||
|
||||
# UI
|
||||
icarus-ui-window-title = Терминал Icarus
|
||||
icarus-ui-fire-button = Огонь
|
||||
icarus-ui-timer-label = Время до выстрела:
|
||||
icarus-ui-cooldown-label = Перезарядка:
|
||||
@@ -0,0 +1,24 @@
|
||||
assaultops-title = Штурмовой оперативники
|
||||
assaultops-description = TODO
|
||||
assaultops-welcome =
|
||||
Вы - штурмовой оперативник. Ваша задача - напасть на { $station } и завладеть всеми ключами доступа GoldenEye, расположенных в головах руководства объекта. Ваше руководство, Синдикат, снабдило вас всем необходимым для выполнения этой задачи.
|
||||
Удачи агент!
|
||||
|
||||
assaultops-opsmajor = [color=crimson]Крупная победа Синдиката![/color]
|
||||
assaultops-opsminor = [color=crimson]Малая победа Синдиката![/color]
|
||||
assaultops-hearty = [color=crimson]Посмертная победа![/color]
|
||||
assaultops-stalemate = [color=yellow]Ничейный исход![/color]
|
||||
assaultops-crewmajor = [color=green]Разгромная победа экипажа![/color]
|
||||
|
||||
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-no-one-ready = Нет готовых игроков! Нельзя начать Штурмовой оперативники.
|
||||
|
||||
assaultops-role-agent = Агент
|
||||
@@ -0,0 +1,2 @@
|
||||
roles-antag-assault-operative-name = Штурмовой оперативники
|
||||
roles-antag-assault-operative-objective = Похищайте глав станции, извлекайте из голов ключи и обратите оружие Nanotrasen против них.
|
||||
14936
Resources/Maps/corvax_assaultopsplanet.yml
Normal file
41
Resources/Prototypes/Corvax/AssaultOperatives/beam.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
- type: entity
|
||||
id: IcarusBeam
|
||||
name: icarus beam
|
||||
description: A beam of light from the sun.
|
||||
components:
|
||||
- type: Clickable
|
||||
- type: MovementIgnoreGravity
|
||||
- type: Sprite
|
||||
sprite: Corvax/AssaultOperatives/sunray.rsi
|
||||
drawdepth: Effects
|
||||
noRot: true
|
||||
scale: 5, 5
|
||||
layers:
|
||||
- state: sunray_splash
|
||||
- state: sunray
|
||||
offset: 0, 1
|
||||
- state: sunray_muzzle
|
||||
offset: 0, 2
|
||||
- type: IcarusBeam
|
||||
- type: AmbientSound
|
||||
range: 14
|
||||
sound:
|
||||
path: /Audio/Corvax/AssaultOperatives/sunbeam_loop.ogg
|
||||
- type: Physics
|
||||
bodyType: Dynamic
|
||||
linearDamping: 0
|
||||
- type: PointLight
|
||||
radius: 12
|
||||
color: yellow
|
||||
energy: 10.0
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
- shape:
|
||||
!type:PhysShapeCircle
|
||||
radius: 2
|
||||
mass: 1
|
||||
hard: false
|
||||
mask:
|
||||
- AllMask
|
||||
layer:
|
||||
- AllMask
|
||||
11
Resources/Prototypes/Corvax/AssaultOperatives/cards.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
- 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: Corvax/AssaultOperatives/goldeneye.rsi
|
||||
layers:
|
||||
- state: goldeneye_key
|
||||
- type: IcarusKey
|
||||
50
Resources/Prototypes/Corvax/AssaultOperatives/terminal.yml
Normal file
@@ -0,0 +1,50 @@
|
||||
- type: entity
|
||||
parent: BaseComputer
|
||||
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.
|
||||
components:
|
||||
- type: Appearance
|
||||
visuals:
|
||||
- type: ComputerVisualizer
|
||||
key: goldeneye_key
|
||||
screen: goldeneye
|
||||
- type: Computer
|
||||
board: CommsComputerCircuitboard # TODO: Create uniq item
|
||||
- type: PointLight
|
||||
radius: 1.5
|
||||
energy: 1.6
|
||||
color: "#b5a13c"
|
||||
- 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: IcarusTerminal
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
- key: enum.IcarusTerminalUiKey.Key
|
||||
type: IcarusTerminalBoundUserInterface
|
||||
- type: ActivatableUI
|
||||
key: enum.IcarusTerminalUiKey.Key
|
||||
@@ -0,0 +1,15 @@
|
||||
- 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: Corvax/Clothing/Mask/gaiter.rsi
|
||||
- type: Clothing
|
||||
sprite: Corvax/Clothing/Mask/gaiter.rsi
|
||||
- type: BreathMask
|
||||
- type: IngestionBlocker
|
||||
- type: DiseaseProtection
|
||||
protection: 0.05
|
||||
- type: IdentityBlocker
|
||||
@@ -41,3 +41,14 @@
|
||||
sprite: Corvax/Clothing/Uniforms/Jumpsuit/centcom_admiral.rsi
|
||||
- type: Clothing
|
||||
sprite: Corvax/Clothing/Uniforms/Jumpsuit/centcom_admiral.rsi
|
||||
|
||||
- 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: Corvax/Clothing/Uniforms/Jumpsuit/tactical_suit.rsi
|
||||
- type: Clothing
|
||||
sprite: Corvax/Clothing/Uniforms/Jumpsuit/tactical_suit.rsi
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,18 @@
|
||||
# Random humanoids
|
||||
|
||||
## AssaultOps
|
||||
|
||||
- type: entity
|
||||
id: RandomHumanoidSpawnerAssaultOp
|
||||
name: Assault Operative
|
||||
components:
|
||||
- type: Icon
|
||||
sprite: Mobs/Species/Human/parts.rsi
|
||||
state: full
|
||||
- type: RandomHumanoidSpawner
|
||||
settings: AssaultOp
|
||||
|
||||
- type: randomHumanoidSettings
|
||||
id: AssaultOp
|
||||
components:
|
||||
- type: AssaultOperative
|
||||
5
Resources/Prototypes/Corvax/GameRules/roundstart.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
- type: gameRule
|
||||
id: Assaultops
|
||||
config:
|
||||
!type:AssaultopsRuleConfiguration
|
||||
id: Assaultops
|
||||
6
Resources/Prototypes/Corvax/Roles/Antags/assaultops.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
- type: antag
|
||||
id: Assaultops
|
||||
name: roles-antag-assault-operative-name
|
||||
antagonist: true
|
||||
setPreference: true
|
||||
objective: roles-antag-assault-operative-objective
|
||||
@@ -0,0 +1,17 @@
|
||||
# Assault Operative Outfit
|
||||
- type: startingGear
|
||||
id: AssaultOperativeGear
|
||||
equipment:
|
||||
jumpsuit: ClothingUniformJumpsuitTactical
|
||||
back: ClothingBackpackChameleon
|
||||
mask: ClothingMaskGaiter
|
||||
eyes: ClothingEyesGlassesSunglasses
|
||||
ears: ClothingHeadsetAltSyndicate
|
||||
gloves: ClothingHandsGlovesCombat
|
||||
shoes: ClothingShoesBootsCombatFilled
|
||||
id: AgentIDCard
|
||||
pocket1: ExtendedEmergencyOxygenTankFilled
|
||||
pocket2: BaseUplinkRadio20TC # Use custom uplink with custom equipment
|
||||
innerclothingskirt: ClothingUniformJumpsuitTactical
|
||||
satchel: ClothingBackpackChameleon
|
||||
duffelbag: ClothingBackpackChameleon
|
||||
11
Resources/Prototypes/Corvax/game_presets.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
- type: gamePreset
|
||||
id: Assaultops
|
||||
alias:
|
||||
- assaultops
|
||||
- assops # yeeeeah
|
||||
name: assaultops-title
|
||||
description: assaultops-description
|
||||
showInVote: false
|
||||
rules:
|
||||
- Assaultops
|
||||
- BasicStationEventScheduler
|
||||
|
After Width: | Height: | Size: 795 B |
|
After Width: | Height: | Size: 705 B |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from skyrat-tg at commit https://github.com/Skyrat-SS13/Skyrat-tg/commit/e2a68d3b15f0daff453ff14a1745f68eeb76b765",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "interrogator_off",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "interrogator_on",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08,
|
||||
0.08
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "interrogator_open"
|
||||
},
|
||||
{
|
||||
"name": "interrogator_closed",
|
||||
"delays": [
|
||||
[
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "goldeneye_key"
|
||||
},
|
||||
{
|
||||
"name": "goldeneye_terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from skyrat-tg at commit https://github.com/Skyrat-SS13/Skyrat-tg/commit/e2a68d3b15f0daff453ff14a1745f68eeb76b765",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "sunray_splash",
|
||||
"delays": [
|
||||
[
|
||||
0.15,
|
||||
0.15,
|
||||
0.15
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sunray",
|
||||
"delays": [
|
||||
[
|
||||
0.15,
|
||||
0.15,
|
||||
0.15
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "sunray_muzzle",
|
||||
"delays": [
|
||||
[
|
||||
0.15,
|
||||
0.15,
|
||||
0.15
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 405 B |
BIN
Resources/Textures/Corvax/Clothing/Mask/gaiter.rsi/icon.png
Normal file
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 348 B |
|
After Width: | Height: | Size: 353 B |
30
Resources/Textures/Corvax/Clothing/Mask/gaiter.rsi/meta.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 395 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 412 B |
|
After Width: | Height: | Size: 392 B |
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/d7e905e4d5ab2b0a8ce210c6ad686aeeebbab426",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "icon"
|
||||
},
|
||||
{
|
||||
"name": "equipped-INNERCLOTHING",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "inhand-left",
|
||||
"directions": 4
|
||||
},
|
||||
{
|
||||
"name": "inhand-right",
|
||||
"directions": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 421 B |
|
After Width: | Height: | Size: 407 B |
@@ -1612,6 +1612,15 @@
|
||||
},
|
||||
{
|
||||
"name": "detective_television"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "goldeneye"
|
||||
},
|
||||
{
|
||||
"name": "goldeneye_key"
|
||||
},
|
||||
{
|
||||
"name": "goldeneye_key_off"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||