]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Station AI customizations (#34501)
authorchromiumboy <50505512+chromiumboy@users.noreply.github.com>
Tue, 13 May 2025 08:50:43 +0000 (03:50 -0500)
committerGitHub <noreply@github.com>
Tue, 13 May 2025 08:50:43 +0000 (01:50 -0700)
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com>
29 files changed:
Content.Client/Silicons/StationAi/StationAiCustomizationBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs [new file with mode: 0644]
Content.Client/Silicons/StationAi/StationAiSystem.cs
Content.Shared/Holopad/HolographicAvatarComponent.cs
Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs
Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/StationAiCustomizationGroupPrototype.cs [new file with mode: 0644]
Content.Shared/Silicons/StationAi/StationAiCustomizationPrototype.cs [new file with mode: 0644]
Resources/Locale/en-US/silicons/station-ai.ftl
Resources/Prototypes/AppearanceCustomization/station_ai.yml [new file with mode: 0644]
Resources/Prototypes/Entities/Mobs/Player/silicon.yml
Resources/Textures/Mobs/Silicon/holograms.rsi/ai_cat.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/holograms.rsi/ai_dog.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/holograms.rsi/ai_face.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/holograms.rsi/ai_female.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/holograms.rsi/ai_male.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/holograms.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_angel.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_angel_dead.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_bliss.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_clown.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_clown_dead.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dorf.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_heartline.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_heartline_dead.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_smiley.png [new file with mode: 0644]
Resources/Textures/Mobs/Silicon/station_ai.rsi/meta.json

diff --git a/Content.Client/Silicons/StationAi/StationAiCustomizationBoundUserInterface.cs b/Content.Client/Silicons/StationAi/StationAiCustomizationBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..1094d2f
--- /dev/null
@@ -0,0 +1,40 @@
+using Content.Shared.Silicons.StationAi;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Silicons.StationAi;
+
+public sealed class StationAiCustomizationBoundUserInterface : BoundUserInterface
+{
+    private StationAiCustomizationMenu? _menu;
+
+    public StationAiCustomizationBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _menu = new StationAiCustomizationMenu(Owner);
+        _menu.OpenCentered();
+        _menu.OnClose += Close;
+
+        _menu.SendStationAiCustomizationMessageAction += SendStationAiCustomizationMessage;
+    }
+
+    public void SendStationAiCustomizationMessage(ProtoId<StationAiCustomizationGroupPrototype> groupProtoId, ProtoId<StationAiCustomizationPrototype> customizationProtoId)
+    {
+        SendPredictedMessage(new StationAiCustomizationMessage(groupProtoId, customizationProtoId));
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+
+        if (!disposing)
+            return;
+
+        _menu?.Dispose();
+    }
+}
diff --git a/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml b/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml
new file mode 100644 (file)
index 0000000..c03bc40
--- /dev/null
@@ -0,0 +1,23 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+         xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+         xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+         SetSize="600 600"
+         MinSize="600 220">
+    <BoxContainer Orientation="Vertical" VerticalExpand="True">
+        <BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="10 5 10 -5">
+            <controls:StripeBack HorizontalExpand="True">
+                <BoxContainer Orientation="Horizontal" HorizontalExpand="True">
+                    <Control SetWidth="200">
+                        <Label Text="{Loc 'station-ai-customization-categories'}" Margin="0 5 0 5" HorizontalExpand="True" HorizontalAlignment="Center"  />
+                    </Control>
+                    <Label Text="{Loc 'station-ai-customization-options'}" Margin="0 5 0 5" HorizontalExpand="True" HorizontalAlignment="Center"/>
+                </BoxContainer>
+            </controls:StripeBack>
+        </BoxContainer>
+        <VerticalTabContainer Name="CustomizationGroupsContainer"
+                              VerticalExpand="True"
+                              HorizontalExpand="True">
+        </VerticalTabContainer>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs b/Content.Client/Silicons/StationAi/StationAiCustomizationMenu.xaml.cs
new file mode 100644 (file)
index 0000000..0099691
--- /dev/null
@@ -0,0 +1,176 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Silicons.StationAi;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+using System.Linq;
+using System.Numerics;
+
+namespace Content.Client.Silicons.StationAi;
+
+[GenerateTypedNameReferences]
+public sealed partial class StationAiCustomizationMenu : FancyWindow
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
+
+    private Dictionary<ProtoId<StationAiCustomizationGroupPrototype>, StationAiCustomizationGroupContainer> _groupContainers = new();
+    private Dictionary<ProtoId<StationAiCustomizationGroupPrototype>, ButtonGroup> _buttonGroups = new();
+
+    public event Action<ProtoId<StationAiCustomizationGroupPrototype>, ProtoId<StationAiCustomizationPrototype>>? SendStationAiCustomizationMessageAction;
+
+    private const float IconScale = 1.75f;
+
+    public StationAiCustomizationMenu(EntityUid owner)
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        var stationAiSystem = _entManager.System<SharedStationAiSystem>();
+
+        // Load customziation data
+        _entManager.TryGetComponent<StationAiCoreComponent>(owner, out var stationAiCore);
+        stationAiSystem.TryGetHeld((owner, stationAiCore), out var insertedAi);
+        _entManager.TryGetComponent<StationAiCustomizationComponent>(insertedAi, out var stationAiCustomization);
+
+        // Create UI entires for each group of customizations
+        var groupPrototypes = _protoManager.EnumeratePrototypes<StationAiCustomizationGroupPrototype>();
+        groupPrototypes = groupPrototypes.OrderBy(x => x.ID); // To ensure consistency in presentation
+
+        foreach (var groupPrototype in groupPrototypes)
+        {
+            StationAiCustomizationPrototype? selectedPrototype = null;
+
+            if (stationAiCustomization?.ProtoIds.TryGetValue(groupPrototype, out var selectedProtoId) == true)
+                _protoManager.TryIndex(selectedProtoId, out selectedPrototype);
+
+            _buttonGroups[groupPrototype] = new ButtonGroup();
+            _groupContainers[groupPrototype] = new StationAiCustomizationGroupContainer(groupPrototype, selectedPrototype, _buttonGroups[groupPrototype], this, _protoManager);
+            CustomizationGroupsContainer.AddTab(_groupContainers[groupPrototype], Loc.GetString(groupPrototype.Name));
+        }
+
+        Title = Loc.GetString("station-ai-customization-menu");
+    }
+
+    public void OnSendStationAiCustomizationMessage
+        (ProtoId<StationAiCustomizationGroupPrototype> groupProtoId, ProtoId<StationAiCustomizationPrototype> customizationProtoId)
+    {
+        SendStationAiCustomizationMessageAction?.Invoke(groupProtoId, customizationProtoId);
+    }
+
+    private sealed class StationAiCustomizationGroupContainer : BoxContainer
+    {
+        public StationAiCustomizationGroupContainer
+            (StationAiCustomizationGroupPrototype groupPrototype,
+            StationAiCustomizationPrototype? selectedPrototype,
+            ButtonGroup buttonGroup,
+            StationAiCustomizationMenu menu,
+            IPrototypeManager protoManager)
+        {
+            Orientation = LayoutOrientation.Vertical;
+            HorizontalExpand = true;
+            VerticalExpand = true;
+
+            // Create UI entries for all customization in the group
+            foreach (var protoId in groupPrototype.ProtoIds)
+            {
+                if (!protoManager.TryIndex(protoId, out var prototype))
+                    continue;
+
+                var entry = new StationAiCustomizationEntryContainer(groupPrototype, prototype, buttonGroup, menu);
+                AddChild(entry);
+
+                if (prototype == selectedPrototype)
+                    entry.SelectButton.Pressed = true;
+            }
+        }
+    }
+
+    private sealed class StationAiCustomizationEntryContainer : BoxContainer
+    {
+        public ProtoId<StationAiCustomizationPrototype> ProtoId;
+        public Button SelectButton;
+
+        public StationAiCustomizationEntryContainer
+            (StationAiCustomizationGroupPrototype groupPrototype,
+            StationAiCustomizationPrototype prototype,
+            ButtonGroup buttonGroup,
+            StationAiCustomizationMenu menu)
+        {
+            ProtoId = prototype;
+
+            Orientation = LayoutOrientation.Horizontal;
+            HorizontalExpand = true;
+
+            // Create a selection button
+            SelectButton = new Button
+            {
+                Text = Loc.GetString(prototype.Name),
+                HorizontalExpand = true,
+                ToggleMode = true,
+                Group = buttonGroup,
+            };
+
+            SelectButton.OnPressed += args =>
+            {
+                menu.OnSendStationAiCustomizationMessage(groupPrototype, prototype);
+            };
+
+            AddChild(SelectButton);
+
+            // Creat a background for the preview
+            var background = new AnimatedTextureRect
+            {
+                HorizontalAlignment = HAlignment.Center,
+                VerticalAlignment = VAlignment.Center,
+                SetWidth = 56,
+                SetHeight = 56,
+                Margin = new Thickness(10f, 2f),
+            };
+
+            background.DisplayRect.TextureScale = new Vector2(IconScale, IconScale);
+
+            if (prototype.PreviewBackground != null)
+            {
+                background.SetFromSpriteSpecifier(prototype.PreviewBackground);
+            }
+
+            AddChild(background);
+
+            // Create a preview icon
+            var icon = new AnimatedTextureRect
+            {
+                HorizontalAlignment = HAlignment.Center,
+                VerticalAlignment = VAlignment.Center,
+                SetWidth = 56,
+                SetHeight = 56,
+            };
+
+            icon.DisplayRect.TextureScale = new Vector2(IconScale, IconScale);
+
+            // Default RSI path/state
+            var rsiPath = prototype.LayerData.FirstOrNull()?.Value.RsiPath;
+            var rsiState = prototype.LayerData.FirstOrNull()?.Value.State;
+
+            // Specified RSI path/state
+            if (!string.IsNullOrEmpty(prototype.PreviewKey) && prototype.LayerData.TryGetValue(prototype.PreviewKey, out var layerData))
+            {
+                rsiPath = layerData.RsiPath;
+                rsiState = layerData.State;
+            }
+
+            // Update icon
+            if (rsiPath != null && rsiState != null)
+            {
+                var specifier = new SpriteSpecifier.Rsi(new ResPath(rsiPath), rsiState);
+                icon.SetFromSpriteSpecifier(specifier);
+            }
+
+            background.AddChild(icon);
+        }
+    }
+}
+
+
index ab9ace3c1d583d8c261dbce7bf37116969fe45e3..75588eda39138c04772f9ed007ba65ccde10d102 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Silicons.StationAi;
+using Robust.Client.GameObjects;
 using Robust.Client.Graphics;
 using Robust.Client.Player;
 using Robust.Shared.Player;
@@ -9,6 +10,7 @@ public sealed partial class StationAiSystem : SharedStationAiSystem
 {
     [Dependency] private readonly IOverlayManager _overlayMgr = default!;
     [Dependency] private readonly IPlayerManager _player = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
 
     private StationAiOverlay? _overlay;
 
@@ -22,6 +24,7 @@ public sealed partial class StationAiSystem : SharedStationAiSystem
         SubscribeLocalEvent<StationAiOverlayComponent, LocalPlayerDetachedEvent>(OnAiDetached);
         SubscribeLocalEvent<StationAiOverlayComponent, ComponentInit>(OnAiOverlayInit);
         SubscribeLocalEvent<StationAiOverlayComponent, ComponentRemove>(OnAiOverlayRemove);
+        SubscribeLocalEvent<StationAiCoreComponent, AppearanceChangeEvent>(OnAppearanceChange);
     }
 
     private void OnAiOverlayInit(Entity<StationAiOverlayComponent> ent, ref ComponentInit args)
@@ -72,6 +75,17 @@ public sealed partial class StationAiSystem : SharedStationAiSystem
         RemoveOverlay();
     }
 
+    private void OnAppearanceChange(Entity<StationAiCoreComponent> entity, ref AppearanceChangeEvent args)
+    {
+        if (args.Sprite == null)
+            return;
+
+        if (_appearance.TryGetData<PrototypeLayerData>(entity.Owner, StationAiVisualState.Key, out var layerData, args.Component))
+            args.Sprite.LayerSetData(StationAiVisualState.Key, layerData);
+
+        args.Sprite.LayerSetVisible(StationAiVisualState.Key, layerData != null);
+    }
+
     public override void Shutdown()
     {
         base.Shutdown();
index cff71f4fb2828efa969e0b3573438acd29b4523a..0e475ff691d6695f98d4262dea785d1bff5a6e99 100644 (file)
@@ -9,5 +9,5 @@ public sealed partial class HolographicAvatarComponent : Component
     /// The prototype sprite layer data for the hologram
     /// </summary>
     [DataField, AutoNetworkedField]
-    public PrototypeLayerData[] LayerData;
+    public PrototypeLayerData[]? LayerData = null;
 }
diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Customization.cs
new file mode 100644 (file)
index 0000000..d3f3fe4
--- /dev/null
@@ -0,0 +1,82 @@
+using Content.Shared.Holopad;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Silicons.StationAi;
+
+public abstract partial class SharedStationAiSystem
+{
+    private ProtoId<StationAiCustomizationGroupPrototype> _stationAiCoreCustomGroupProtoId = "StationAiCoreIconography";
+    private ProtoId<StationAiCustomizationGroupPrototype> _stationAiHologramCustomGroupProtoId = "StationAiHolograms";
+
+    private void InitializeCustomization()
+    {
+        SubscribeLocalEvent<StationAiCoreComponent, StationAiCustomizationMessage>(OnStationAiCustomization);
+    }
+
+    private void OnStationAiCustomization(Entity<StationAiCoreComponent> entity, ref StationAiCustomizationMessage args)
+    {
+        if (!_protoManager.TryIndex(args.GroupProtoId, out var groupPrototype) || !_protoManager.TryIndex(args.CustomizationProtoId, out var customizationProto))
+            return;
+
+        if (!TryGetHeld((entity, entity.Comp), out var held))
+            return;
+
+        if (!TryComp<StationAiCustomizationComponent>(held, out var stationAiCustomization))
+            return;
+
+        if (stationAiCustomization.ProtoIds.TryGetValue(args.GroupProtoId, out var protoId) && protoId == args.CustomizationProtoId)
+            return;
+
+        stationAiCustomization.ProtoIds[args.GroupProtoId] = args.CustomizationProtoId;
+
+        Dirty(held, stationAiCustomization);
+
+        // Update hologram
+        if (groupPrototype.Category == StationAiCustomizationType.Hologram)
+            UpdateHolographicAvatar((held, stationAiCustomization));
+
+        // Update core iconography
+        if (groupPrototype.Category == StationAiCustomizationType.CoreIconography && TryComp<StationAiHolderComponent>(entity, out var stationAiHolder))
+            UpdateAppearance((entity, stationAiHolder));
+    }
+
+    private void UpdateHolographicAvatar(Entity<StationAiCustomizationComponent> entity)
+    {
+        if (!TryComp<HolographicAvatarComponent>(entity, out var avatar))
+            return;
+
+        if (!entity.Comp.ProtoIds.TryGetValue(_stationAiHologramCustomGroupProtoId, out var protoId))
+            return;
+
+        if (!_protoManager.TryIndex(protoId, out var prototype))
+            return;
+
+        if (!prototype.LayerData.TryGetValue(StationAiState.Hologram.ToString(), out var layerData))
+            return;
+
+        avatar.LayerData = [layerData];
+        Dirty(entity, avatar);
+    }
+
+    private void CustomizeAppearance(Entity<StationAiCoreComponent> entity, StationAiState state)
+    {
+        var stationAi = GetInsertedAI(entity);
+
+        if (stationAi == null)
+        {
+            _appearance.RemoveData(entity.Owner, StationAiVisualState.Key);
+            return;
+        }
+
+        if (!TryComp<StationAiCustomizationComponent>(stationAi, out var stationAiCustomization) ||
+            !stationAiCustomization.ProtoIds.TryGetValue(_stationAiCoreCustomGroupProtoId, out var protoId) ||
+            !_protoManager.TryIndex(protoId, out var prototype) ||
+            !prototype.LayerData.TryGetValue(state.ToString(), out var layerData))
+        {
+            return;
+        }
+
+        // This data is handled manually in the client StationAiSystem
+        _appearance.SetData(entity.Owner, StationAiVisualState.Key, layerData);
+    }
+}
index 372a758f80caaca652d880bc9a55f4f06396aacf..1615ab0fa309ccf27dd9a9523fc71c23876f179b 100644 (file)
@@ -28,6 +28,7 @@ using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 using Robust.Shared.Timing;
 using System.Diagnostics.CodeAnalysis;
+using Robust.Shared.Utility;
 
 namespace Content.Shared.Silicons.StationAi;
 
@@ -56,6 +57,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
     [Dependency] private readonly   SharedTransformSystem _xforms = default!;
     [Dependency] private readonly   SharedUserInterfaceSystem _uiSystem = default!;
     [Dependency] private readonly   StationAiVisionSystem _vision = default!;
+    [Dependency] private readonly   IPrototypeManager _protoManager = default!;
 
     // StationAiHeld is added to anything inside of an AI core.
     // StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / core).
@@ -82,6 +84,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem
         InitializeAirlock();
         InitializeHeld();
         InitializeLight();
+        InitializeCustomization();
 
         SubscribeLocalEvent<StationAiWhitelistComponent, BoundUserInterfaceCheckRangeEvent>(OnAiBuiCheck);
 
@@ -107,25 +110,35 @@ public abstract partial class SharedStationAiSystem : EntitySystem
 
     private void OnCoreVerbs(Entity<StationAiCoreComponent> ent, ref GetVerbsEvent<Verb> args)
     {
-        if (!_admin.IsAdmin(args.User) ||
-            TryGetHeld((ent.Owner, ent.Comp), out _))
+        var user = args.User;
+
+        // Admin option to take over the station AI core
+        if (_admin.IsAdmin(args.User) &&
+            !TryGetHeld((ent.Owner, ent.Comp), out _))
         {
-            return;
+            args.Verbs.Add(new Verb()
+            {
+                Text = Loc.GetString("station-ai-takeover"),
+                Category = VerbCategory.Debug,
+                Act = () =>
+                {
+                    var brain = SpawnInContainerOrDrop(DefaultAi, ent.Owner, StationAiCoreComponent.Container);
+                    _mind.ControlMob(user, brain);
+                },
+                Impact = LogImpact.High,
+            });
         }
 
-        var user = args.User;
-
-        args.Verbs.Add(new Verb()
+        // Option to open the station AI customization menu
+        if (TryGetHeld((ent, ent.Comp), out var insertedAi) && insertedAi == user)
         {
-            Text = Loc.GetString("station-ai-takeover"),
-            Category = VerbCategory.Debug,
-            Act = () =>
+            args.Verbs.Add(new Verb()
             {
-                var brain = SpawnInContainerOrDrop(DefaultAi, ent.Owner, StationAiCoreComponent.Container);
-                _mind.ControlMob(user, brain);
-            },
-            Impact = LogImpact.High,
-        });
+                Text = Loc.GetString("station-ai-customization-menu"),
+                Act = () => _uiSystem.TryOpenUi(ent.Owner, StationAiCustomizationUiKey.Key, insertedAi),
+                Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/emotes.svg.192dpi.png")),
+            });
+        }
     }
 
     private void OnAiAccessible(Entity<StationAiOverlayComponent> ent, ref AccessibleOverrideEvent args)
@@ -494,14 +507,21 @@ public abstract partial class SharedStationAiSystem : EntitySystem
         if (!Resolve(entity.Owner, ref entity.Comp, false))
             return;
 
-        if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) ||
-            container.Count == 0)
+        // Todo: when AIs can die, add a check to see if the AI is in the 'dead' state
+        var state = StationAiState.Empty;
+
+        if (_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) && container.Count > 0)
+            state = StationAiState.Occupied;
+
+        // If the entity is a station AI core, attempt to customize its appearance
+        if (TryComp<StationAiCoreComponent>(entity, out var stationAiCore))
         {
-            _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Empty);
+            CustomizeAppearance((entity, stationAiCore), state);
             return;
         }
 
-        _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Occupied);
+        // Otherwise let generic visualizers handle the appearance update
+        _appearance.SetData(entity.Owner, StationAiVisualState.Key, state);
     }
 
     public virtual void AnnounceIntellicardUsage(EntityUid uid, SoundSpecifier? cue = null) { }
@@ -550,17 +570,23 @@ public sealed partial class JumpToCoreEvent : InstantActionEvent
 [Serializable, NetSerializable]
 public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent;
 
-
 [Serializable, NetSerializable]
 public enum StationAiVisualState : byte
 {
     Key,
 }
 
+[Serializable, NetSerializable]
+public enum StationAiSpriteState : byte
+{
+    Key,
+}
+
 [Serializable, NetSerializable]
 public enum StationAiState : byte
 {
     Empty,
     Occupied,
     Dead,
+    Hologram,
 }
diff --git a/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs b/Content.Shared/Silicons/StationAi/StationAiCustomizationComponent.cs
new file mode 100644 (file)
index 0000000..a2b713e
--- /dev/null
@@ -0,0 +1,53 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// Holds data for altering the appearance of station AIs.
+/// </summary>
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class StationAiCustomizationComponent : Component
+{
+    /// <summary>
+    /// Dictionary of the prototype data used for customizing the appearance of the entity.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public Dictionary<ProtoId<StationAiCustomizationGroupPrototype>, ProtoId<StationAiCustomizationPrototype>> ProtoIds = new();
+}
+
+/// <summary>
+/// Message sent to server that contains a station AI customization that the client has selected
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class StationAiCustomizationMessage : BoundUserInterfaceMessage
+{
+    public readonly ProtoId<StationAiCustomizationGroupPrototype> GroupProtoId;
+    public readonly ProtoId<StationAiCustomizationPrototype> CustomizationProtoId;
+
+    public StationAiCustomizationMessage(ProtoId<StationAiCustomizationGroupPrototype> groupProtoId, ProtoId<StationAiCustomizationPrototype> customizationProtoId)
+    {
+        GroupProtoId = groupProtoId;
+        CustomizationProtoId = customizationProtoId;
+    }
+}
+
+/// <summary>
+/// Key for opening the station AI customization UI
+/// </summary>
+[Serializable, NetSerializable]
+public enum StationAiCustomizationUiKey : byte
+{
+    Key,
+}
+
+/// <summary>
+/// The different catagories of station Ai customizations available
+/// </summary>
+[Serializable, NetSerializable]
+public enum StationAiCustomizationType : byte
+{
+    CoreIconography,
+    Hologram,
+}
diff --git a/Content.Shared/Silicons/StationAi/StationAiCustomizationGroupPrototype.cs b/Content.Shared/Silicons/StationAi/StationAiCustomizationGroupPrototype.cs
new file mode 100644 (file)
index 0000000..bbb687e
--- /dev/null
@@ -0,0 +1,31 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// Holds data for customizing the appearance of station AIs.
+/// </summary>
+[Prototype]
+public sealed partial class StationAiCustomizationGroupPrototype : IPrototype
+{
+    [IdDataField]
+    public string ID { get; } = string.Empty;
+
+    /// <summary>
+    /// The localized name of the customization.
+    /// </summary>
+    [DataField(required: true)]
+    public LocId Name;
+
+    /// <summary>
+    /// The type of customization that is associated with this group.
+    /// </summary>
+    [DataField]
+    public StationAiCustomizationType Category = StationAiCustomizationType.CoreIconography;
+
+    /// <summary>
+    /// The list of prototypes associated with the customization group.
+    /// </summary>
+    [DataField(required: true)]
+    public List<ProtoId<StationAiCustomizationPrototype>> ProtoIds = new();
+}
diff --git a/Content.Shared/Silicons/StationAi/StationAiCustomizationPrototype.cs b/Content.Shared/Silicons/StationAi/StationAiCustomizationPrototype.cs
new file mode 100644 (file)
index 0000000..5fe3891
--- /dev/null
@@ -0,0 +1,54 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Silicons.StationAi;
+
+/// <summary>
+/// Holds data for customizing the appearance of station AIs.
+/// </summary>
+[Prototype]
+public sealed partial class StationAiCustomizationPrototype : IPrototype, IInheritingPrototype
+{
+    [IdDataField]
+    public string ID { get; } = string.Empty;
+
+    /// <summary>
+    /// The (unlocalized) name of the customization.
+    /// </summary>
+    [DataField(required: true)]
+    public LocId Name;
+
+    /// <summary>
+    /// Stores the data which is used to modify the appearance of the station AI.
+    /// </summary>
+    [DataField(required: true)]
+    public Dictionary<string, PrototypeLayerData> LayerData = new();
+
+    /// <summary>
+    /// Key used to index the prototype layer data and extract a preview of the customization (for menus, etc)
+    /// </summary>
+    [DataField]
+    public string PreviewKey = string.Empty;
+
+    /// <summary>
+    /// Specifies a background to use for previewing the customization (for menus, etc)
+    /// </summary>
+    [DataField]
+    public SpriteSpecifier? PreviewBackground;
+
+    /// <summary>
+    /// The prototype we inherit from.
+    /// </summary>
+    [ViewVariables]
+    [ParentDataFieldAttribute(typeof(AbstractPrototypeIdArraySerializer<StationAiCustomizationPrototype>))]
+    public string[]? Parents { get; }
+
+    /// <summary>
+    /// Specifies whether the prototype is abstract.
+    /// </summary>
+    [ViewVariables]
+    [NeverPushInheritance]
+    [AbstractDataField]
+    public bool Abstract { get; }
+}
index 76c30eb10187282ae0764b19ab79755dac8b8062..abdddbe1e0d2bc3501631105e8269bcaee9f28b8 100644 (file)
@@ -22,3 +22,25 @@ toggle-light = Toggle light
 ai-device-not-responding = Device is not responding
 
 ai-consciousness-download-warning = Your consciousness is being downloaded.
+
+# UI
+station-ai-customization-menu = AI customization
+station-ai-customization-categories = Categories
+station-ai-customization-options = Options (choice of one)
+station-ai-customization-core = AI core displays
+station-ai-customization-hologram = Holographic avatars
+
+# Customizations
+station-ai-icon-ai = Ghost in the machine
+station-ai-icon-angel = Guardian angel
+station-ai-icon-bliss = Simpler times
+station-ai-icon-clown = Clownin' around
+station-ai-icon-dorf = Adventure awaits
+station-ai-icon-heartline = Lifeline
+station-ai-icon-smiley = All smiles
+
+station-ai-hologram-female = Female appearance
+station-ai-hologram-male = Male appearance
+station-ai-hologram-face = Disembodied head
+station-ai-hologram-cat = Cat form
+station-ai-hologram-dog = Corgi form
\ No newline at end of file
diff --git a/Resources/Prototypes/AppearanceCustomization/station_ai.yml b/Resources/Prototypes/AppearanceCustomization/station_ai.yml
new file mode 100644 (file)
index 0000000..465cfdf
--- /dev/null
@@ -0,0 +1,163 @@
+# Groups
+- type: stationAiCustomizationGroup
+  id: StationAiCoreIconography
+  name: station-ai-customization-core
+  category: CoreIconography
+  protoIds:
+  - StationAiIconAi
+  - StationAiIconAngel
+  - StationAiIconBliss
+  - StationAiIconClown
+  - StationAiIconDorf
+  - StationAiIconHeartline
+  - StationAiIconSmiley
+
+- type: stationAiCustomizationGroup
+  id: StationAiHolograms
+  name: station-ai-customization-hologram
+  category: Hologram
+  protoIds:
+  - StationAiHologramFemale
+  - StationAiHologramMale
+  - StationAiHologramFace
+  - StationAiHologramCat
+  - StationAiHologramDog
+
+# Iconography
+- type: stationAiCustomization
+  abstract: true
+  id: StationAiIconBase
+  previewKey: Occupied
+  previewBackground:
+    sprite: Mobs/Silicon/station_ai.rsi
+    state: base
+
+- type: stationAiCustomization
+  parent: StationAiIconBase
+  id: StationAiIconAi
+  name: station-ai-icon-ai
+  layerData:
+    Occupied:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai
+    Dead:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai_dead
+
+- type: stationAiCustomization
+  parent: StationAiIconBase
+  id: StationAiIconAngel
+  name: station-ai-icon-angel
+  layerData:
+    Occupied:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai_angel
+    Dead:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai_angel_dead
+
+- type: stationAiCustomization
+  parent: StationAiIconBase
+  id: StationAiIconBliss
+  name: station-ai-icon-bliss
+  layerData:
+    Occupied:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai_bliss
+    Dead:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai_dead
+
+- type: stationAiCustomization
+  parent: StationAiIconBase
+  id: StationAiIconClown
+  name: station-ai-icon-clown
+  layerData:
+    Occupied:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai_clown
+    Dead:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai_clown_dead
+
+- type: stationAiCustomization
+  parent: StationAiIconBase
+  id: StationAiIconDorf
+  name: station-ai-icon-dorf
+  layerData:
+    Occupied:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai_dorf
+    Dead:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: ai_dead
+
+- type: stationAiCustomization
+  parent: StationAiIconBase
+  id: StationAiIconHeartline
+  name: station-ai-icon-heartline
+  layerData:
+    Occupied:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: "ai_heartline"
+    Dead:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: "ai_heartline_dead"
+
+- type: stationAiCustomization
+  parent: StationAiIconBase
+  id: StationAiIconSmiley
+  name: station-ai-icon-smiley
+  layerData:
+    Occupied:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: "ai_smiley"
+    Dead:
+      sprite: Mobs/Silicon/station_ai.rsi
+      state: "ai_dead"
+
+# Holograms
+- type: stationAiCustomization
+  id: StationAiHologramFemale
+  name: station-ai-hologram-female
+  previewKey: Hologram
+  layerData:
+    Hologram:
+      sprite: Mobs/Silicon/holograms.rsi
+      state: ai_female
+
+- type: stationAiCustomization
+  id: StationAiHologramMale
+  name: station-ai-hologram-male
+  previewKey: Hologram
+  layerData:
+    Hologram:
+      sprite: Mobs/Silicon/holograms.rsi
+      state: ai_male
+
+- type: stationAiCustomization
+  id: StationAiHologramFace
+  name: station-ai-hologram-face
+  previewKey: Hologram
+  layerData:
+    Hologram:
+      sprite: Mobs/Silicon/holograms.rsi
+      state: ai_face
+
+- type: stationAiCustomization
+  id: StationAiHologramCat
+  name: station-ai-hologram-cat
+  previewKey: Hologram
+  layerData:
+    Hologram:
+      sprite: Mobs/Silicon/holograms.rsi
+      state: ai_cat
+
+- type: stationAiCustomization
+  id: StationAiHologramDog
+  name: station-ai-hologram-dog
+  previewKey: Hologram
+  layerData:
+    Hologram:
+      sprite: Mobs/Silicon/holograms.rsi
+      state: ai_dog
index 9a1a4c88cf4694ddc3269b785e7904abac78a81b..d70c13187f3f30c1ebfbf4a68182b46457443361 100644 (file)
     canShuttle: false
     title: comms-console-announcement-title-station-ai
     color: "#5ed7aa"
-  - type: HolographicAvatar
-    layerData:
-    - sprite: Mobs/Silicon/station_ai.rsi
-      state: default
   - type: ShowJobIcons
 
 - type: entity
     layers:
     - state: base
     - state: ai_empty
-      map: ["unshaded"]
       shader: unshaded
+    - state: ai
+      map: ["enum.StationAiVisualState.Key"]
+      shader: unshaded
+      visible: false
   - type: Appearance
   - type: InteractionPopup
     interactSuccessString: petting-success-station-ai
     messagePerceivedByOthers: petting-success-station-ai-others # Otherwise AI cannot tell its being pet as It's just a brain inside of the core, not the core itself.
     interactSuccessSound:
       path: /Audio/Ambience/Objects/periodic_beep.ogg
-  - type: GenericVisualizer
-    visuals:
-      enum.StationAiVisualState.Key:
-        unshaded:
-          Empty: { state: ai_empty }
-          Occupied: { state: ai }
   - type: Telephone
     compatibleRanges:
     - Grid
   - type: StationAiWhitelist
   - type: UserInterface
     interfaces:
-        enum.HolopadUiKey.AiRequestWindow:
-          type: HolopadBoundUserInterface
-        enum.HolopadUiKey.AiActionWindow:
-          type: HolopadBoundUserInterface
+      enum.HolopadUiKey.AiRequestWindow:
+        type: HolopadBoundUserInterface
+      enum.HolopadUiKey.AiActionWindow:
+        type: HolopadBoundUserInterface
+      enum.StationAiCustomizationUiKey.Key:
+        type: StationAiCustomizationBoundUserInterface
 
 # The job-ready version of an AI spawn.
 - type: entity
   - type: ContainerSpawnPoint
     containerId: station_ai_mind_slot
     job: StationAi
-  - type: Sprite
-    sprite: Mobs/Silicon/station_ai.rsi
-    layers:
-    - state: base
-    - state: ai
-      shader: unshaded
 
 # The actual brain inside the core
 - type: entity
   - type: StartingMindRole
     mindRole: "MindRoleSiliconBrain"
     silent: true
+  - type: StationAiCustomization
+    protoIds:
+      StationAiCoreIconography: StationAiIconAi
+      StationAiHolograms: StationAiHologramFemale
+  - type: HolographicAvatar
+    layerData:
+    - sprite: Mobs/Silicon/holograms.rsi
+      state: ai_female
   - type: NameIdentifier
     group: StationAi
 
diff --git a/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_cat.png b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_cat.png
new file mode 100644 (file)
index 0000000..6413df0
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_cat.png differ
diff --git a/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_dog.png b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_dog.png
new file mode 100644 (file)
index 0000000..de652a8
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_dog.png differ
diff --git a/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_face.png b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_face.png
new file mode 100644 (file)
index 0000000..f3a12a5
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_face.png differ
diff --git a/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_female.png b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_female.png
new file mode 100644 (file)
index 0000000..c871354
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_female.png differ
diff --git a/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_male.png b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_male.png
new file mode 100644 (file)
index 0000000..aedd762
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/holograms.rsi/ai_male.png differ
diff --git a/Resources/Textures/Mobs/Silicon/holograms.rsi/meta.json b/Resources/Textures/Mobs/Silicon/holograms.rsi/meta.json
new file mode 100644 (file)
index 0000000..8eaf916
--- /dev/null
@@ -0,0 +1,38 @@
+{
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/blob/e2923df112df8aa025846d0764697bad6506586a/icons/mob/AI.dmi - modified by chromiumboy.",
+    "size": {
+        "x": 32,
+        "y": 32
+    },
+    "states": [
+        {
+            "name": "ai_female"
+        },
+        {
+            "name": "ai_male"
+        },
+        {
+            "name": "ai_face",
+            "delays": [
+                [
+                    2.3,
+                    0.2
+                ]
+            ]
+        },
+        {
+            "name": "ai_cat",
+            "delays": [
+                [
+                    0.75,
+                    0.75
+                ]
+            ]
+        },
+        {
+            "name": "ai_dog"
+        }
+    ]
+}
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_angel.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_angel.png
new file mode 100644 (file)
index 0000000..fd9a601
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_angel.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_angel_dead.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_angel_dead.png
new file mode 100644 (file)
index 0000000..2d959b8
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_angel_dead.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_bliss.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_bliss.png
new file mode 100644 (file)
index 0000000..d087d3b
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_bliss.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_clown.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_clown.png
new file mode 100644 (file)
index 0000000..842b3f4
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_clown.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_clown_dead.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_clown_dead.png
new file mode 100644 (file)
index 0000000..1f1050e
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_clown_dead.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dorf.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dorf.png
new file mode 100644 (file)
index 0000000..e25aa6d
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_dorf.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_heartline.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_heartline.png
new file mode 100644 (file)
index 0000000..d4518fd
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_heartline.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_heartline_dead.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_heartline_dead.png
new file mode 100644 (file)
index 0000000..4b9f3f9
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_heartline_dead.png differ
diff --git a/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_smiley.png b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_smiley.png
new file mode 100644 (file)
index 0000000..6e3c3e7
Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/station_ai.rsi/ai_smiley.png differ
index c8d174c57bfc3bb04d9dd67d08e166a4c9bb99ec..bdce787a4898c2d36bcbecb61db30fb601da3dcb 100644 (file)
 {
-  "version": 1,
-  "license": "CC-BY-SA-3.0",
-  "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/blob/2a19963297f91efb452dbb5c1d4eb28a14776b0a/icons/mob/silicon/ai.dmi",
-  "size": {
-    "x": 32,
-    "y": 32
-  },
-  "states": [
-    {
-      "name": "ai",
-      "delays": [
-        [
-          0.2,
-          0.2,
-          0.1,
-          0.2,
-          0.2,
-          0.2,
-          0.2,
-          0.2,
-          0.2,
-          0.2,
-          0.2,
-          0.2,
-          0.2,
-          0.2,
-          0.1
-        ]
-      ]
+    "version": 1,
+    "license": "CC-BY-SA-3.0",
+    "copyright": "Taken from vgstation at https://github.com/vgstation-coders/vgstation13/blob/e2923df112df8aa025846d0764697bad6506586a/icons/mob/AI.dmi - modified by chromiumboy.",
+    "size": {
+        "x": 32,
+        "y": 32
     },
-    {
-      "name": "ai_camera",
-      "delays": [
-        [
-          1.0,
-          1.0
-        ]
-      ]
-    },
-    {
-      "name": "ai_dead"
-    },
-    {
-      "name": "ai_empty",
-      "delays": [
-        [
-          0.7,
-          0.7
-        ]
-      ]
-    },
-    {
-      "name": "default",
-      "directions": 4
-    },
-    {
-      "name": "base"
-    }
-  ]
-}
\ No newline at end of file
+    "states": [
+        {
+            "name": "ai",
+            "delays": [
+                [
+                    0.2,
+                    0.2,
+                    0.1,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.1
+                ]
+            ]
+        },
+        {
+            "name": "ai_angel",
+            "delays": [
+                [
+                    0.08,
+                    0.08,
+                    0.08,
+                    0.08,
+                    0.08,
+                    0.08
+                ]
+            ]
+        },
+        {
+            "name": "ai_angel_dead",
+            "delays": [
+                [
+                    1.00,
+                    0.08,
+                    0.50,
+                    0.20
+                ]
+            ]
+        },
+        {
+            "name": "ai_bliss"
+        },
+        {
+            "name": "ai_clown",
+            "delays": [
+                [
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2
+                ]
+            ]
+        },
+        {
+            "name": "ai_clown_dead",
+            "delays": [
+                [
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2,
+                    0.2
+                ]
+            ]
+        },
+        {
+            "name": "ai_dorf",
+            "delays": [
+                [
+                    0.5,
+                    0.5
+                ]
+            ]
+        },
+        {
+            "name": "ai_heartline",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },
+        {
+            "name": "ai_heartline_dead",
+            "delays": [
+                [
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15,
+                    0.15
+                ]
+            ]
+        },
+        {
+            "name": "ai_smiley",
+            "delays": [
+                [
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1,
+                    0.1
+                ]
+            ]
+        },
+        {
+            "name": "ai_camera",
+            "delays": [
+                [
+                    1.0,
+                    1.0
+                ]
+            ]
+        },
+        {
+            "name": "ai_dead"
+        },
+        {
+            "name": "ai_empty",
+            "delays": [
+                [
+                    0.7,
+                    0.7
+                ]
+            ]
+        },
+        {
+            "name": "default",
+            "directions": 4
+        },
+        {
+            "name": "base"
+        }
+    ]
+}