Airlock visuals (#7261)

This commit is contained in:
Joosep Jääger 2022-04-16 05:31:12 +00:00 committed by GitHub
parent 636dc9c26a
commit 0cdb34741e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 528 additions and 6 deletions

View File

@ -0,0 +1,54 @@
using Content.Shared.AirlockPainter;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Utility;
using System.Linq;
using static Robust.Shared.GameObjects.SharedSpriteComponent;
namespace Content.Client.AirlockPainter
{
public sealed class AirlockPainterSystem : SharedAirlockPainterSystem
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
public List<AirlockPainterEntry> Entries { get; private set; } = new();
public override void Initialize()
{
base.Initialize();
foreach (string style in Styles)
{
string? iconPath = Groups
.FindAll(x => x.StylePaths.ContainsKey(style))?
.MaxBy(x => x.IconPriority)?.StylePaths[style];
if (iconPath == null)
{
Entries.Add(new AirlockPainterEntry(style, null));
continue;
}
RSIResource doorRsi = _resourceCache.GetResource<RSIResource>(TextureRoot / new ResourcePath(iconPath));
if (!doorRsi.RSI.TryGetState("closed", out var icon))
{
Entries.Add(new AirlockPainterEntry(style, null));
continue;
}
Entries.Add(new AirlockPainterEntry(style, icon.Frame0));
}
}
}
public sealed class AirlockPainterEntry
{
public string Name;
public Texture? Icon;
public AirlockPainterEntry(string name, Texture? icon)
{
Name = name;
Icon = icon;
}
}
}

View File

@ -0,0 +1,38 @@
using Content.Shared.AirlockPainter;
using Robust.Client.GameObjects;
namespace Content.Client.AirlockPainter.UI
{
public sealed class AirlockPainterBoundUserInterface : BoundUserInterface
{
private AirlockPainterWindow? _window;
public List<string> Styles = new();
public AirlockPainterBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new AirlockPainterWindow();
if (State != null)
UpdateState(State);
// Add styles
var painterSystem = EntitySystem.Get<AirlockPainterSystem>();
_window.Populate(painterSystem.Entries);
_window.OpenCentered();
_window.OnClose += Close;
_window.OnSpritePicked += OnSpritePicked;
}
private void OnSpritePicked(int index)
{
SendMessage(new AirlockPainterSpritePickedMessage(index));
}
}
}

View File

@ -0,0 +1,14 @@
<DefaultWindow xmlns="https://spacestation14.io"
MinSize="300 300"
SetSize="300 300"
Title="{Loc 'airlock-painter-window-title'}">
<BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="150">
<Label Name="SelectedSpriteLabel"
Text="{Loc 'airlock-painter-selected-style'}">
</Label>
<ItemList Name="SpriteList"
SizeFlagsStretchRatio="8"
VerticalExpand="True">
</ItemList>
</BoxContainer>
</DefaultWindow>

View File

@ -0,0 +1,28 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.AirlockPainter.UI
{
[GenerateTypedNameReferences]
public sealed partial class AirlockPainterWindow : DefaultWindow
{
public event Action<int>? OnSpritePicked;
public AirlockPainterWindow()
{
RobustXamlLoader.Load(this);
SpriteList.OnItemSelected += e => OnSpritePicked?.Invoke(e.ItemIndex);
}
public void Populate(List<AirlockPainterEntry> entries)
{
SpriteList.Clear();
foreach (var entry in entries)
{
SpriteList.AddItem(entry.Name, entry.Icon);
}
}
}
}

View File

@ -4,6 +4,7 @@ using Content.Shared.Doors.Components;
using JetBrains.Annotations;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
@ -17,6 +18,7 @@ namespace Content.Client.Doors
{
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private const string AnimationKey = "airlock_animation";
@ -65,7 +67,7 @@ namespace Content.Client.Doors
{
IoCManager.InjectDependencies(this);
CloseAnimation = new Animation {Length = TimeSpan.FromSeconds(_delay)};
CloseAnimation = new Animation { Length = TimeSpan.FromSeconds(_delay) };
{
var flick = new AnimationTrackSpriteFlick();
CloseAnimation.AnimationTracks.Add(flick);
@ -89,7 +91,7 @@ namespace Content.Client.Doors
}
}
OpenAnimation = new Animation {Length = TimeSpan.FromSeconds(_delay)};
OpenAnimation = new Animation { Length = TimeSpan.FromSeconds(_delay) };
{
var flick = new AnimationTrackSpriteFlick();
OpenAnimation.AnimationTracks.Add(flick);
@ -112,7 +114,7 @@ namespace Content.Client.Doors
}
}
}
EmaggingAnimation = new Animation {Length = TimeSpan.FromSeconds(_delay)};
EmaggingAnimation = new Animation { Length = TimeSpan.FromSeconds(_delay) };
{
var flickUnlit = new AnimationTrackSpriteFlick();
EmaggingAnimation.AnimationTracks.Add(flickUnlit);
@ -122,7 +124,7 @@ namespace Content.Client.Doors
if (!_simpleVisuals)
{
DenyAnimation = new Animation {Length = TimeSpan.FromSeconds(_denyDelay)};
DenyAnimation = new Animation { Length = TimeSpan.FromSeconds(_denyDelay) };
{
var flick = new AnimationTrackSpriteFlick();
DenyAnimation.AnimationTracks.Add(flick);
@ -161,6 +163,18 @@ namespace Content.Client.Doors
var weldedVisible = false;
var emergencyLightsVisible = false;
if (component.TryGetData(DoorVisuals.BaseRSI, out string baseRsi))
{
if (!_resourceCache.TryGetResource<RSIResource>(SharedSpriteComponent.TextureRoot / baseRsi, out var res))
{
Logger.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
}
foreach (ISpriteLayer layer in sprite.AllLayers)
{
layer.Rsi = res?.RSI;
}
}
if (animPlayer.HasRunningAnimation(AnimationKey))
{
animPlayer.Stop(AnimationKey);

View File

@ -5,6 +5,7 @@ namespace Content.Client.Entry
{
public static string[] List => new[]
{
"AirlockPainter",
"AmmoBox",
"Pickaxe",
"IngestionBlocker",

View File

@ -0,0 +1,22 @@
using Content.Server.UserInterface;
using Content.Shared.AirlockPainter;
using Content.Shared.Sound;
using Robust.Server.GameObjects;
namespace Content.Server.AirlockPainter
{
[RegisterComponent]
public sealed class AirlockPainterComponent : Component
{
[DataField("spraySound")]
public SoundSpecifier SpraySound = new SoundPathSpecifier("/Audio/Effects/spray2.ogg");
[DataField("sprayTime")]
public float SprayTime = 3.0f;
[DataField("isSpraying")]
public bool IsSpraying = false;
public int Index = default!;
}
}

View File

@ -0,0 +1,138 @@
using Content.Server.DoAfter;
using Content.Server.Popups;
using Content.Server.UserInterface;
using Content.Shared.AirlockPainter;
using Content.Shared.AirlockPainter.Prototypes;
using Content.Shared.Doors.Components;
using Content.Shared.Interaction;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Player;
namespace Content.Server.AirlockPainter
{
/// <summary>
/// A system for painting airlocks using airlock painter
/// </summary>
[UsedImplicitly]
public sealed class AirlockPainterSystem : SharedAirlockPainterSystem
{
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly PopupSystem _popupSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AirlockPainterComponent, AfterInteractEvent>(AfterInteractOn);
SubscribeLocalEvent<AirlockPainterComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<AirlockPainterComponent, AirlockPainterSpritePickedMessage>(OnSpritePicked);
SubscribeLocalEvent<AirlockPainterDoAfterComplete>(OnDoAfterComplete);
SubscribeLocalEvent<AirlockPainterDoAfterCancelled>(OnDoAfterCancelled);
}
private void OnDoAfterComplete(AirlockPainterDoAfterComplete ev)
{
ev.Component.IsSpraying = false;
if (TryComp<AppearanceComponent>(ev.Target, out var appearance) &&
TryComp<PaintableAirlockComponent>(ev.Target, out PaintableAirlockComponent? airlock))
{
SoundSystem.Play(Filter.Pvs(ev.User, entityManager:EntityManager), ev.Component.SpraySound.GetSound(), ev.User);
appearance.SetData(DoorVisuals.BaseRSI, ev.Sprite);
}
}
private void OnDoAfterCancelled(AirlockPainterDoAfterCancelled ev)
{
ev.Component.IsSpraying = false;
}
private void OnActivate(EntityUid uid, AirlockPainterComponent component, ActivateInWorldEvent args)
{
if (!EntityManager.TryGetComponent(args.User, out ActorComponent? actor))
return;
DirtyUI(uid, component);
component.Owner.GetUIOrNull(AirlockPainterUiKey.Key)?.Open(actor.PlayerSession);
args.Handled = true;
}
private void AfterInteractOn(EntityUid uid, AirlockPainterComponent component, AfterInteractEvent args)
{
if (component.IsSpraying || args.Target is not { Valid: true } target || !args.CanReach)
return;
if (!EntityManager.TryGetComponent<PaintableAirlockComponent>(target, out var airlock))
return;
if (!_prototypeManager.TryIndex<AirlockGroupPrototype>(airlock.Group, out var grp))
{
Logger.Error("Group not defined: %s", airlock.Group);
return;
}
string style = Styles[component.Index];
if (!grp.StylePaths.TryGetValue(style, out var sprite))
{
string msg = Loc.GetString("airlock-painter-style-not-available");
_popupSystem.PopupEntity(msg, args.User, Filter.Entities(args.User));
return;
}
component.IsSpraying = true;
var doAfterEventArgs = new DoAfterEventArgs(args.User, component.SprayTime, default, target)
{
BreakOnTargetMove = true,
BreakOnUserMove = true,
BreakOnDamage = true,
BreakOnStun = true,
NeedHand = true,
BroadcastFinishedEvent = new AirlockPainterDoAfterComplete(uid, target, sprite, component),
BroadcastCancelledEvent = new AirlockPainterDoAfterCancelled(component),
};
_doAfterSystem.DoAfter(doAfterEventArgs);
}
private sealed class AirlockPainterDoAfterComplete : EntityEventArgs
{
public readonly EntityUid User;
public readonly EntityUid Target;
public readonly string Sprite;
public readonly AirlockPainterComponent Component;
public AirlockPainterDoAfterComplete(EntityUid user, EntityUid target, string sprite, AirlockPainterComponent component)
{
User = user;
Target = target;
Sprite = sprite;
Component = component;
}
}
private sealed class AirlockPainterDoAfterCancelled : EntityEventArgs
{
public readonly AirlockPainterComponent Component;
public AirlockPainterDoAfterCancelled(AirlockPainterComponent component)
{
Component = component;
}
}
private void OnSpritePicked(EntityUid uid, AirlockPainterComponent component, AirlockPainterSpritePickedMessage args)
{
component.Index = args.Index;
DirtyUI(uid, component);
}
private void DirtyUI(EntityUid uid,
AirlockPainterComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
_userInterfaceSystem.TrySetUiState(uid, AirlockPainterUiKey.Key,
new AirlockPainterBoundUserInterfaceState(component.Index));
}
}
}

View File

@ -0,0 +1,32 @@
using Robust.Shared.Serialization;
namespace Content.Shared.AirlockPainter
{
[Serializable, NetSerializable]
public enum AirlockPainterUiKey
{
Key,
}
[Serializable, NetSerializable]
public sealed class AirlockPainterSpritePickedMessage : BoundUserInterfaceMessage
{
public int Index { get; }
public AirlockPainterSpritePickedMessage(int index)
{
Index = index;
}
}
[Serializable, NetSerializable]
public sealed class AirlockPainterBoundUserInterfaceState : BoundUserInterfaceState
{
public int SelectedStyle { get; }
public AirlockPainterBoundUserInterfaceState(int selectedStyle)
{
SelectedStyle = selectedStyle;
}
}
}

View File

@ -0,0 +1,12 @@
using Content.Shared.AirlockPainter.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.AirlockPainter
{
[RegisterComponent]
public sealed class PaintableAirlockComponent : Component
{
[DataField("group", customTypeSerializer:typeof(PrototypeIdSerializer<AirlockGroupPrototype>))]
public string Group = default!;
}
}

View File

@ -0,0 +1,20 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.AirlockPainter.Prototypes
{
[Prototype("AirlockGroup")]
public sealed class AirlockGroupPrototype : IPrototype
{
[IdDataField]
public string ID { get; } = default!;
[DataField("stylePaths")]
public Dictionary<string, string> StylePaths = default!;
// The priority determines, which sprite is used when showing
// the icon for a style in the airlock painter UI. The highest priority
// gets shown.
[DataField("iconPriority")]
public int IconPriority = 0;
}
}

View File

@ -0,0 +1,30 @@
using System.Linq;
using Content.Shared.AirlockPainter.Prototypes;
using Robust.Shared.Prototypes;
namespace Content.Shared.AirlockPainter
{
public abstract class SharedAirlockPainterSystem : EntitySystem
{
[Dependency] protected readonly IPrototypeManager _prototypeManager = default!;
public List<string> Styles { get; private set; } = new();
public List<AirlockGroupPrototype> Groups { get; private set; } = new();
public override void Initialize()
{
base.Initialize();
HashSet<string> styles = new();
foreach (AirlockGroupPrototype grp in _prototypeManager.EnumeratePrototypes<AirlockGroupPrototype>())
{
Groups.Add(grp);
foreach (string style in grp.StylePaths.Keys)
{
styles.Add(style);
}
}
Styles = styles.ToList();
}
}
}

View File

@ -249,6 +249,7 @@ public enum DoorVisuals
Powered,
BoltLights,
EmergencyLights,
BaseRSI,
}
[Serializable, NetSerializable]

View File

@ -0,0 +1,3 @@
airlock-painter-style-not-available = Cannot apply the selected style to this type of airlock
airlock-painter-window-title = Airlock painter
airlock-painter-selected-style = Selected style

View File

@ -0,0 +1,20 @@
- type: entity
parent: BaseItem
id: AirlockPainter
name: airlock painter
description: An airlock painter for painting airlocks.
components:
- type: Sprite
sprite: Objects/Tools/airlock_painter.rsi
state: airlock_painter
netsync: false
- type: Item
sprite: Objects/Tools/airlock_painter.rsi
- type: UserInterface
interfaces:
- key: enum.AirlockPainterUiKey.Key
type: AirlockPainterBoundUserInterface
- type: AirlockPainter
whitelist:
tags:
- PaintableAirlock

View File

@ -97,7 +97,8 @@
layer: #removed opaque from the layer, allowing lasers to pass through glass airlocks
- Impassable
- VaultImpassable
- type: PaintableAirlock
group: Windoor
- type: entity
parent: AirlockGlass
@ -106,6 +107,8 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/engineering.rsi
- type: PaintableAirlock
group: Glass
- type: entity
parent: AirlockGlass
@ -114,6 +117,8 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/cargo.rsi
- type: PaintableAirlock
group: Glass
- type: entity
parent: AirlockGlass
@ -122,6 +127,8 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/medical.rsi
- type: PaintableAirlock
group: Glass
- type: entity
parent: AirlockGlass
@ -130,6 +137,8 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/science.rsi
- type: PaintableAirlock
group: Glass
- type: entity
parent: AirlockGlass
@ -138,6 +147,8 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/command.rsi
- type: PaintableAirlock
group: Glass
- type: entity
parent: AirlockGlass
@ -146,3 +157,5 @@
components:
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/security.rsi
- type: PaintableAirlock
group: Glass

View File

@ -83,5 +83,7 @@
- type: IconSmooth
key: walls
mode: NoSprite
- type: PaintableAirlock
group: Standard
placement:
mode: SnapgridCenter

View File

@ -21,6 +21,8 @@
visuals:
- type: AirlockVisualizer
- type: WiresVisualizer
- type: PaintableAirlock
group: External
- type: entity
parent: AirlockExternal
@ -30,4 +32,6 @@
- type: Occluder
enabled: false
- type: Sprite
sprite: Structures/Doors/Airlocks/Glass/external.rsi
sprite: Structures/Doors/Airlocks/Glass/external.rsi
- type: PaintableAirlock
group: ExternalGlass

View File

@ -50,6 +50,8 @@
- type: Tag
tags:
- ForceNoFixRotations
- type: PaintableAirlock
group: Shuttle
- type: entity
id: AirlockGlassShuttle
@ -80,3 +82,5 @@
map: ["enum.WiresVisualLayers.MaintenancePanel"]
- type: Occluder
enabled: false
- type: PaintableAirlock
group: ShuttleGlass

View File

@ -0,0 +1,55 @@
- type: AirlockGroup
id: Standard
iconPriority: 100
stylePaths:
basic: Structures/Doors/Airlocks/Standard/basic.rsi
cargo: Structures/Doors/Airlocks/Standard/cargo.rsi
command: Structures/Doors/Airlocks/Standard/command.rsi
engineering: Structures/Doors/Airlocks/Standard/engineering.rsi
freezer: Structures/Doors/Airlocks/Standard/freezer.rsi
maintenance: Structures/Doors/Airlocks/Standard/maint.rsi
medical: Structures/Doors/Airlocks/Standard/medical.rsi
science: Structures/Doors/Airlocks/Standard/science.rsi
security: Structures/Doors/Airlocks/Standard/security.rsi
- type: AirlockGroup
id: Glass
iconPriority: 90
stylePaths:
basic: Structures/Doors/Airlocks/Glass/basic.rsi
command: Structures/Doors/Airlocks/Glass/command.rsi
science: Structures/Doors/Airlocks/Glass/science.rsi
cargo: Structures/Doors/Airlocks/Glass/cargo.rsi
engineering: Structures/Doors/Airlocks/Glass/engineering.rsi
medical: Structures/Doors/Airlocks/Glass/medical.rsi
security: Structures/Doors/Airlocks/Glass/security.rsi
- type: AirlockGroup
id: Windoor
iconPriority: 80
stylePaths:
basic: Structures/Doors/Airlocks/Glass/glass.rsi
- type: AirlockGroup
id: External
iconPriority: 70
stylePaths:
external: Structures/Doors/Airlocks/Standard/external.rsi
- type: AirlockGroup
id: ExternalGlass
iconPriority: 60
stylePaths:
external: Structures/Doors/Airlocks/Glass/external.rsi
- type: AirlockGroup
id: Shuttle
iconPriority: 50
stylePaths:
shuttle: Structures/Doors/Airlocks/Standard/shuttle.rsi
- type: AirlockGroup
id: ShuttleGlass
iconPriority: 40
stylePaths:
shuttle: Structures/Doors/Airlocks/Glass/shuttle.rsi

View File

@ -225,6 +225,9 @@
- type: Tag
id: Payload # for grenade/bomb crafting
- type: Tag
id: PaintableAirlock
- type: Tag
id: PercussionInstrument

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 B

View File

@ -0,0 +1,14 @@
{
"copyright" : "Taken from https://github.com/tgstation/tgstation at commit a21274e56ae84b2c96e8b6beeca805df3d5402e8.",
"license" : "CC-BY-SA-3.0",
"size" : {
"x" : 32,
"y" : 32
},
"states" : [
{
"name" : "airlock_painter"
}
],
"version" : 1
}