Merge branch 'Transfer/Engi' into EngiTransfer

This commit is contained in:
BIGZi0348
2024-11-27 00:13:01 +03:00
49 changed files with 1267 additions and 12 deletions

View File

@@ -135,6 +135,7 @@ namespace Content.Client.Entry
_prototypeManager.RegisterIgnore("wireLayout");
_prototypeManager.RegisterIgnore("alertLevels");
_prototypeManager.RegisterIgnore("nukeopsRole");
_prototypeManager.RegisterIgnore("stationGoal"); // WD
_prototypeManager.RegisterIgnore("ghostRoleRaffleDecider");
//WD-EDIT

View File

@@ -22,6 +22,7 @@ using Content.Shared._White.MagGloves;
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Utility;
using Content.Shared._White._Engi.BucketHelmet;
namespace Content.Server.Strip
{
@@ -135,6 +136,16 @@ namespace Content.Server.Strip
}
// WD EDIT END
// WD EDIT START
if (args.Slot == "ears" && TryComp(strippable, out PreventStrippingFromEarsComponent? _))
{
var message = Loc.GetString("buckethelmet-cant-strip");
_popupSystem.PopupEntity(message, user, user);
return;
}
// WD EDIT END
if (args.IsHand)
{
StripHand((user, userHands), (strippable.Owner, null), args.Slot, strippable);
@@ -605,7 +616,7 @@ namespace Content.Server.Strip
if (ev.Event.InventoryOrHand)
{
if ( ev.Event.InsertOrRemove && !CanStripInsertInventory((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
if (ev.Event.InsertOrRemove && !CanStripInsertInventory((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
!ev.Event.InsertOrRemove && !CanStripRemoveInventory(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
{
ev.Cancel();
@@ -613,7 +624,7 @@ namespace Content.Server.Strip
}
else
{
if ( ev.Event.InsertOrRemove && !CanStripInsertHand((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
if (ev.Event.InsertOrRemove && !CanStripInsertHand((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
!ev.Event.InsertOrRemove && !CanStripRemoveHand(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
{
ev.Cancel();
@@ -634,14 +645,14 @@ namespace Content.Server.Strip
if (ev.InventoryOrHand)
{
if (ev.InsertOrRemove)
StripInsertInventory((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName);
else StripRemoveInventory(entity.Owner, ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
StripInsertInventory((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName);
else StripRemoveInventory(entity.Owner, ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
}
else
{
if (ev.InsertOrRemove)
StripInsertHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
else StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
StripInsertHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
else StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
}
}
}

View File

@@ -0,0 +1,30 @@
using Content.Shared.Inventory.Events;
using Content.Shared._White._Engi.BucketHelmet;
namespace Content.Server._White._Engi.BucketHelmet;
/// <summary>
/// WD.
/// This handles placemet of PreventStrippingFromEarsComponent when bucket helmet is in use.
/// </summary>
public sealed class BucketHelmetSystem : EntitySystem
{
public override void Initialize()
{
SubscribeLocalEvent<BucketHelmetComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<BucketHelmetComponent, GotUnequippedEvent>(OnGotUnequipped);
}
public void OnGotUnequipped(EntityUid uid, BucketHelmetComponent component, GotUnequippedEvent args)
{
RemComp<PreventStrippingFromEarsComponent>(args.Equipee);
}
public void OnGotEquipped(EntityUid uid, BucketHelmetComponent component, GotEquippedEvent args)
{
if (args.Slot == "head")
EnsureComp<PreventStrippingFromEarsComponent>(args.Equipee);
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Audio;
namespace Content.Server._White._Engi.PacifiedOnChaplainAction
{
/// <summary>
/// WD.
/// Adds verb for chaplain to pacify entity.
/// </summary>
[RegisterComponent]
public sealed partial class PacifiedOnChaplainActionComponent : Component
{
[DataField]
public SoundSpecifier ActionSound = new SoundPathSpecifier("/Audio/Effects/holy.ogg");
}
}

View File

@@ -0,0 +1,113 @@
using Content.Shared.ActionBlocker;
using Content.Server.Popups;
using Robust.Shared.Audio.Systems;
using Content.Shared.Interaction;
using Content.Shared.Verbs;
using Content.Server.Bible.Components;
using Content.Shared.Timing;
using Content.Shared.CombatMode.Pacification;
using Content.Server.Administration.Logs;
using Content.Shared.Database;
namespace Content.Server._White._Engi.PacifiedOnChaplainAction
{
/// <summary>
/// WD
/// </summary>
public sealed class PacifiedOnChaplainAction : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PacifiedOnChaplainActionComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<PacifiedOnChaplainActionComponent, GetVerbsEvent<AlternativeVerb>>(AddPacifiedOnChaplainVerb);
}
private void Action(PacifiedOnChaplainActionComponent component, EntityUid target, EntityUid user)
{
var popup = "";
if (HasComp<PacifiedComponent>(target))
{
popup = "unpacified-by-chaplain";
RemComp<PacifiedComponent>(target);
}
else
{
popup = "pacified-by-chaplain";
EnsureComp<PacifiedComponent>(target);
}
_adminLogger.Add(LogType.Verb,
LogImpact.Medium,
$"{ToPrettyString(target):target} {popup} {ToPrettyString(user):user}");
_popupSystem.PopupEntity(Loc.GetString(popup, ("target", target)), user, user);
_audio.PlayPvs(component.ActionSound, user);
}
private void OnAfterInteract(EntityUid uid, PacifiedOnChaplainActionComponent component, AfterInteractEvent args)
{
if (!args.CanReach)
return;
if (!TryComp(uid, out UseDelayComponent? useDelay) || _delay.IsDelayed((uid, useDelay)))
return;
if (args.Target == null)
return;
if (!HasComp<BibleUserComponent>(args.User))
return;
Action(component, (EntityUid) args.Target, args.User);
_delay.TryResetDelay((uid, useDelay));
return;
}
private void AddPacifiedOnChaplainVerb(EntityUid uid, PacifiedOnChaplainActionComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanInteract || !args.CanAccess)
return;
if (!HasComp<BibleUserComponent>(args.User))
return;
if (!_blocker.CanInteract(args.User, uid))
return;
var verbName = "";
if (HasComp<PacifiedComponent>(args.Target))
verbName = Loc.GetString("unpacify-by-chaplain");
else
verbName = Loc.GetString("pacify-by-chaplain");
AlternativeVerb verb = new()
{
Act = () =>
{
if (!TryComp(uid, out UseDelayComponent? useDelay) || _delay.IsDelayed((uid, useDelay)))
return;
Action(component, args.Target, args.User);
_delay.TryResetDelay((uid, useDelay));
},
Text = verbName,
Priority = 2
};
args.Verbs.Add(verb);
}
}
}

View File

@@ -0,0 +1,66 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Commands;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
namespace Content.Server._White._Engi.StationGoal
{
[AdminCommand(AdminFlags.Fun)]
public sealed class StationGoalCommand : IConsoleCommand
{
[Dependency] private readonly IEntityManager _entManager = default!;
public string Command => "sendstationgoal";
public string Description => Loc.GetString("send-station-goal-command-description");
public string Help => Loc.GetString("send-station-goal-command-help-text", ("command", Command));
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length != 2)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
if (!NetEntity.TryParse(args[0], out var euidNet) || !_entManager.TryGetEntity(euidNet, out var euid))
{
shell.WriteError($"Failed to parse euid '{args[0]}'.");
return;
}
var protoId = args[1];
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
if (!prototypeManager.TryIndex<StationGoalPrototype>(protoId, out var proto))
{
shell.WriteError($"No station goal found with ID {protoId}!");
return;
}
var stationGoalPaper = IoCManager.Resolve<IEntityManager>().System<StationGoalPaperSystem>();
if (!stationGoalPaper.SendStationGoal(euid, protoId))
{
shell.WriteError("Station goal was not sent");
return;
}
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
switch (args.Length)
{
case 1:
var stations = ContentCompletionHelper.StationIds(_entManager);
return CompletionResult.FromHintOptions(stations, "[StationId]");
case 2:
var options = IoCManager.Resolve<IPrototypeManager>()
.EnumeratePrototypes<StationGoalPrototype>()
.Select(p => new CompletionOption(p.ID));
return CompletionResult.FromHintOptions(options, Loc.GetString("send-station-goal-command-arg-id"));
}
return CompletionResult.Empty;
}
}
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Prototypes;
namespace Content.Server._White._Engi.StationGoal
{
/// <summary>
/// WD.
/// If attached to a station prototype, will send the station a random goal from the list.
/// </summary>
[RegisterComponent]
public sealed partial class StationGoalComponent : Component
{
[DataField]
public List<ProtoId<StationGoalPrototype>> Goals = new();
}
}

View File

@@ -0,0 +1,11 @@
namespace Content.Server._White._Engi.StationGoal
{
/// <summary>
/// WD.
/// Paper with a written station goal in it.
/// </summary>
[RegisterComponent]
public sealed partial class StationGoalPaperComponent : Component
{
}
}

View File

@@ -0,0 +1,132 @@
using Content.Server.Fax;
using Content.Server.GameTicking.Events;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.Fax.Components;
using Content.Shared.Paper;
using Robust.Server.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Content.Server.RandomMetadata;
namespace Content.Server._White._Engi.StationGoal
{
/// <summary>
/// WD.
/// System to spawn paper with station goal.
/// </summary>
public sealed class StationGoalPaperSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly FaxSystem _fax = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly RandomMetadataSystem _randomMeta = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundStartingEvent>(OnRoundStarting);
}
private void OnRoundStarting(RoundStartingEvent ev)
{
var playerCount = _playerManager.PlayerCount;
var query = EntityQueryEnumerator<StationGoalComponent>();
while (query.MoveNext(out var uid, out var station))
{
var tempGoals = new List<ProtoId<StationGoalPrototype>>(station.Goals);
StationGoalPrototype? selGoal = null;
while (tempGoals.Count > 0)
{
var goalId = _random.Pick(tempGoals);
var goalProto = _proto.Index(goalId);
if (playerCount > goalProto.MaxPlayers ||
playerCount < goalProto.MinPlayers)
{
tempGoals.Remove(goalId);
continue;
}
selGoal = goalProto;
break;
}
if (selGoal is null)
return;
if (SendStationGoal(uid, selGoal))
{
Log.Info($"Goal {selGoal.ID} has been sent to station {MetaData(uid).EntityName}");
}
}
}
public bool SendStationGoal(EntityUid? ent, ProtoId<StationGoalPrototype> goal)
{
return SendStationGoal(ent, _proto.Index(goal));
}
/// <summary>
/// Send a station goal on selected station to all faxes which are authorized to receive it.
/// </summary>
/// <returns>True if at least one fax received paper</returns>
public bool SendStationGoal(EntityUid? ent, StationGoalPrototype goal)
{
if (ent is null)
return false;
if (!TryComp<StationDataComponent>(ent, out var stationData))
return false;
var today = DateTime.Today.ToString("dd.MM");
var namesList = new List<string>
{
"names_first_male",
"names_last_male"
};
var operatorName = _randomMeta.GetRandomFromSegments(namesList, " ");
var faxContent = Loc.GetString("engi-station-goal-form",
("station", MetaData(ent.Value).EntityName),
("date", today),
("operator", operatorName),
("text", Loc.GetString(goal.Text)));
var printout = new FaxPrintout(
faxContent,
Loc.GetString("engi-station-goal-fax-paper-name"),
null,
null,
"paper_stamp-centcom",
new List<StampDisplayInfo>
{
new() { StampedName = Loc.GetString("stamp-component-stamped-name-centcom"), StampedColor = Color.FromHex("#006600") },
});
var wasSent = false;
var query = EntityQueryEnumerator<FaxMachineComponent>();
while (query.MoveNext(out var faxUid, out var fax))
{
if (!fax.ReceiveStationGoal)
continue;
var largestGrid = _station.GetLargestGrid(stationData);
var grid = Transform(faxUid).GridUid;
if (grid is not null && largestGrid == grid.Value)
{
_fax.Receive(faxUid, printout, null, fax);
foreach (var spawnEnt in goal.Spawns)
{
SpawnAtPosition(spawnEnt, Transform(faxUid).Coordinates);
}
wasSent = true;
}
}
return wasSent;
}
}
}

View File

@@ -0,0 +1,29 @@
using Robust.Shared.Prototypes;
namespace Content.Server._White._Engi.StationGoal
{
/// <summary>
/// WD
/// </summary>
[Serializable, Prototype("stationGoal")]
public sealed class StationGoalPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = default!;
[DataField]
public string Text { get; set; } = string.Empty;
[DataField]
public int? MinPlayers;
[DataField]
public int? MaxPlayers;
/// <summary>
/// Goal may require certain items to complete. These items will appear near the receving fax machine at the start of the round.
/// </summary>
[DataField]
public List<EntProtoId> Spawns = new();
}
}

View File

@@ -59,6 +59,14 @@ public sealed partial class FaxMachineComponent : Component
[DataField]
public bool ReceiveNukeCodes { get; set; } = false;
/// <summary>
/// WD.
/// Should that fax receive station goal info
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("receiveStationGoal")]
public bool ReceiveStationGoal { get; set; } = false;
/// <summary>
/// Sound to play when fax has been emagged
/// </summary>

View File

@@ -0,0 +1,11 @@
namespace Content.Shared._White._Engi.BucketHelmet;
/// <summary>
/// WD.
/// This is used for bucket helmet.
/// </summary>
[RegisterComponent]
public sealed partial class BucketHelmetComponent : Component
{
}

View File

@@ -0,0 +1,11 @@
namespace Content.Shared._White._Engi.BucketHelmet;
/// <summary>
/// WD.
/// This is used to block stripping headsets when bucket helmet is on.
/// </summary>
[RegisterComponent]
public sealed partial class PreventStrippingFromEarsComponent : Component
{
}

View File

@@ -0,0 +1,26 @@
using Content.Shared.Damage;
using Robust.Shared.GameStates;
namespace Content.Shared._White._Engi.DamageableClothing;
/// <summary>
/// WD.
/// This component goes on an equippable item that should take damage while in use.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class DamageableClothingComponent : Component
{
/// <summary>
/// The entity that's wearing the item.
/// </summary>
[ViewVariables, AutoNetworkedField]
public EntityUid? User;
/// <summary>
/// The damage modifier to use on item.
/// </summary>
[DataField]
public DamageModifierSet DamageModifier = default!;
}

View File

@@ -0,0 +1,16 @@
namespace Content.Shared._White._Engi.DamageableClothing;
/// <summary>
/// WD.
/// This component gets dynamically added to an Entity via the <see cref="DamageableClothing"/>.
/// </summary>
[RegisterComponent]
public sealed partial class DamageableClothingUserComponent : Component
{
/// <summary>
/// The entity that's also being damaged.
/// </summary>
[DataField]
public EntityUid? ItemId;
}

View File

@@ -0,0 +1,46 @@
using Robust.Shared.Timing;
using Content.Shared.Inventory.Events;
namespace Content.Shared._White._Engi.DamageableClothing;
/// <summary>
/// WD
/// </summary>
public sealed partial class DamageableClothingSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
public override void Initialize()
{
base.Initialize();
InitializeUser();
SubscribeLocalEvent<DamageableClothingComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<DamageableClothingComponent, GotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<DamageableClothingComponent, ComponentShutdown>(OnShutdown);
}
private void OnEquipped(EntityUid uid, DamageableClothingComponent component, GotEquippedEvent args)
{
if (_gameTiming.ApplyingState)
return;
component.User = args.Equipee;
var userComp = EnsureComp<DamageableClothingUserComponent>(args.Equipee);
userComp.ItemId = args.Equipment;
}
private void OnUnequipped(EntityUid uid, DamageableClothingComponent component, GotUnequippedEvent args)
{
RemCompDeferred<DamageableClothingUserComponent>(args.Equipee);
component.User = null;
}
private void OnShutdown(EntityUid uid, DamageableClothingComponent component, ComponentShutdown args)
{
if (component.User != null)
{
RemCompDeferred<DamageableClothingUserComponent>(component.User.Value);
component.User = null;
}
}
}

View File

@@ -0,0 +1,49 @@
using Content.Shared.Damage;
namespace Content.Shared._White._Engi.DamageableClothing;
/// <summary>
/// WD
/// </summary>
public sealed partial class DamageableClothingSystem
{
[Dependency] private readonly DamageableSystem _damageable = default!;
private void InitializeUser()
{
SubscribeLocalEvent<DamageableClothingUserComponent, DamageModifyEvent>(OnUserDamageModified);
SubscribeLocalEvent<DamageableClothingComponent, DamageModifyEvent>(OnDamageModified);
SubscribeLocalEvent<DamageableClothingUserComponent, EntityTerminatingEvent>(OnEntityTerminating);
}
private void OnUserDamageModified(EntityUid uid, DamageableClothingUserComponent component, DamageModifyEvent args)
{
if (TryComp<DamageableClothingComponent>(component.ItemId, out var blocking))
{
if (args.Damage.GetTotal() <= 0)
return;
if (!TryComp<DamageableComponent>(component.ItemId, out var dmgComp))
return;
var blockFraction = 1;
blockFraction = Math.Clamp(blockFraction, 0, 1);
_damageable.TryChangeDamage(component.ItemId, blockFraction * args.OriginalDamage);
}
}
private void OnDamageModified(EntityUid uid, DamageableClothingComponent component, DamageModifyEvent args)
{
var modifier = component.DamageModifier;
args.Damage = DamageSpecifier.ApplyModifierSet(args.Damage, modifier);
}
private void OnEntityTerminating(EntityUid uid, DamageableClothingUserComponent component, ref EntityTerminatingEvent args)
{
if (!TryComp<DamageableClothingComponent>(component.ItemId, out var blockingComponent))
return;
RemCompDeferred<DamageableClothingUserComponent>(uid);
}
}

View File

@@ -0,0 +1,4 @@
ent-ClothingHeadBucketHelmet = шлем из ведра
.desc = Обычное ведро с двумя прорезями для глаз. При ношении на голове что-то липкое внутри цепляется за гарнитуру.
buckethelmet-cant-strip = Шлем из ведра не позволяет это сделать

View File

@@ -0,0 +1,2 @@
ent-ClothingOuterArmorReflectiveGhetto = самодельный отражающий жилет
.desc = Два зеркала соединённые проводами для сомнительной защиты от лазеров.

View File

@@ -0,0 +1,2 @@
ent-MirrorShieldGhetto = самодельный зеркальный щит
.desc = Сделанное на скорую руку зеркало с рукояткой для использования как сомнительная защита от лазеров.

View File

@@ -0,0 +1,5 @@
pacified-by-chaplain = {$target} пасифицирован.
unpacified-by-chaplain = {$target} освобождён.
pacify-by-chaplain = Пасифицировать
unpacify-by-chaplain = Освободить

View File

@@ -0,0 +1,3 @@
ghost-role-information-possessed-blade-name = Одержимый Клинок
ghost-role-information-possessed-blade-description = Вы - Одержимый Клинок. Подчиняйтесь своему владельцу.
ghost-role-information-possessed-blade-rules = Вы не имеете право атаковать своего владельца. Право собственности может быть передано только вашим владельцем.

View File

@@ -0,0 +1,251 @@
ent-StationGoalPaper = цель станции от Центком
.desc = Похоже у вас будет много работы.
engi-station-goal-fax-paper-name = цель станции от Центком
engi-station-goal-form = Постановление Центрального Командования
---------------------------------------------------------------------
Утверждение цели и задачи для станции { $station }
Центральным Командованием № 2562-CU-2
Дата: { $date }.2562 года
---------------------------------------------------------------------
Утвердить следующие цели для космической станции:
{ $text }
---------------------------------------------------------------------
1. Командование станции обязано строго следовать данной цели и заданию, а также предоставить все необходимые ресурсы и усилия для успешного выполнения миссии.
2. Право отменить данное постановление имеет исключительно Центральное Командование.
3. Сотрудникам станции запрещено покидать объект до момента выполнения цели. Исключение - станция находится в критическом состоянии и не является пригодной для жизни.
---------------------------------------------------------------------
Данное Постановление выписано:
И.Ф: { $operator }
Должность: Оператор Центрального Командования
Печать:
engi-station-goal-singularity = [bold]Постройка генератора, основанного на сингулярности.[/bold]
Все детали для цели должны быть заказаны или собраны на станции. Категорически запрещено использовать уже имеющиеся компоненты.
Для успешного выполнения цели необходимо сделать запуск сингулярности.
После окончания смены вся конструкция будет отсоединена от станции и транспортирована на другой объект.
engi-station-goal-solar-panels = [bold]Постройка сети солнечных панелей.[/bold]
Требования:
- Сеть должна состоять из минимум двух ветвей, расположенных на противоположных концах станции.
- Каждая ветвь должна состоять из минимум 36 солнечных панелей.
- Каждая ветвь должна иметь минимум два СМЭСа, несоединённых со станцией.
engi-station-goal-artifacts = [bold]Нахождение и исследование артефактов.[/bold]
Экипаж станции должен найти минимум три артефакта любым способом, отличным от покупки.
Сотрудники научного отдела должны изучить найденные артефакты и задокументировать их свойства.
В качестве документации могут быть приняты отчёты о каждом узле (печатаются на специальной консоли) и отдельный документ с описанием в доступной форме схемы артефакта.
Каждый документ должен быть подтверждён печатью научного руководителя или капитана. Артефакты с документацией должны быть доставлены на станцию центрального командования.
engi-station-goal-bank = [bold]Постройка орбитального хранилища с припасами и технологиями.[/bold]
Хранилище должно быть размещено в космосе, отдельно от основной станции, защищено от метеоритов и иметь автономное питание.
В хранилище должно быть минимум четыре ящика:
- ящик с продвинутыми медикаментами;
- ящик с запасами лучших семян;
- ящик-холодильник еды с высокой питательной ценностью;
- ящик с ценными, но не уникальными платами.
engi-station-goal-zoo = [bold]Улучшение рекреации персонала станции.[/bold]
Для этого инженерный отдел должен построить зоопарк с как минимум 3 (тремя) вольерами. На каждый вольер один вид. Каждый вольер должен быть обеспечен едой для конкретного вида и роботом-уборщиком. Площадь каждого вольера как минимум 16 м².
Животные должны быть заказаны в отделе снабжения.
engi-station-goal-mining-outpost = [bold]Постройка орбитального шахтёрского аванпоста.[/bold]
Аванпост должен быть размещен в космосе, отдельно от основной станции, защищён от метеоритов и иметь автономное питание, гравитацию и атмосферу.
Аванпост должен иметь как минимум две жилые комнаты с освещением и окнами. Оборудование для проведения работ: как минимум по две кирки, сумки для руды. Как минимум два шахтёрских скафандра.
На территории аванпоста должен быть склад для размещения добытого сырья и припасов: как минимум 500u (пятьсот единиц) пива в ящике-холодильнике для закусок. Как минимум по четыре набора медикаментов от механических повреждений.
engi-station-goal-tesla = [bold]Постройка генератора, основанного на Тесле.[/bold]
Все детали для цели должны быть заказаны или собраны на станции. Категорически запрещено использовать уже имеющиеся компоненты.
Для успешного выполнения цели необходимо сделать запуск Теслы.
После окончания смены вся конструкция будет отсоединена от станции и транспортирована на другой объект.
engi-station-goal-security = [bold]Постройка и снабжение тренировочного комплекса для Службы Безопасности, с последующей проверкой состава.[/bold]
В будущем данная станция будет передислоцирована в сектор граничащий с небезопасным.
Задачей Инженерного отдела является постройка тренировочного комплекса, представляющего из себя полосу препятствий. Минимальное время на прохождение среднестатистического, подготовленного офицера, примерно 30 секунд.
Данная полоса должна включать в себя следующее:
- Змееобразные коридоры стекла.
- Различные препятствия для перелезания.
- Деревянную постройку с мишенями, имитирующую захват случайного отдела.
Инженерный отдел в праве строить и другие безопасные виды препятствий.
Задачей Медицинского отдела является изготовление партии таблеток Эфедрина, дозировкой 10u каждая, в минимальном количестве 10 шт, с последующей передачей на хранение в защищенном контейнере Смотрителю станции.
Задачей Службы Безопасности является проведение для каждого члена своего отдела тренировки на данной полосе с письменной фиксацией результатов. В результатах должно быть описано Имя и Фамилия сотрудника, а так же время за которое им была пройдена полоса. Лучшие сотрудники получат шанс на контракт о последующей работе на следующей смене данной станции. Результаты каждого сотрудника будет необходимо указать в отчете о состоянии цели.
engi-station-goal-shuttle-med = [bold]Постройка пилотируемого медицинского шаттла.[/bold]
Шаттл должен соответствовать следующим требованиям:
1. Обеспечен стабильным источником питания и резервной батареей СМЭС.
2. Уметь совершать следующие движения: крен, тангаж и рысканье.
3. Доступ к используемому оборудованию внутри отсеков не должен быть затруднён.
4. Иметь на борту химическую лабораторию с соответствующим оборудованием, рассчитанным минимум на одного сотрудника.
5. Иметь отсек для медицинских коек, рассчитанным минимум на десять человек.
6. Иметь на борту отсек с запасами медикаментов и провизии.
Справочная информация для неквалифицированного персонала:
Крен — вращательное движение.
Тангаж — поступательное движение "вперёд" и "назад".
Рысканье — поступательное движение "боком".
engi-station-goal-shuttle-sec = [bold]Постройка пилотируемого десантного шаттла.[/bold]
Шаттл должен соответствовать следующим требованиям:
1. Обеспечен стабильным источником питания и резервной батареей СМЭС.
2. Уметь совершать следующие движения: крен, тангаж и рысканье.
3. Доступ к используемому оборудованию внутри отсеков не должен быть затруднён.
4. Иметь на борту места и снаряжение как минимум на пять офицеров СБ.
Снаряжение для каждого офицера должно представлять из себя хотя бы один вид легального огнестрельного оружия, полученного не из арсенала, бронежилет, шлем, униформу.
5. Иметь на борту отсек с запасами медикаментов и провизии минимум на пять человек.
Справочная информация для неквалифицированного персонала:
Крен — вращательное движение.
Тангаж — поступательное движение "вперёд" и "назад".
Рысканье — поступательное движение "боком".
engi-station-goal-shuttle-rnd = [bold]Постройка пилотируемого исследовательского шаттла.[/bold]
Шаттл должен соответствовать следующим требованиям:
1. Обеспечен стабильным источником питания и резервной батареей СМЭС.
2. Уметь совершать следующие движения: крен, тангаж и рысканье.
3. Доступ к используемому оборудованию внутри отсеков не должен быть затруднён.
4. Иметь на борту следующие устройства и снаряжение: М.А.К.А.К. (x1), М.А.Р.Т.Ы.Х. (х2), синхронизатор аномалий (x1), магнитные сапоги (х2) и экспериментальный сосуд аномалии (х1), а также как минимум два скафандра EVA и два костюма радиационной защиты.
Справочная информация для неквалифицированного персонала:
Крен — вращательное движение.
Тангаж — поступательное движение "вперёд" и "назад".
Рысканье — поступательное движение "боком".
engi-station-goal-shuttle-srv = [bold]Постройка пилотируемого пассажирского шаттла.[/bold]
Шаттл должен соответствовать следующим требованиям:
1. Обеспечен стабильным источником питания и резервной батареей СМЭС.
2. Уметь совершать следующие движения: крен, тангаж и рысканье.
3. Доступ к используемому оборудованию внутри отсеков не должен быть затруднён.
4. Иметь на борту полностью обустроенный бар, в котором должен быть раздатчик безалкоголя, раздатчик алкоголя, бочка кваса, ящик барных принадлежностей, ящик пополнения раздатчика алкоголя, ящик пополнения раздатчика безалкоголя.
5. Иметь на борту обустроенную кухню, в которой должен быть ящик кухонных припасов и ящик кухонных принадлежностей. А также один гидропонный лоток и холодильник.
6. На борту должны быть приватные комнаты для комфортного проживания как минимум четверых человек.
7. На борту должен быть музыкальный автомат и ящик настольных игр.
Справочная информация для неквалифицированного персонала:
Крен — вращательное движение.
Тангаж — поступательное движение "вперёд" и "назад".
Рысканье — поступательное движение "боком".
engi-station-goal-shuttle-emergency = [bold]Постройка пилотируемого спасательного шаттла.[/bold]
Шаттл должен соответствовать следующим требованиям:
1. Обеспечен стабильным источником питания и резервной батареей СМЭС.
2. Уметь совершать следующие движения: крен, тангаж и рысканье.
3. Доступ к используемому оборудованию внутри отсеков не должен быть затруднён.
4. Иметь на борту ящик медицинских припасов, продвинутый аварийный набор, два ящика с наборами EVA, ящик стекла, два ящика стали, ящик пластали, ящик джетпаков, ящик ИРП.
Справочная информация для неквалифицированного персонала:
Крен — вращательное движение.
Тангаж — поступательное движение "вперёд" и "назад".
Рысканье — поступательное движение "боком".
engi-station-goal-theatre = [bold]Постройка театральной зоны.[/bold]
На данной станции пройдут переговоры с крупными компаниями для заключения партнёрских соглашений.
Театральная зона должна включать в себя:
1. Сцена с минимальным размером в 28м².
2. Минимальная вместимость зала в 21 персону.
3. Закулисье с различными нарядами, музыкальными инструментами, несколькими ящиками игрушек и мехом модели Х.О.Н.К.
4. Комнату или комнаты для четырех актеров. На каждого актера должно приходится по пять любых пирогов.
Материалы для клоунских скафандров вы можете получить из ящика театрального снаряжения.
В случае проведения тестового представления сервисный отдел и командование станции получат вознаграждение по прибытию на Станцию ЦК.
По окончании работ ЦК может прислать представителя для проведения тестового представления.
engi-station-goal-ai = [bold]Постройка автономного отсека ИИ.[/bold]
По окончанию строительства отсек должен быть отстыкован от станции и отправлен дрейфовать в космосе.
Требования:
1. Автономное питание на солнечных панелях или РИТЭГ, подключенные к СМЭС и Подстанции.
2. Иметь отсек для позитронного мозга.
3. Консоль сканера массы.
4. Два борга без позитронного мозга: инженерного и научного назначения.
5. Шлюз в отсек с командным доступом.
6. Сервер коммуникации и маршрутизатор проводных камер.
7. Снаружи отсек должен быть окружён контуром охлаждения.
Контур охлаждения должен представлять собой канал, шириной не менее одного метра, полностью заполненный любым газом, кроме обычной воздушной смеси, при температуре ниже -100 градусов по Цельсию и давлении как минимум в 3 раза превышающем атмосферное. Контур должен опоясывать не менее 80% модуля.
Рекомендуется предпринять дополнительные меры безопасности:
- Защиту от метеоритов.
- Сеть вокруг отсека под высоким напряжением.
engi-station-goal-botany = [bold]Постройка теплиц и выведение приспособленных к температуре растений.[/bold]
На станции или вблизи неё требуется построить три отсека с контролируемым климатом:
- Отсек с температурой 5 градусов Цельсия.
- Отсек с температурой 25 градусов Цельсия.
- Отсек с температурой 45 градусов Цельсия.
Каждый отсек необходимо снабдить минимум шестью гидропонными лотками и полным резервуаром с водой.
В каждом отсеке должно находится хотя бы одно растение, приспособленное к температуре отсека.
engi-station-goal-bunker = [bold]Приспособить станцию к мощным гиперэнергетическим потокам.[/bold]
Необходимо пристроить каждому отделу комнату, далее именуемой “бункер”, в которой экипаж станции мог бы укрыться от последствий гиперэнергетических потоков.
Требования к бункеру:
1. Минимальный размер 25м².
2. Укреплённые стены.
3. Вход через два непрозрачных шлюза с соответствующим отделу доступом.
4. От четырех кресел пилота.
5. Содержать базовые лекарства в виде таблеток со справкой о их назначении и наборы от механических повреждений.
6. Запасы провизии с расчетом на четыре человека, на срок от 72 часов.
7. Автономное питание и канистры с воздухом и кислородом.
8. Шкафчики со снаряжением для биологической и радиационной защиты. Так же аварийные скафандры EVA и костюм сапёра.
9. Интерком с общим ключом шифрования.
engi-station-goal-circus = [bold]Постройка полноценного цирка хомяков.[/bold]
На данной станции пройдут переговоры с крупными компаниями для заключения партнёрских соглашений.
Цирковая зона должна включать в себя:
1. Сцена с минимальным размером в 10м².
2. Минимальная вместимость зала в 10 персон.
3. Закулисье с различными нарядами, музыкальными инструментами и несколькими ящиками игрушек.
4. Комнату дял клеток с хомяками. На каждого актера должно приходится по два уникальных костюма.
В случае проведения тестового представления сервисный отдел и командование станции получат вознаграждение по прибытию на Станцию ЦК.
По окончании работ ЦК может прислать представителя для проведения тестового представления.
station-goal-biodome = [bold]Постройка биокупола для исследования и выращивания аномальных организмов.[/bold]
Требования к проекту:
- Купол с регулируемой атмосферой для выращивания и изучения аномалии "Плоть" и/или "Цветок".
- Наличие системы безопасного входа/выхода для Купола.
- Наличие системы безопасности для защиты от биологических угроз и утечки опасных веществ.
- Наличие системы экстренной отчистки Купола от биологического материала.
- Создание научной группы для проведения экспериментов и анализов собранных образцов.
Проект считается завершённым при успешной реализации созданной Куполом продукции на 10000 (десять тысяч) кредитов.

View File

@@ -1,3 +1,3 @@
send-station-goal-command-description = Отправляет выбранную цель станции на всех факсы способные её принять
send-station-goal-command-description = WD. Отправляет выбранную цель станции на все факсы способные её принять
send-station-goal-command-help-text = Использование: { $command } <id-цели>
send-station-goal-command-arg-id = <ID цели>

View File

@@ -66,3 +66,7 @@
- type: PhysicalComposition
materialComposition:
Plastic: 50
- type: Construction # WD
deconstructionTarget: null
graph: ClothingHeadBucketHelmet
node: start

View File

@@ -28,3 +28,7 @@
storedRotation: -44 # WD
shape: # WD
- 0,0,1,3
- type: KnockDownOnHit
knockDownBehavior: NoDrop
knockdownTime: 0.8
requireWield: true

View File

@@ -39,3 +39,7 @@
isBloodDagger: false
- type: UseDelay
delay: 1
- type: KnockDownOnHit
knockDownBehavior: NoDrop
knockdownTime: 0.8
requireWield: true

View File

@@ -82,6 +82,7 @@
- type: FaxMachine
name: "Central Command"
notifyAdmins: true
receiveStationGoal: true # WD
- type: entity
parent: FaxMachineBase
@@ -109,5 +110,6 @@
- type: FaxMachine
name: "Captain's Office"
receiveNukeCodes: true
receiveStationGoal: true # WD
- type: StealTarget
stealGroup: FaxMachineCaptain

View File

@@ -263,11 +263,11 @@
sprite: White/Objects/Weapons/Chaplain/scythe-inhands.rsi
- type: Sharp
# Может пиздеть
# Может пиздеть и может давать пизды
- type: entity
parent: HolyKatana
id: PossessedBlade
name: одержимый клинок
name: Одержимый Клинок
description: Когда на станции царит хаос, приятно иметь рядом друга.
components:
- type: Sprite
@@ -281,9 +281,9 @@
- suitStorage
- type: GhostRole
allowSpeech: true
name: Одержимый Клинок
description: Вы - одержимый клинок. Подчиняйтесь своему владельцу.
rules: ghost-role-component-default-rules
name: ghost-role-information-possessed-blade-name
description: ghost-role-information-possessed-blade-description
rules: ghost-role-information-possessed-blade-rules
- type: GhostTakeoverAvailable
- type: Examiner
- type: Item
@@ -292,6 +292,14 @@
shape:
- 0, 0, 1, 3
sprite: White/Objects/Weapons/Chaplain/possessed.rsi
- type: DamageOtherOnHit
damage:
types:
Slash: 13
- type: CombatMode
- type: UseDelay
delay: 2.0
- type: PacifiedOnChaplainAction
# Приклеен к руке, быстро и громко бьет
- type: entity
@@ -494,6 +502,9 @@
soundSwing:
collection: HammerMiss
- type: DisarmMalus
- type: KnockDownOnHit
knockDownBehavior: NoDrop
knockdownTime: 0.4
# Имеет все инструменты в себе, но работает медленно и почти не наносит урона
- type: entity

View File

@@ -0,0 +1,26 @@
- type: entity
parent: ClothingHeadBase
id: ClothingHeadBucketHelmet
name: bucket helmet
description: A regular bucket with two eye holes. When worn on the head, something sticky inside latches on the earpiece.
components:
- type: Sprite
sprite: White/_Engi/Clothing/Head/bucketHelmet.rsi
- type: Clothing
sprite: White/_Engi/Clothing/Head/bucketHelmet.rsi
- type: IdentityBlocker
- type: Armor
modifiers:
coefficients:
Blunt: 0.95
- type: Tag
tags:
- HidesHair
- WhitelistChameleon
- type: Construction
deconstructionTarget: start
graph: ClothingHeadBucketHelmet
node: helmet
- type: Item
size: Normal
- type: BucketHelmet

View File

@@ -0,0 +1,53 @@
- type: entity
parent: ClothingOuterArmorBasic
id: ClothingOuterArmorReflectiveGhetto
name: makeshift reflective vest
description: Two mirrors connected by wires for dubious laser protection.
components:
- type: Sprite
sprite: White/_Engi/Clothing/OuterClothing/armor_reflect_ghetto.rsi
state: icon
- type: Clothing
sprite: White/_Engi/Clothing/OuterClothing/armor_reflect_ghetto.rsi
- type: Armor
modifiers:
coefficients:
Slash: 0.9
Heat: 0.7
- type: Reflect
reflectProb: 0.7
innate: true
placement:
- Body
reflects:
- Energy
- type: Construction
graph: ClothingOuterArmorReflectiveGhetto
node: vest
- type: DamageableClothing
damageModifier:
coefficients:
Blunt: 2
Slash: 0.9
Piercing: 1.5
Heat: .5
flatReductions:
Heat: 0.5
- type: Damageable
damageContainer: Shield
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 60
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- !type:PlaySoundBehavior
sound:
collection: GlassBreak
- !type:SpawnEntitiesBehavior
spawn:
ShardGlass:
min: 3
max: 5

View File

@@ -0,0 +1,62 @@
- type: entity
name: makeshift mirror shield
parent: BaseItem
id: MirrorShieldGhetto
description: A makeshift mirror with a handle, used as dubious laser protection.
components:
- type: Sprite
sprite: White/_Engi/Objects/Weapons/ghetto_mirror_shield.rsi
state: icon
- type: Item
sprite: White/_Engi/Objects/Weapons/ghetto_mirror_shield.rsi
size: Ginormous
- type: Tag
tags:
- MirrorShieldGhetto
- type: Reflect
reflectProb: 0.7
innate: true
reflects:
- Energy
- type: Blocking
passiveBlockModifier:
coefficients:
Blunt: 2
Slash: 0.9
Piercing: 1.5
Heat: .6
activeBlockModifier:
coefficients:
Blunt: 2
Slash: 0.9
Piercing: 1.5
Heat: .3
flatReductions:
Heat: 0.5
blockSound: !type:SoundPathSpecifier
path: /Audio/Effects/glass_step.ogg
- type: MeleeBlock
- type: Damageable
damageContainer: Shield
- type: Construction
graph: MirrorShieldGhetto
node: shield
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 40
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- !type:PlaySoundBehavior
sound:
collection: GlassBreak
- !type:SpawnEntitiesBehavior
spawn:
ShardGlass:
min: 2
max: 3
- type: StaticPrice
price: 50
- type: DisarmMalus

View File

@@ -0,0 +1,21 @@
- type: constructionGraph
id: ClothingHeadBucketHelmet
start: start
graph:
- node: start
entity: Bucket
edges:
- to: helmet
conditions:
- !type:SolutionEmpty
solution: bucket
steps:
- tool: Cutting
doAfter: 5
- node: helmet
entity: ClothingHeadBucketHelmet
edges:
- to: start
steps:
- tool: Welding
doAfter: 5

View File

@@ -0,0 +1,42 @@
- type: constructionGraph
id: MirrorShieldGhetto
start: start
graph:
- node: start
edges:
- to: shield
steps:
- material: Glass
amount: 5
doAfter: 3
- material: Steel
amount: 3
doAfter: 3
- node: shield
entity: MirrorShieldGhetto
- type: constructionGraph
id: ClothingOuterArmorReflectiveGhetto
start: start
graph:
- node: start
edges:
- to: vest
steps:
- material: Cable
amount: 5
- tag: MirrorShieldGhetto
name: самодельный зеркальный щит
icon:
sprite: White/_Engi/Objects/Weapons/ghetto_mirror_shield.rsi
state: icon
amount: 1
- tag: MirrorShieldGhetto
name: самодельный зеркальный щит
icon:
sprite: White/_Engi/Objects/Weapons/ghetto_mirror_shield.rsi
state: icon
amount: 1
doAfter: 6
- node: vest
entity: ClothingOuterArmorReflectiveGhetto

View File

@@ -0,0 +1,25 @@
- type: construction
name: самодельный зеркальный щит
id: MirrorShieldGhetto
graph: MirrorShieldGhetto
startNode: start
targetNode: shield
category: construction-category-weapons
objectType: Item
description: Сделанное на скорую руку зеркало с рукояткой для использования как сомнительная защита от лазеров.
icon:
sprite: White/_Engi/Objects/Weapons/ghetto_mirror_shield.rsi
state: icon
- type: construction
name: самодельный отражающий жилет
id: ClothingOuterArmorReflectiveGhetto
graph: ClothingOuterArmorReflectiveGhetto
startNode: start
targetNode: vest
category: construction-category-clothing
objectType: Item
description: Два зеркала соединённые проводами для сомнительной защиты от лазеров.
icon:
sprite: White/_Engi/Clothing/OuterClothing/armor_reflect_ghetto.rsi
state: icon

View File

@@ -0,0 +1,75 @@
- type: stationGoal
id: Singularity
text: engi-station-goal-singularity
- type: stationGoal
id: SolarPanels
text: engi-station-goal-solar-panels
- type: stationGoal
id: Artifacts
text: engi-station-goal-artifacts
- type: stationGoal
id: Bank
text: engi-station-goal-bank
- type: stationGoal
id: Zoo
text: engi-station-goal-zoo
- type: stationGoal
id: MiningOutpost
text: engi-station-goal-mining-outpost
- type: stationGoal
id: Tesla
text: engi-station-goal-tesla
- type: stationGoal
id: SecurityTraining
text: engi-station-goal-security
- type: stationGoal
id: ShuttleMed
text: engi-station-goal-shuttle-med
- type: stationGoal
id: ShuttleSec
text: engi-station-goal-shuttle-sec
- type: stationGoal
id: ShuttleRnd
text: engi-station-goal-shuttle-rnd
- type: stationGoal
id: ShuttleSrv
text: engi-station-goal-shuttle-srv
- type: stationGoal
id: ShuttleEmergency
text: engi-station-goal-shuttle-emergency
- type: stationGoal
id: Theatre
text: engi-station-goal-theatre
- type: stationGoal
id: CellAI
text: engi-station-goal-ai
- type: stationGoal
id: Botany
text: engi-station-goal-botany
- type: stationGoal
id: Bunker
text: engi-station-goal-bunker
- type: stationGoal
id: HamsterСircus
text: engi-station-goal-circus
- type: stationGoal
id: Biodome
text: station-goal-biodome

View File

@@ -120,3 +120,6 @@
- type: Tag
id: VoiceActivatedBombImplant
- type: Tag
id: MirrorShieldGhetto

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,18 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "stepppasha",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "equipped-HELMET",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

View File

@@ -0,0 +1,26 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "stepppasha",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "equipped-OUTERCLOTHING",
"directions": 4
},
{
"name": "inhand-left",
"directions": 4
},
{
"name": "inhand-right",
"directions": 4
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 758 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

View File

@@ -0,0 +1,22 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from https://github.com/Citadel-Station-13/Citadel-Station-13/commit/84223c65f5caf667a84f3c0f49bc2a41cdc6c4e3",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "icon"
},
{
"name": "inhand-right",
"directions": 4
},
{
"name": "inhand-left",
"directions": 4
}
]
}