[April Fools] Leggy (#36133)

* leggy

* shader

* fix visuals for other clients

* static
This commit is contained in:
slarticodefast 2025-03-29 12:35:04 +01:00 • committed by GitHub
parent c42279a003
commit 52c4ee954c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 447 additions and 17 deletions

View file

@ -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<StationAiCoreComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationAiCoreComponent, AnimationCompletedEvent>(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<AnimationPlayerComponent>(uid, out var animPlayer))
return;
if (!AppearanceSystem.TryGetData<StationAiState>(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<SpriteComponent>(uid, out var sprite))
return;
if (!TryComp<AnimationPlayerComponent>(uid, out var animPlayer))
return;
if (!AppearanceSystem.TryGetData<StationAiState>(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;
}
}
}

View file

@ -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<StationAiHeldComponent, InteractionAttemptEvent>(OnHeldInteraction);
SubscribeLocalEvent<StationAiHeldComponent, AttemptRelayActionComponentChangeEvent>(OnHeldRelay);
SubscribeLocalEvent<StationAiHeldComponent, JumpToCoreEvent>(OnCoreJump);
SubscribeLocalEvent<StationAiHeldComponent, VisitCoreEvent>(OnCoreVisit);
SubscribeLocalEvent<VisitingMindComponent, UnVisitCoreEvent>(OnCoreUnVisit);
SubscribeLocalEvent<TryGetIdentityShortInfoEvent>(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<StationAiHeldComponent> 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<AppearanceComponent>(core, out var app))
return;
_appearance.SetData(core, StationAiVisualState.Key, StationAiState.Standing, app);
}
private void OnCoreUnVisit(Entity<VisitingMindComponent> 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<AppearanceComponent>(ent.Owner, out var app))
return;
_appearance.SetData(ent.Owner, StationAiVisualState.Key, StationAiState.Occupied, app);
}
/// <summary>
@ -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))

View file

@ -460,6 +460,9 @@ public abstract partial class SharedStationAiSystem : EntitySystem
private void OnAiRemove(Entity<StationAiCoreComponent> 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,
}

View file

@ -39,4 +39,16 @@ public sealed partial class StationAiCoreComponent : Component
public EntProtoId? PhysicalEntityProto = "StationAiHoloLocal";
public const string Container = "station_ai_mind_slot";
/// <summary>
/// The key used to index the leggy animation.
/// </summary>
[ViewVariables]
public const string AnimationKey = "leggy_animation";
/// <summary>
/// The current visual state of the core.
/// </summary>
[ViewVariables]
public StationAiState CurrentState = StationAiState.Empty;
}

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB