diff --git a/Content.Client/Silicons/StationAi/StationAiVisualizerSystem.cs b/Content.Client/Silicons/StationAi/StationAiVisualizerSystem.cs new file mode 100644 index 0000000000..475e31d91f --- /dev/null +++ b/Content.Client/Silicons/StationAi/StationAiVisualizerSystem.cs @@ -0,0 +1,173 @@ +using Content.Shared.Silicons.StationAi; +using Robust.Client.Animations; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; + +namespace Content.Client.Silicons.StationAi; + +public sealed partial class StationAiVisualizerSystem : VisualizerSystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnAnimationCompleted); + } + + private static readonly Animation CoreUpAnimation = new() + { + Length = TimeSpan.FromSeconds(0.8f), + AnimationTracks = + { + new AnimationTrackSpriteFlick + { + LayerKey = StationAiVisualLayers.CoreStanding, + KeyFrames = + { + new AnimationTrackSpriteFlick.KeyFrame(new RSI.StateId("up"), 0f) + } + }, + new AnimationTrackSpriteFlick + { + LayerKey = StationAiVisualLayers.ScreenStanding, + KeyFrames = + { + new AnimationTrackSpriteFlick.KeyFrame(new RSI.StateId("ai_up"), 0f) + } + } + } + }; + + private static readonly Animation CoreDownAnimation = new() + { + Length = TimeSpan.FromSeconds(0.8f), + AnimationTracks = + { + new AnimationTrackSpriteFlick + { + LayerKey = StationAiVisualLayers.CoreStanding, + KeyFrames = + { + new AnimationTrackSpriteFlick.KeyFrame(new RSI.StateId("down"), 0f) + } + }, + new AnimationTrackSpriteFlick + { + LayerKey = StationAiVisualLayers.ScreenStanding, + KeyFrames = + { + new AnimationTrackSpriteFlick.KeyFrame(new RSI.StateId("ai_down"), 0f) + } + } + } + }; + + protected override void OnAppearanceChange(EntityUid uid, StationAiCoreComponent component, ref AppearanceChangeEvent args) + { + if (args.Sprite == null) + return; + if (!TryComp(uid, out var animPlayer)) + return; + + if (!AppearanceSystem.TryGetData(uid, StationAiVisualState.Key, out var state, args.Component)) + state = StationAiState.Empty; + + UpdateVisuals(uid, state, component, args.Sprite, animPlayer); + } + + private void OnAnimationCompleted(EntityUid uid, StationAiCoreComponent comp, AnimationCompletedEvent args) + { + if (args.Key != StationAiCoreComponent.AnimationKey) + return; + if (!TryComp(uid, out var sprite)) + return; + if (!TryComp(uid, out var animPlayer)) + return; + + if (!AppearanceSystem.TryGetData(uid, StationAiVisualState.Key, out var state)) + state = comp.CurrentState; + + // Convert to finished state + StationAiState targetState; + switch (state) + { + case StationAiState.Up: + targetState = StationAiState.Standing; + break; + case StationAiState.Down: + targetState = StationAiState.Occupied; + break; + default: + targetState = state; + break; + } + + UpdateVisuals(uid, targetState, comp, sprite, animPlayer); + } + + private void UpdateVisuals(EntityUid uid, StationAiState state, StationAiCoreComponent comp, SpriteComponent sprite, AnimationPlayerComponent? animPlayer = null) + { + if (state == comp.CurrentState) + return; + if (!Resolve(uid, ref animPlayer)) + return; + if (AnimationSystem.HasRunningAnimation(uid, animPlayer, StationAiCoreComponent.AnimationKey)) + return; + + var targetState = state; + if (comp.CurrentState == StationAiState.Occupied && state == StationAiState.Standing) + targetState = StationAiState.Up; // play standing up animation first + else if (comp.CurrentState == StationAiState.Standing && state == StationAiState.Occupied) + targetState = StationAiState.Down; // play sitting down animation first + + comp.CurrentState = targetState; + + switch (targetState) + { + case StationAiState.Empty: + sprite.LayerSetVisible(StationAiVisualLayers.Core, true); + sprite.LayerSetVisible(StationAiVisualLayers.Screen, true); + sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, false); + sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, false); + sprite.LayerSetState(StationAiVisualLayers.Screen, new RSI.StateId("ai_empty")); + break; + case StationAiState.Occupied: + sprite.LayerSetVisible(StationAiVisualLayers.Core, true); + sprite.LayerSetVisible(StationAiVisualLayers.Screen, true); + sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, false); + sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, false); + sprite.LayerSetState(StationAiVisualLayers.Screen, new RSI.StateId("ai")); + break; + case StationAiState.Dead: + sprite.LayerSetVisible(StationAiVisualLayers.Core, true); + sprite.LayerSetVisible(StationAiVisualLayers.Screen, true); + sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, false); + sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, false); + sprite.LayerSetState(StationAiVisualLayers.Screen, new RSI.StateId("ai_dead")); + break; + case StationAiState.Up: + sprite.LayerSetVisible(StationAiVisualLayers.Core, false); + sprite.LayerSetVisible(StationAiVisualLayers.Screen, false); + sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, true); + sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, true); + AnimationSystem.Play((uid, animPlayer), CoreUpAnimation, StationAiCoreComponent.AnimationKey); + break; + case StationAiState.Down: + sprite.LayerSetVisible(StationAiVisualLayers.Core, false); + sprite.LayerSetVisible(StationAiVisualLayers.Screen, false); + sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, true); + sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, true); + AnimationSystem.Play((uid, animPlayer), CoreDownAnimation, StationAiCoreComponent.AnimationKey); + break; + case StationAiState.Standing: + sprite.LayerSetVisible(StationAiVisualLayers.Core, false); + sprite.LayerSetVisible(StationAiVisualLayers.Screen, false); + sprite.LayerSetVisible(StationAiVisualLayers.CoreStanding, true); + sprite.LayerSetVisible(StationAiVisualLayers.ScreenStanding, true); + sprite.LayerSetState(StationAiVisualLayers.CoreStanding, new RSI.StateId("base_high")); + sprite.LayerSetState(StationAiVisualLayers.ScreenStanding, new RSI.StateId("ai_high")); + break; + default: + break; + } + } +} diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs index 8acfb56376..bd19f300db 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.Held.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.Actions.Events; using Content.Shared.IdentityManagement; using Content.Shared.Interaction.Events; +using Content.Shared.Mind.Components; using Content.Shared.Popups; using Content.Shared.Verbs; using Robust.Shared.Serialization; @@ -27,6 +28,8 @@ public abstract partial class SharedStationAiSystem SubscribeLocalEvent(OnHeldInteraction); SubscribeLocalEvent(OnHeldRelay); SubscribeLocalEvent(OnCoreJump); + SubscribeLocalEvent(OnCoreVisit); + SubscribeLocalEvent(OnCoreUnVisit); SubscribeLocalEvent(OnTryGetIdentityShortInfo); } @@ -50,7 +53,46 @@ public abstract partial class SharedStationAiSystem if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null) return; - _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner) ; + _xforms.DropNextTo(core.Comp.RemoteEntity.Value, core.Owner); + } + + private void OnCoreVisit(Entity ent, ref VisitCoreEvent args) + { + if (!TryGetCore(ent.Owner, out var core) || core.Comp?.RemoteEntity == null) + return; + + if (!_mind.TryGetMind(ent.Owner, out var mindId, out var mind)) + return; + + // move the player's mind to the core + _mind.Visit(mindId, core, mind); + _xforms.Unanchor(core); + + if (!TryComp(core, out var app)) + return; + + _appearance.SetData(core, StationAiVisualState.Key, StationAiState.Standing, app); + } + + private void OnCoreUnVisit(Entity ent, ref UnVisitCoreEvent args) + { + if (!Transform(ent.Owner).Anchored && !_xforms.AnchorEntity(ent.Owner)) + return; + + // move the player's mind back to the camera + if (ent.Comp.MindId != null) + _mind.UnVisit(ent.Comp.MindId.Value); + + if (!TryComp(ent.Owner, out StationAiCoreComponent? coreComp) || coreComp.RemoteEntity == null) + return; + + // move the camera back to the core + _xforms.DropNextTo(coreComp.RemoteEntity.Value, ent.Owner); + + if (!TryComp(ent.Owner, out var app)) + return; + + _appearance.SetData(ent.Owner, StationAiVisualState.Key, StationAiState.Occupied, app); } /// @@ -164,7 +206,7 @@ public abstract partial class SharedStationAiSystem var verb = new AlternativeVerb { Text = isOpen ? Loc.GetString("ai-close") : Loc.GetString("ai-open"), - Act = () => + Act = () => { // no need to show menu if device is not powered. if (!PowerReceiver.IsPowered(ent.Owner)) diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs index 4cf36f560e..47a520be13 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs @@ -460,6 +460,9 @@ public abstract partial class SharedStationAiSystem : EntitySystem private void OnAiRemove(Entity ent, ref EntRemovedFromContainerMessage args) { + if (args.Container.ID != StationAiCoreComponent.Container) + return; + if (_timing.ApplyingState) return; @@ -538,6 +541,16 @@ public sealed partial class JumpToCoreEvent : InstantActionEvent } +public sealed partial class VisitCoreEvent : InstantActionEvent +{ + +} + +public sealed partial class UnVisitCoreEvent : InstantActionEvent +{ + +} + [Serializable, NetSerializable] public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent; @@ -548,10 +561,21 @@ public enum StationAiVisualState : byte Key, } +public enum StationAiVisualLayers : byte +{ + Core, + Screen, + CoreStanding, + ScreenStanding, +} + [Serializable, NetSerializable] public enum StationAiState : byte { Empty, Occupied, Dead, + Up, + Down, + Standing, } diff --git a/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs b/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs index e97e7626b8..1a76076c51 100644 --- a/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs +++ b/Content.Shared/Silicons/StationAi/StationAiCoreComponent.cs @@ -39,4 +39,16 @@ public sealed partial class StationAiCoreComponent : Component public EntProtoId? PhysicalEntityProto = "StationAiHoloLocal"; public const string Container = "station_ai_mind_slot"; + + /// + /// The key used to index the leggy animation. + /// + [ViewVariables] + public const string AnimationKey = "leggy_animation"; + + /// + /// The current visual state of the core. + /// + [ViewVariables] + public StationAiState CurrentState = StationAiState.Empty; } diff --git a/Resources/Prototypes/Actions/station_ai.yml b/Resources/Prototypes/Actions/station_ai.yml index 67d5626219..3d70c012b9 100644 --- a/Resources/Prototypes/Actions/station_ai.yml +++ b/Resources/Prototypes/Actions/station_ai.yml @@ -12,6 +12,36 @@ state: ai_core event: !type:JumpToCoreEvent +- type: entity + id: ActionVisitCore + name: Leggy + description: Move around using your legs. + components: + - type: InstantAction + priority: -20 + itemIconStyle: BigAction + icon: + sprite: Interface/Actions/actions_ai.rsi + state: leg + event: !type:VisitCoreEvent + useDelay: 2 + startDelay: true + +- type: entity + id: ActionUnVisitCore + name: No Leggy + description: Return back to the remote. + components: + - type: InstantAction + priority: -20 + itemIconStyle: BigAction + icon: + sprite: Interface/Actions/actions_ai.rsi + state: leg + event: !type:UnVisitCoreEvent + useDelay: 2 + startDelay: true + - type: entity id: ActionSurvCameraLights name: Toggle camera lights diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml index 0769972512..440789ee4a 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml @@ -34,6 +34,7 @@ - type: ActionGrant actions: - ActionJumpToCore + - ActionVisitCore - ActionSurvCameraLights - ActionAIViewLaws - type: UserInterface @@ -325,10 +326,13 @@ name: AI Core description: The latest in Artificial Intelligences. parent: + - BaseMob - BaseStructure - AiHolder suffix: Empty components: + - type: Transform + noRot: true - type: WarpPoint - type: ContainerComp proto: AiHeld @@ -350,25 +354,77 @@ - type: StationAiVision - type: InteractionOutline - type: Sprite - sprite: Mobs/Silicon/station_ai.rsi + noRot: true layers: - state: base + sprite: Mobs/Silicon/station_ai.rsi + map: ["enum.StationAiVisualLayers.Core"] - state: ai_empty - map: ["unshaded"] + sprite: Mobs/Silicon/station_ai.rsi shader: unshaded + map: ["enum.StationAiVisualLayers.Screen"] + - state: base_high + sprite: Mobs/Silicon/leggy.rsi + map: ["enum.StationAiVisualLayers.CoreStanding"] + offset: "0.0,0.5" + visible: false + - state: ai_high + sprite: Mobs/Silicon/leggy.rsi + shader: unshaded + map: ["enum.StationAiVisualLayers.ScreenStanding"] + offset: "0.0,0.5" + visible: false - type: Appearance + - type: AnimationPlayer + - type: ActionGrant + actions: + - ActionUnVisitCore + - type: Anchorable + flags: + - None + - type: Physics + bodyType: Static + - type: Tag + tags: + - DoorBumpOpener + - FootstepSound + - Structure + - type: Access + groups: + - AllAccess + - type: IntrinsicRadioReceiver + - type: IntrinsicRadioTransmitter + channels: + - Binary + - Common + - Command + - Engineering + - Medical + - Science + - Security + - Service + - Supply + - type: ActiveRadio + channels: + - Binary + - Common + - Command + - Engineering + - Medical + - Science + - Security + - Service + - Supply + - type: ShowJobIcons + - type: Speech + speechVerb: Robotic + speechSounds: Borg - type: InteractionPopup interactSuccessString: petting-success-station-ai interactFailureString: petting-failure-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 @@ -396,12 +452,6 @@ - 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 diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/leg.png b/Resources/Textures/Interface/Actions/actions_ai.rsi/leg.png new file mode 100644 index 0000000000..13bcfcbe33 Binary files /dev/null and b/Resources/Textures/Interface/Actions/actions_ai.rsi/leg.png differ diff --git a/Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json b/Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json index 434b9052e0..0f33441961 100644 --- a/Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json +++ b/Resources/Textures/Interface/Actions/actions_ai.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/blob/c473a8bcc28fbd80827dfca5660d81ca6e833e2c/icons/hud/screen_ai.dmi , some sprites updated by ScarKy0(Discord), door actions by ScarKy0(Discord) and @Max_tanuki(Twitter)", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/blob/c473a8bcc28fbd80827dfca5660d81ca6e833e2c/icons/hud/screen_ai.dmi , some sprites updated by ScarKy0(Discord), door actions by ScarKy0(Discord) and @Max_tanuki(Twitter), leg made by muburu_ (Dicord) and adjusted by slarticodefast", "size": { "x": 32, "y": 32 @@ -48,6 +48,9 @@ }, { "name": "door_overcharge_off" + }, + { + "name": "leg" } ] } diff --git a/Resources/Textures/Mobs/Silicon/leggy.rsi/ai_down.png b/Resources/Textures/Mobs/Silicon/leggy.rsi/ai_down.png new file mode 100644 index 0000000000..04fea30464 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/leggy.rsi/ai_down.png differ diff --git a/Resources/Textures/Mobs/Silicon/leggy.rsi/ai_high.png b/Resources/Textures/Mobs/Silicon/leggy.rsi/ai_high.png new file mode 100644 index 0000000000..f822b7d480 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/leggy.rsi/ai_high.png differ diff --git a/Resources/Textures/Mobs/Silicon/leggy.rsi/ai_up.png b/Resources/Textures/Mobs/Silicon/leggy.rsi/ai_up.png new file mode 100644 index 0000000000..54e1fb8d91 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/leggy.rsi/ai_up.png differ diff --git a/Resources/Textures/Mobs/Silicon/leggy.rsi/base_high.png b/Resources/Textures/Mobs/Silicon/leggy.rsi/base_high.png new file mode 100644 index 0000000000..135ea9b994 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/leggy.rsi/base_high.png differ diff --git a/Resources/Textures/Mobs/Silicon/leggy.rsi/down.png b/Resources/Textures/Mobs/Silicon/leggy.rsi/down.png new file mode 100644 index 0000000000..bc9f3e98bb Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/leggy.rsi/down.png differ diff --git a/Resources/Textures/Mobs/Silicon/leggy.rsi/meta.json b/Resources/Textures/Mobs/Silicon/leggy.rsi/meta.json new file mode 100644 index 0000000000..459aa80b77 --- /dev/null +++ b/Resources/Textures/Mobs/Silicon/leggy.rsi/meta.json @@ -0,0 +1,96 @@ +{ + "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, leg sprites made by muburu_ (Dicord) and adjusted by slarticodefast", + "size": { + "x": 32, + "y": 64 + }, + "states": [ + { + "name": "ai_high", + "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": "base_high" + }, + { + "name": "down", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ai_down", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "up", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + }, + { + "name": "ai_up", + "delays": [ + [ + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1, + 0.1 + ] + ] + } + ] +} diff --git a/Resources/Textures/Mobs/Silicon/leggy.rsi/up.png b/Resources/Textures/Mobs/Silicon/leggy.rsi/up.png new file mode 100644 index 0000000000..dedd7382d5 Binary files /dev/null and b/Resources/Textures/Mobs/Silicon/leggy.rsi/up.png differ