parent
76c415380a
commit
de42819c0b
|
|
@ -0,0 +1,11 @@
|
|||
<tips:ClippyUI xmlns="https://spacestation14.io"
|
||||
xmlns:tips="clr-namespace:Content.Client.Tips"
|
||||
MinSize="64 64"
|
||||
Visible="False">
|
||||
<PanelContainer Name="LabelPanel" Access="Public" Visible="False" MaxWidth="300" MaxHeight="200">
|
||||
<ScrollContainer Name="ScrollingContents" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" HorizontalExpand="True" VerticalExpand="True" HScrollEnabled="False" ReturnMeasure="True">
|
||||
<RichTextLabel Name="Label" Access="Public"/>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
<SpriteView Name="Entity" Access="Public" MinSize="128 128"/>
|
||||
</tips:ClippyUI>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
using Content.Client.Paper;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Tips;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ClippyUI : UIWidget
|
||||
{
|
||||
public ClippyState State = ClippyState.Hidden;
|
||||
public bool ModifyLayers = true;
|
||||
|
||||
public ClippyUI()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void InitLabel(PaperVisualsComponent? visuals, IResourceCache resCache)
|
||||
{
|
||||
if (visuals == null)
|
||||
return;
|
||||
|
||||
Label.ModulateSelfOverride = visuals.FontAccentColor;
|
||||
|
||||
if (visuals.BackgroundImagePath == null)
|
||||
return;
|
||||
|
||||
LabelPanel.ModulateSelfOverride = visuals.BackgroundModulate;
|
||||
var backgroundImage = resCache.GetResource<TextureResource>(visuals.BackgroundImagePath);
|
||||
var backgroundImageMode = visuals.BackgroundImageTile ? StyleBoxTexture.StretchMode.Tile : StyleBoxTexture.StretchMode.Stretch;
|
||||
var backgroundPatchMargin = visuals.BackgroundPatchMargin;
|
||||
LabelPanel.PanelOverride = new StyleBoxTexture
|
||||
{
|
||||
Texture = backgroundImage,
|
||||
TextureScale = visuals.BackgroundScale,
|
||||
Mode = backgroundImageMode,
|
||||
PatchMarginLeft = backgroundPatchMargin.Left,
|
||||
PatchMarginBottom = backgroundPatchMargin.Bottom,
|
||||
PatchMarginRight = backgroundPatchMargin.Right,
|
||||
PatchMarginTop = backgroundPatchMargin.Top
|
||||
};
|
||||
}
|
||||
|
||||
public enum ClippyState : byte
|
||||
{
|
||||
Hidden,
|
||||
Revealing,
|
||||
Speaking,
|
||||
Hiding,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
using Content.Client.Gameplay;
|
||||
using System.Numerics;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.Paper;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Tips;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.Audio;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using static Content.Client.Tips.ClippyUI;
|
||||
|
||||
namespace Content.Client.Tips;
|
||||
|
||||
public sealed class ClippyUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly IStateManager _state = default!;
|
||||
[Dependency] private readonly IConsoleHost _conHost = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IResourceCache _resCache = default!;
|
||||
[UISystemDependency] private readonly AudioSystem _audio = default!;
|
||||
|
||||
public const float Padding = 50;
|
||||
public static Angle WaddleRotation = Angle.FromDegrees(10);
|
||||
|
||||
private EntityUid _entity;
|
||||
private float _secondsUntilNextState;
|
||||
private int _previousStep = 0;
|
||||
private ClippyEvent? _currentMessage;
|
||||
private readonly Queue<ClippyEvent> _queuedMessages = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_conHost.RegisterCommand("local_clippy", ClippyCommand);
|
||||
UIManager.OnScreenChanged += OnScreenChanged;
|
||||
}
|
||||
|
||||
private void ClippyCommand(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteLine("usage: clippy <message> [entity prototype] [speak time] [animate time] [waddle]");
|
||||
return;
|
||||
}
|
||||
|
||||
var ev = new ClippyEvent(args[0]);
|
||||
|
||||
string proto;
|
||||
if (args.Length > 1)
|
||||
{
|
||||
ev.Proto = args[1];
|
||||
|
||||
if (!_protoMan.HasIndex<EntityPrototype>(ev.Proto))
|
||||
{
|
||||
shell.WriteError($"Unknown prototype: {ev.Proto}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (args.Length > 2)
|
||||
ev.SpeakTime = float.Parse(args[2]);
|
||||
|
||||
if (args.Length > 3)
|
||||
ev.SlideTime = float.Parse(args[3]);
|
||||
|
||||
if (args.Length > 4)
|
||||
ev.WaddleInterval = float.Parse(args[4]);
|
||||
|
||||
AddMessage(ev);
|
||||
}
|
||||
|
||||
public void AddMessage(ClippyEvent ev)
|
||||
{
|
||||
_queuedMessages.Enqueue(ev);
|
||||
}
|
||||
|
||||
public override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
var screen = UIManager.ActiveScreen;
|
||||
if (screen == null)
|
||||
{
|
||||
_queuedMessages.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
var clippy = screen.GetOrAddWidget<ClippyUI>();
|
||||
_secondsUntilNextState -= args.DeltaSeconds;
|
||||
|
||||
if (_secondsUntilNextState <= 0)
|
||||
NextState(clippy);
|
||||
else
|
||||
{
|
||||
var pos = UpdatePosition(clippy, screen.Size, args); ;
|
||||
LayoutContainer.SetPosition(clippy, pos);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 UpdatePosition(ClippyUI clippy, Vector2 screenSize, FrameEventArgs args)
|
||||
{
|
||||
if (_currentMessage == null)
|
||||
return default;
|
||||
|
||||
var slideTime = _currentMessage.SlideTime;
|
||||
|
||||
var offset = clippy.State switch
|
||||
{
|
||||
ClippyState.Hidden => 0,
|
||||
ClippyState.Revealing => Math.Clamp(1 - _secondsUntilNextState / slideTime, 0, 1),
|
||||
ClippyState.Hiding => Math.Clamp(_secondsUntilNextState / slideTime, 0, 1),
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
var waddle = _currentMessage.WaddleInterval;
|
||||
|
||||
if (_currentMessage == null
|
||||
|| waddle <= 0
|
||||
|| clippy.State == ClippyState.Hidden
|
||||
|| clippy.State == ClippyState.Speaking
|
||||
|| !EntityManager.TryGetComponent(_entity, out SpriteComponent? sprite))
|
||||
{
|
||||
return new Vector2(screenSize.X - offset * (clippy.DesiredSize.X + Padding), (screenSize.Y - clippy.DesiredSize.Y) / 2);
|
||||
}
|
||||
|
||||
var numSteps = (int) Math.Ceiling(slideTime / waddle);
|
||||
var curStep = (int) Math.Floor(numSteps * offset);
|
||||
var stepSize = (clippy.DesiredSize.X + Padding) / numSteps;
|
||||
|
||||
if (curStep != _previousStep)
|
||||
{
|
||||
_previousStep = curStep;
|
||||
sprite.Rotation = sprite.Rotation > 0
|
||||
? -WaddleRotation
|
||||
: WaddleRotation;
|
||||
|
||||
if (EntityManager.TryGetComponent(_entity, out FootstepModifierComponent? step))
|
||||
{
|
||||
var audioParams = step.FootstepSoundCollection.Params
|
||||
.AddVolume(-7f)
|
||||
.WithVariation(0.1f);
|
||||
_audio.PlayGlobal(step.FootstepSoundCollection, EntityUid.Invalid, audioParams);
|
||||
}
|
||||
}
|
||||
|
||||
return new Vector2(screenSize.X - stepSize * curStep, (screenSize.Y - clippy.DesiredSize.Y) / 2);
|
||||
}
|
||||
|
||||
private void NextState(ClippyUI clippy)
|
||||
{
|
||||
SpriteComponent? sprite;
|
||||
switch (clippy.State)
|
||||
{
|
||||
case ClippyState.Hidden:
|
||||
if (!_queuedMessages.TryDequeue(out var next))
|
||||
return;
|
||||
|
||||
if (next.Proto != null)
|
||||
{
|
||||
_entity = EntityManager.SpawnEntity(next.Proto, MapCoordinates.Nullspace);
|
||||
clippy.ModifyLayers = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_entity = EntityManager.SpawnEntity(_cfg.GetCVar(CCVars.ClippyEntity), MapCoordinates.Nullspace);
|
||||
clippy.ModifyLayers = true;
|
||||
}
|
||||
if (!EntityManager.TryGetComponent(_entity, out sprite))
|
||||
return;
|
||||
clippy.InitLabel(EntityManager.GetComponentOrNull<PaperVisualsComponent>(_entity), _resCache);
|
||||
|
||||
var scale = sprite.Scale;
|
||||
if (clippy.ModifyLayers)
|
||||
{
|
||||
sprite.Scale = Vector2.One;
|
||||
}
|
||||
else
|
||||
{
|
||||
sprite.Scale = new Vector2(2, 2);
|
||||
}
|
||||
clippy.Entity.SetEntity(_entity);
|
||||
clippy.Entity.Scale = scale;
|
||||
|
||||
_currentMessage = next;
|
||||
_secondsUntilNextState = next.SlideTime;
|
||||
clippy.State = ClippyState.Revealing;
|
||||
_previousStep = 0;
|
||||
if (clippy.ModifyLayers)
|
||||
{
|
||||
sprite.LayerSetAnimationTime("revealing", 0);
|
||||
sprite.LayerSetVisible("revealing", true);
|
||||
sprite.LayerSetVisible("speaking", false);
|
||||
sprite.LayerSetVisible("hiding", false);
|
||||
}
|
||||
sprite.Rotation = 0;
|
||||
clippy.Label.SetMarkup(_currentMessage.Msg);
|
||||
clippy.LabelPanel.Visible = false;
|
||||
clippy.Visible = true;
|
||||
sprite.Visible = true;
|
||||
break;
|
||||
|
||||
case ClippyState.Revealing:
|
||||
clippy.State = ClippyState.Speaking;
|
||||
if (!EntityManager.TryGetComponent(_entity, out sprite))
|
||||
return;
|
||||
sprite.Rotation = 0;
|
||||
_previousStep = 0;
|
||||
if (clippy.ModifyLayers)
|
||||
{
|
||||
sprite.LayerSetAnimationTime("speaking", 0);
|
||||
sprite.LayerSetVisible("revealing", false);
|
||||
sprite.LayerSetVisible("speaking", true);
|
||||
sprite.LayerSetVisible("hiding", false);
|
||||
}
|
||||
clippy.LabelPanel.Visible = true;
|
||||
clippy.InvalidateArrange();
|
||||
clippy.InvalidateMeasure();
|
||||
if (_currentMessage != null)
|
||||
_secondsUntilNextState = _currentMessage.SpeakTime;
|
||||
|
||||
break;
|
||||
|
||||
case ClippyState.Speaking:
|
||||
clippy.State = ClippyState.Hiding;
|
||||
if (!EntityManager.TryGetComponent(_entity, out sprite))
|
||||
return;
|
||||
if (clippy.ModifyLayers)
|
||||
{
|
||||
sprite.LayerSetAnimationTime("hiding", 0);
|
||||
sprite.LayerSetVisible("revealing", false);
|
||||
sprite.LayerSetVisible("speaking", false);
|
||||
sprite.LayerSetVisible("hiding", true);
|
||||
}
|
||||
clippy.LabelPanel.Visible = false;
|
||||
if (_currentMessage != null)
|
||||
_secondsUntilNextState = _currentMessage.SlideTime;
|
||||
break;
|
||||
|
||||
default: // finished hiding
|
||||
|
||||
EntityManager.DeleteEntity(_entity);
|
||||
_entity = default;
|
||||
clippy.Visible = false;
|
||||
_currentMessage = null;
|
||||
_secondsUntilNextState = 0;
|
||||
clippy.State = ClippyState.Hidden;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnScreenChanged((UIScreen? Old, UIScreen? New) ev)
|
||||
{
|
||||
ev.Old?.RemoveWidget<ClippyUI>();
|
||||
_currentMessage = null;
|
||||
EntityManager.DeleteEntity(_entity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
using Content.Shared.Tips;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Tips;
|
||||
|
||||
public sealed class TipsSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IUserInterfaceManager _uiMan = default!;
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeNetworkEvent<ClippyEvent>(OnClippyEv);
|
||||
}
|
||||
|
||||
private void OnClippyEv(ClippyEvent ev)
|
||||
{
|
||||
_uiMan.GetUIController<ClippyUIController>().AddMessage(ev);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,11 @@ using Content.Server.GameTicking;
|
|||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Tips;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
|
@ -22,6 +26,7 @@ public sealed class TipsSystem : EntitySystem
|
|||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly GameTicker _ticker = default!;
|
||||
[Dependency] private readonly IConsoleHost _conHost = default!;
|
||||
|
||||
private bool _tipsEnabled;
|
||||
private float _tipTimeOutOfRound;
|
||||
|
|
@ -42,8 +47,65 @@ public sealed class TipsSystem : EntitySystem
|
|||
Subs.CVar(_cfg, CCVars.TipsDataset, SetDataset, true);
|
||||
|
||||
RecalculateNextTipTime();
|
||||
_conHost.RegisterCommand("clippy", SendClippy);
|
||||
_conHost.RegisterCommand("tip", SendTip);
|
||||
}
|
||||
|
||||
private void SendTip(IConsoleShell shell, string argstr, string[] args)
|
||||
{
|
||||
AnnounceRandomTip();
|
||||
RecalculateNextTipTime();
|
||||
}
|
||||
|
||||
private void SendClippy(IConsoleShell shell, string argstr, string[] args)
|
||||
{
|
||||
if (args.Length < 2)
|
||||
{
|
||||
shell.WriteLine(
|
||||
"usage: clippy <player Uid | broadcast> <message> [entity prototype] [speak time] [slide time] [waddle]");
|
||||
return;
|
||||
}
|
||||
|
||||
ActorComponent? actor = null;
|
||||
if (args[0] != "broadcast" && args[0] != "all")
|
||||
{
|
||||
if (!EntityUid.TryParse(args[0], out var uid)
|
||||
|| !TryComp(uid, out actor))
|
||||
{
|
||||
shell.WriteError($"Could not find player {args[0]}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var ev = new ClippyEvent(args[1]);
|
||||
|
||||
string proto;
|
||||
if (args.Length > 2)
|
||||
{
|
||||
ev.Proto = args[2];
|
||||
if (!_prototype.HasIndex<EntityPrototype>(args[2]))
|
||||
{
|
||||
shell.WriteError($"Unknown prototype: {args[2]}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.Length > 3)
|
||||
ev.SpeakTime = float.Parse(args[3]);
|
||||
|
||||
if (args.Length > 4)
|
||||
ev.SlideTime = float.Parse(args[4]);
|
||||
|
||||
if (args.Length > 5)
|
||||
ev.WaddleInterval = float.Parse(args[5]);
|
||||
|
||||
if (actor != null)
|
||||
RaiseNetworkEvent(ev, actor.PlayerSession);
|
||||
else
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
|
@ -87,10 +149,11 @@ public sealed class TipsSystem : EntitySystem
|
|||
return;
|
||||
|
||||
var tip = _random.Pick(tips.Values);
|
||||
var msg = Loc.GetString("tips-system-chat-message-wrap", ("tip", tip));
|
||||
var msg = $"Tippy the Clown says:\n\n{tip}";
|
||||
|
||||
_chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg,
|
||||
EntityUid.Invalid, false, false, Color.MediumPurple);
|
||||
var ev = new ClippyEvent(msg);
|
||||
ev.SpeakTime = 1 + tip.Length * 0.05f;
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
|
||||
private void RecalculateNextTipTime()
|
||||
|
|
|
|||
|
|
@ -2043,6 +2043,10 @@ namespace Content.Shared.CCVar
|
|||
public static readonly CVarDef<bool> GatewayGeneratorEnabled =
|
||||
CVarDef.Create("gateway.generator_enabled", true);
|
||||
|
||||
// Clippy!
|
||||
public static readonly CVarDef<string> ClippyEntity =
|
||||
CVarDef.Create("clippy.entity", "Tippy", CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/*
|
||||
* DEBUG
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Tips;
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class ClippyEvent : EntityEventArgs
|
||||
{
|
||||
public ClippyEvent(string msg)
|
||||
{
|
||||
Msg = msg;
|
||||
}
|
||||
|
||||
public string Msg;
|
||||
public string? Proto;
|
||||
public float SpeakTime = 5;
|
||||
public float SlideTime = 3;
|
||||
public float WaddleInterval = 0.5f;
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
- type: entity
|
||||
id: Tippy
|
||||
components:
|
||||
- type: Sprite
|
||||
netsync: false
|
||||
noRot: false
|
||||
scale: 4,4
|
||||
layers:
|
||||
- sprite: Tips/tippy.rsi
|
||||
state: left
|
||||
map: [ "revealing" ]
|
||||
- sprite: Tips/tippy.rsi
|
||||
state: right
|
||||
map: [ "hiding" ]
|
||||
- sprite: Tips/tippy.rsi
|
||||
state: down
|
||||
visible: false
|
||||
map: [ "speaking" ]
|
||||
# footstep sounds wile waddling onto the screen.
|
||||
- type: FootstepModifier
|
||||
footstepSoundCollection:
|
||||
collection: FootstepClown
|
||||
# visuals for the speech bubble.
|
||||
# only supports background image.
|
||||
- type: PaperVisuals
|
||||
backgroundImagePath: "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png"
|
||||
backgroundPatchMargin: 16.0, 16.0, 16.0, 16.0
|
||||
backgroundModulate: "#ffffcc"
|
||||
fontAccentColor: "#000000"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"version": 1,
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "",
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "down"
|
||||
},
|
||||
{
|
||||
"name": "left"
|
||||
},
|
||||
{
|
||||
"name": "right"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -136,3 +136,8 @@
|
|||
- Commands:
|
||||
- "|"
|
||||
- oldhelp
|
||||
|
||||
- Flags: FUN
|
||||
Commands:
|
||||
- clippy
|
||||
- tip
|
||||
|
|
|
|||
Loading…
Reference in New Issue