- add: Clippy

This commit is contained in:
Jabkas
2024-04-16 20:40:28 +03:00
parent 8a176945bd
commit 0d2e7dda61
14 changed files with 541 additions and 8 deletions

View File

@@ -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._Amour.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,
}
}

View File

@@ -0,0 +1,11 @@
<tips1:ClippyUI xmlns="https://spacestation14.io"
xmlns:tips1="clr-namespace:Content.Client._Amour.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"/>
</tips1:ClippyUI>

View File

@@ -0,0 +1,280 @@
using System.Numerics;
using Content.Client.Message;
using Content.Client.Paper;
using Content.Shared._Amour.Tips;
using Content.Shared.CCVar;
using Content.Shared.Movement.Components;
using Robust.Client.Audio;
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.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client._Amour.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!;
[UISystemDependency] private readonly EntityManager _entSys = 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
{
ClippyUI.ClippyState.Hidden => 0,
ClippyUI.ClippyState.Revealing => Math.Clamp(1 - _secondsUntilNextState / slideTime, 0, 1),
ClippyUI.ClippyState.Hiding => Math.Clamp(_secondsUntilNextState / slideTime, 0, 1),
_ => 1,
};
var waddle = _currentMessage.WaddleInterval;
if (_currentMessage == null
|| waddle <= 0
|| clippy.State == ClippyUI.ClippyState.Hidden
|| clippy.State == ClippyUI.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 ClippyUI.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);
if (!EntityManager.HasComponent<PaperVisualsComponent>(_entity))
{
var paper = EntityManager.AddComponent<PaperVisualsComponent>(_entity);
paper.BackgroundImagePath = "/Textures/Interface/Paper/paper_background_default.svg.96dpi.png";
paper.BackgroundPatchMargin = new(16f, 16f, 16f, 16f);
paper.BackgroundModulate = new(255, 255, 204);
paper.FontAccentColor = new(0, 0, 0);
}
var scale = sprite.Scale;
if (clippy.ModifyLayers)
{
sprite.Scale = Vector2.One;
}
else
{
sprite.Scale = new Vector2(3, 3);
}
clippy.Entity.SetEntity(_entity);
clippy.Entity.Scale = scale;
_currentMessage = next;
_secondsUntilNextState = next.SlideTime;
clippy.State = ClippyUI.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.Label.Visible = false;
clippy.Visible = true;
sprite.Visible = true;
break;
case ClippyUI.ClippyState.Revealing:
clippy.State = ClippyUI.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.Label.Visible = true;
clippy.LabelPanel.Visible = true;
clippy.InvalidateArrange();
clippy.InvalidateMeasure();
if (_currentMessage != null)
_secondsUntilNextState = _currentMessage.SpeakTime;
break;
case ClippyUI.ClippyState.Speaking:
clippy.State = ClippyUI.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: // закончил прятаться
EntityManager.DeleteEntity(_entity);
_entity = default;
clippy.Visible = false;
_currentMessage = null;
_secondsUntilNextState = 0;
clippy.State = ClippyUI.ClippyState.Hidden;
break;
}
}
private void OnScreenChanged((UIScreen? Old, UIScreen? New) ev)
{
ev.Old?.RemoveWidget<ClippyUI>();
_currentMessage = null;
EntityManager.DeleteEntity(_entity);
}
}

View File

@@ -0,0 +1,20 @@
using Content.Shared._Amour.Tips;
using Robust.Client.UserInterface;
namespace Content.Client._Amour.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);
}
}

View File

@@ -1,9 +1,12 @@
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Shared._Amour.Tips;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Dataset;
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,11 +25,13 @@ 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;
private float _tipTimeInRound;
private string _tipsDataset = "";
private float _tipTippyChance;
[ViewVariables(VVAccess.ReadWrite)]
private TimeSpan _nextTipTime = TimeSpan.Zero;
@@ -40,17 +45,80 @@ public sealed class TipsSystem : EntitySystem
Subs.CVar(_cfg, CCVars.TipFrequencyInRound, SetInRound, true);
Subs.CVar(_cfg, CCVars.TipsEnabled, SetEnabled, true);
Subs.CVar(_cfg, CCVars.TipsDataset, SetDataset, true);
Subs.CVar(_cfg, CCVars.TipsTippyChance, SetTippyChance, true);
RecalculateNextTipTime();
_conHost.RegisterCommand("clippy", Loc.GetString("cmd-tippy-desc"), Loc.GetString("cmd-tippy-help"), SendClippy, SendClippyHelper);
_conHost.RegisterCommand("tip", Loc.GetString("cmd-tip-desc"), "tip", SendTip);
}
private CompletionResult SendClippyHelper(IConsoleShell shell, string[] args)
{
return args.Length switch
{
1 => CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Loc.GetString("cmd-tippy-auto-1")),
2 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-2")),
3 => CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs<EntityPrototype>(), Loc.GetString("cmd-tippy-auto-3")),
4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")),
5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")),
6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")),
_ => CompletionResult.Empty
};
}
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);
if (!_tipsEnabled)
return;
if (_nextTipTime != TimeSpan.Zero && _timing.CurTime > _nextTipTime)
{
AnnounceRandomTip();
@@ -71,7 +139,6 @@ public sealed class TipsSystem : EntitySystem
private void SetEnabled(bool value)
{
_tipsEnabled = value;
if (_nextTipTime != TimeSpan.Zero)
RecalculateNextTipTime();
}
@@ -81,16 +148,29 @@ public sealed class TipsSystem : EntitySystem
_tipsDataset = value;
}
private void SetTippyChance(float value)
{
_tipTippyChance = value;
}
private void AnnounceRandomTip()
{
if (!_prototype.TryIndex<DatasetPrototype>(_tipsDataset, out var tips))
return;
var tip = _random.Pick(tips.Values);
var msg = Loc.GetString("tips-system-chat-message-wrap", ("tip", tip));
_chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg,
EntityUid.Invalid, false, false, Color.MediumPurple);
if (_random.Prob(_tipTippyChance))
{
var ev = new ClippyEvent(msg);
ev.SpeakTime = 1 + tip.Length * 0.05f;
RaiseNetworkEvent(ev);
}
else
{
_chat.ChatMessageToManyFiltered(Filter.Broadcast(), ChatChannel.OOC, tip, msg,
EntityUid.Invalid, false, false, Color.MediumPurple);
}
}
private void RecalculateNextTipTime()
@@ -104,7 +184,6 @@ public sealed class TipsSystem : EntitySystem
_nextTipTime = _timing.CurTime + TimeSpan.FromSeconds(_tipTimeOutOfRound);
}
}
private void OnGameRunLevelChanged(GameRunLevelChangedEvent ev)
{
// reset for lobby -> inround

View File

@@ -2018,10 +2018,19 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> GatewayGeneratorEnabled =
CVarDef.Create("gateway.generator_enabled", true);
//Amour EDIT Успешно спиздил
// Clippy!
public static readonly CVarDef<string> ClippyEntity =
CVarDef.Create("clippy.entity", "Tippy", CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// The chance for Tippy to replace a normal tip message.
/// </summary>
public static readonly CVarDef<float> TipsTippyChance =
CVarDef.Create("tips.tippy_chance", 0.01f);
//Amour EDIT Успешно спиздил
/*
* DEBUG
*/
/// <summary>
/// A simple toggle to test <c>OptionsVisualizerComponent</c>.
/// </summary>

View File

@@ -0,0 +1,18 @@
using Robust.Shared.Serialization;
namespace Content.Shared._Amour.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;
}

View File

@@ -0,0 +1,11 @@
cmd-tippy-desc = Передайте сообщение от имени клоуна Типпи.
cmd-tippy-help = tippy <user | all> <message> [entity prototype] [speak time] [slide time] [waddle interval]
cmd-tippy-auto-1 = <user | all>
cmd-tippy-auto-2 = сообщение
cmd-tippy-auto-3 = entity prototype
cmd-tippy-auto-4 = время произнесения, в секундах
cmd-tippy-auto-5 = время скольжения, в секундах
cmd-tippy-auto-6 = интервал перехода вразвалочку, в секундах
cmd-tippy-error-no-user = Пользователь не найден.
cmd-tippy-error-no-prototype = Прототип не найден: {$proto}
cmd-tip-desc = Создайте случайный игровой совет.

View File

@@ -0,0 +1,29 @@
- type: entity
id: Tippy
components:
- type: Sprite
netsync: false
noRot: false
scale: 4,4
layers:
- sprite: _Amour/Tips/tippy.rsi
state: left
map: [ "revealing" ]
- sprite: _Amour/Tips/tippy.rsi
state: right
map: [ "hiding" ]
- sprite: _Amour/Tips/tippy.rsi
state: down
visible: false
map: [ "speaking" ]
# на экране раздаются звуки ковыляющих шагов.
- type: FootstepModifier
footstepSoundCollection:
collection: FootstepClown
# визуальные эффекты для речевого пузыря.
# поддерживает только фоновое изображение.
- 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

View File

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

View File

@@ -95,6 +95,8 @@
- tp
- tpto
- respawn
- clippy
- tip
- Flags: SERVER
Commands: