12 Commits

Author SHA1 Message Date
Morbo
11c5ecf880 Move Icarus to dir & use timing instead of accum 2023-01-08 00:28:16 +03:00
Morbo
9c6a393617 Why i did this 2023-01-08 00:28:15 +03:00
Morbo
eba81123e7 Squashed commit of Icarus
commit e3784bc237
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 11 16:41:35 2022 +0300

    Complete spawn beam command

commit acc2fd5a55
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 11 02:15:30 2022 +0300

    Start creating spawn beam command

commit 179116cc22
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jul 10 01:41:20 2022 +0300

    Update UI button & add timer and cooldown

commit d07d294b91
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 22:32:46 2022 +0300

    Remove fire verb from terminal

commit f6e950cbf7
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 22:32:09 2022 +0300

    Update locale keys

commit 4410380abc
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 22:30:05 2022 +0300

    Fix locale

commit ddd080f5f8
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 22:29:10 2022 +0300

    Add simple terminal UI for firing

commit 925ec94daf
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 18:03:32 2022 +0300

    Dont call Ignite with visual update at every tick

commit 9e868145ff
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jul 9 17:55:31 2022 +0300

    Correct beam spawning directed in center of station

commit 1e9966e946
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 20:57:19 2022 +0300

    Start work on beam spawn & launch direction

commit a3dab14c6c
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 13:48:02 2022 +0300

    Rename stuff & add move separate launch method

commit 586cf26f0f
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 05:16:36 2022 +0300

    Add arson radius for beam

commit 9b76fc7d74
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 03:02:12 2022 +0300

    Use AllMask for fixture beam

commit 24b3223403
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 02:55:35 2022 +0300

    Use ambient component for loop sound

commit a477e2f64f
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jul 8 02:54:57 2022 +0300

    Combine sun ray sprites in one

commit dc2f707f12
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Wed Jul 6 04:09:42 2022 +0300

    Add states & preparing phase

commit 888f5714dc
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Wed Jul 6 00:35:05 2022 +0300

    Tweak beam component

commit 4793eaa494
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Wed Jul 6 00:34:35 2022 +0300

    Copy singularity destroy logic to beam

commit 9f0a8ca631
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:37:49 2022 +0300

    Fix terminal parent

commit bdc027444f
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:37:34 2022 +0300

    No rotation for beam

commit 54834dbb07
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:18:21 2022 +0300

    Remove IcarusBeam comments

commit 8941193a0f
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:16:03 2022 +0300

    Terminal spawn IcarusBeam

commit 993b4315bb
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:12:58 2022 +0300

    Revert "Concatenate images to one"

    This reverts commit acfe951a0c.

commit c3d4e811db
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Mon Jul 4 17:12:49 2022 +0300

    Add loop sound & destroying all

commit acfe951a0c
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jul 3 16:47:21 2022 +0300

    Concatenate images to one

commit c0794feada
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jul 3 16:40:03 2022 +0300

    Basic Icarus beam

commit f6598c255b
Merge: f570a9de6c c3a759f848
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Wed Jun 29 16:50:41 2022 +0300

    Merge branch 'master' into icarus

commit f570a9de6c
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 16:52:33 2022 +0300

    Basic fire system

commit 4591833d81
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 07:16:52 2022 +0300

    Add announcements & activation verb

commit a99c5c3488
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 06:39:24 2022 +0300

    Move system to server and add basic fire logic

commit 6df2a1a571
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 04:39:30 2022 +0300

    Use ItemSlots component for simplicity

commit ab44d02630
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sun Jun 26 03:35:02 2022 +0300

    Add basic key insert to slot

commit dafe921ac4
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Sat Jun 25 17:11:41 2022 +0300

    Add sounds

commit 445a0d544f
Merge: 9864bae192 937ee3da02
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 06:33:45 2022 +0300

    Merge branch 'master' into icarus

commit 9864bae192
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 04:33:24 2022 +0300

    Add basic server icarus components

commit 184bd103b0
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 03:39:24 2022 +0300

    Add to machine meta

commit a88eb38484
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 03:39:13 2022 +0300

    Update terminal sprites

commit d88fe1c1c4
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Fri Jun 24 03:28:55 2022 +0300

    Add computer goldeneye sprites to machines

commit 81fb2c02a2
Author: Arthur <14136326+Morb0@users.noreply.github.com>
Date:   Thu Jun 23 16:04:49 2022 +0300

    Add sprites
2023-01-07 17:27:46 +03:00
Morbo
d6afbb30bb Update todo 2023-01-07 06:15:01 +03:00
Morbo
8434c9cd9f Use custom starting gear 2023-01-07 06:14:10 +03:00
Morbo
335ad04647 Add gaiter mask 2023-01-07 06:13:42 +03:00
Morbo
1521dff5e3 Add tactical suit 2023-01-07 05:55:09 +03:00
Morbo
b6fc63a61d Use custom map 2023-01-07 05:31:57 +03:00
Morbo
bd18265088 Add custom spawnpoint 2023-01-07 05:08:37 +03:00
Morbo
da7170e600 Add todos 2023-01-07 04:58:42 +03:00
Morbo
e9b053c856 Add round end screen info 2023-01-07 04:55:07 +03:00
Morbo
d5afa731cf Assault Ops gamemode barebones 2023-01-07 04:41:31 +03:00
61 changed files with 16553 additions and 1 deletions

View File

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

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

View File

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

View File

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

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

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

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

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

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

View File

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

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

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

Binary file not shown.

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

View File

@@ -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 = Перезарядка:

View File

@@ -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 = Агент

View File

@@ -0,0 +1,2 @@
roles-antag-assault-operative-name = Штурмовой оперативники
roles-antag-assault-operative-objective = Похищайте глав станции, извлекайте из голов ключи и обратите оружие Nanotrasen против них.

File diff suppressed because it is too large Load Diff

View 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

View 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

View 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

View File

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

View File

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

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

View File

@@ -0,0 +1,5 @@
- type: gameRule
id: Assaultops
config:
!type:AssaultopsRuleConfiguration
id: Assaultops

View 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

View File

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

View File

@@ -0,0 +1,11 @@
- type: gamePreset
id: Assaultops
alias:
- assaultops
- assops # yeeeeah
name: assaultops-title
description: assaultops-description
showInVote: false
rules:
- Assaultops
- BasicStationEventScheduler

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 B

View File

@@ -1612,6 +1612,15 @@
},
{
"name": "detective_television"
}
},
{
"name": "goldeneye"
},
{
"name": "goldeneye_key"
},
{
"name": "goldeneye_key_off"
}
]
}