Files
RobustToolbox/SS14.Client/GameObjects/Components/Renderable/AnimatedSpriteComponent.cs
Acruid 6247ff4eff Console Rework (#483)
* Extracted the logic from DebugConsole into a new ClientConsole class.

* ClientConsole moved to IoC system.
Verb system replaced with concmds.

* Shared Cleanup

* ClientChatConsole skeleton.

* DebugConsole and LobbyChat are now both subscribed to ClientChatConsole.

* Removed server chat commands.

* cleaned up server command sysyem.

* More chat handling, and aliasing.

* Nightly work on Say command.

* Fixes a bug in Maths.Angle.

* Chat channel colors moved to ClientChatConsole.

* Now Server commands are sent without opening DebugConsole.

* Emotes work.
Clientside chat formatting works.

* Fixed angle unit test.
2018-01-04 00:53:41 +01:00

419 lines
14 KiB
C#

using OpenTK;
using OpenTK.Graphics;
using SS14.Client.Graphics;
using SS14.Client.Graphics.Sprites;
using SS14.Client.Graphics.TexHelpers;
using SS14.Client.Interfaces.GameObjects;
using SS14.Shared.Interfaces.Map;
using SS14.Client.Interfaces.Resource;
using SS14.Shared;
using SS14.Shared.GameObjects;
using SS14.Shared.Interfaces.GameObjects;
using SS14.Shared.Interfaces.GameObjects.Components;
using SS14.Shared.IoC;
using SS14.Shared.Maths;
using SS14.Shared.Utility;
using System;
using System.Collections.Generic;
using System.Linq;
using YamlDotNet.RepresentationModel;
using Vector2i = SS14.Shared.Maths.Vector2i;
using SS14.Shared.Map;
using Vector2 = SS14.Shared.Maths.Vector2;
using SS14.Client.Graphics.Utility;
using SS14.Shared.Console;
namespace SS14.Client.GameObjects
{
public class AnimatedSpriteComponent : Component, ISpriteRenderableComponent, IClickTargetComponent
{
public override string Name => "AnimatedSprite";
public override uint? NetID => NetIDs.ANIMATED_SPRITE;
protected string baseSprite;
protected string currentSprite;
protected AnimatedSprite sprite;
protected IRenderableComponent master;
protected List<IRenderableComponent> slaves = new List<IRenderableComponent>();
protected bool visible = true;
public DrawDepth DrawDepth { get; set; }
private SpeechBubble _speechBubble;
public Color Color { get; set; } = Color.White;
public int MapID { get; private set; }
public override Type StateType => typeof(AnimatedSpriteComponentState);
/// <summary>
/// Center of the Y axis of the sprite bounds in world coords.
/// </summary>
public float Bottom => Owner.GetComponent<ITransformComponent>().WorldPosition.Y + sprite.LocalAABB.Height / 2;
public Box2 AverageAABB
{
get
{
var aaabb = sprite.AverageAABB;
return Box2.FromDimensions(
aaabb.Left, aaabb.Top,
aaabb.Width, aaabb.Height
);
}
}
#region ISpriteComponent Members
public virtual Box2 LocalAABB => sprite.LocalAABB;
public bool HorizontalFlip { get; set; }
#endregion ISpriteComponent Members
public override void OnAdd(IEntity owner)
{
base.OnAdd(owner);
//Send a spritechanged message so everything knows whassup.
Owner.SendMessage(this, ComponentMessageType.SpriteChanged);
}
public override void Initialize()
{
base.Initialize();
var transform = Owner.GetComponent<ITransformComponent>();
transform.OnMove += OnMove;
MapID = transform.MapID;
}
public override void Shutdown()
{
var transform = Owner.GetComponent<ITransformComponent>();
transform.OnMove -= OnMove;
base.Shutdown();
}
public void OnMove(object sender, MoveEventArgs args)
{
MapID = args.NewPosition.MapID;
}
public void SetSprite()
{
if (baseSprite != null)
{
SetSprite(baseSprite);
}
}
public void SetSprite(string name)
{
currentSprite = name;
sprite = IoCManager.Resolve<IResourceCache>().GetAnimatedSprite(name);
}
public void SetAnimationState(string state, bool loop = true)
{
sprite.SetAnimationState(state);
sprite.SetLoop(loop);
}
public override ComponentReplyMessage ReceiveMessage(object sender, ComponentMessageType type,
params object[] list)
{
ComponentReplyMessage reply = base.ReceiveMessage(sender, type, list);
if (sender == this) //Don't listen to our own messages!
return ComponentReplyMessage.Empty;
switch (type)
{
case ComponentMessageType.SlaveAttach:
SetMaster(Owner.EntityManager.GetEntity((int)list[0]));
break;
case ComponentMessageType.ItemUnEquipped:
case ComponentMessageType.Dropped:
UnsetMaster();
break;
case ComponentMessageType.EntitySaidSomething:
ChatChannel channel;
if (Enum.TryParse(list[0].ToString(), true, out channel))
{
string text = list[1].ToString();
if (channel == ChatChannel.Local || channel == ChatChannel.Player ||
channel == ChatChannel.Radio)
{
(_speechBubble ?? (_speechBubble = new SpeechBubble(Owner.Name + Owner.Uid))).SetText(text);
}
}
break;
}
return reply;
}
protected void SetDrawDepth(DrawDepth p)
{
DrawDepth = p;
}
public virtual Sprite GetCurrentSprite()
{
return sprite.GetCurrentSprite();
}
/// <summary>
/// Check if the world position is inside of the sprite texture. This checks both sprite bounds and transparency.
/// </summary>
/// <param name="worldPos">World position to check.</param>
/// <returns>Is the world position inside of the sprite?</returns>
public virtual bool WasClicked(LocalCoordinates worldPos)
{
if (sprite == null || !visible) return false;
var spriteToCheck = GetCurrentSprite();
var screenScale = CluwneLib.Camera.PixelsPerMeter;
// local screen bounds
var localBounds = spriteToCheck.LocalBounds;
// local world bounds
var worldBounds = localBounds.Scale(1.0f / screenScale);
// move the origin from bottom right to center
worldBounds = worldBounds.Translated(new Vector2(-worldBounds.Width / 2, -worldBounds.Height / 2));
// absolute world bounds
worldBounds = worldBounds.Translated(Owner.GetComponent<ITransformComponent>().WorldPosition);
// check if clicked inside of the rectangle
if (!worldBounds.Contains(worldPos.ToWorld().Position))
return false;
// Get the sprite's position within the texture
var texRect = spriteToCheck.TextureRect;
// Get the clicked position relative to the texture (World to Texture)
var pixelPos = new Vector2i((int)((worldPos.X - worldBounds.Left) * screenScale), (int)((worldPos.Y - worldBounds.Top) * screenScale));
// offset pos by texture sub-rectangle
pixelPos = pixelPos + new Vector2i(texRect.Left, texRect.Top);
// make sure the position is actually inside the texture
if (!texRect.Contains(pixelPos.X, pixelPos.Y))
throw new InvalidOperationException("The click was inside the sprite bounds, but not inside the texture bounds? Check yo math.");
// fetch texture key of the sprite
var resCache = IoCManager.Resolve<IResourceCache>();
if (!resCache.TextureToKey.TryGetValue(spriteToCheck.Texture, out string textureKey))
throw new InvalidOperationException("Trying to look up a texture that does not exist in the ResourceCache.");
// use the texture key to fetch the Image of the sprite
if (!TextureCache.Textures.TryGetValue(textureKey, out TextureInfo texInfo))
throw new InvalidOperationException("The texture exists in the ResourceCache, but not in the CluwneLib TextureCache?");
// Check if the clicked pixel is transparent enough in the Image
return texInfo.Image[(uint)pixelPos.X, (uint)pixelPos.Y].AByte >= Limits.ClickthroughLimit;
}
public override void LoadParameters(YamlMappingNode mapping)
{
YamlNode node;
if (mapping.TryGetNode("drawdepth", out node))
{
SetDrawDepth(node.AsEnum<DrawDepth>());
}
if (mapping.TryGetNode("color", out node))
{
try
{
Color = System.Drawing.Color.FromName(node.ToString());
}
catch
{
Color = node.AsHexColor();
}
}
if (mapping.TryGetNode("sprite", out node))
{
baseSprite = node.AsString();
SetSprite(baseSprite);
}
}
public virtual void Render(Vector2 topLeft, Vector2 bottomRight)
{
UpdateSlaves();
//Render slaves beneath
IEnumerable<IRenderableComponent> renderablesBeneath = from IRenderableComponent c in slaves
//FIXTHIS
orderby c.DrawDepth ascending
where c.DrawDepth < DrawDepth
select c;
foreach (IRenderableComponent component in renderablesBeneath.ToList())
{
component.Render(topLeft, bottomRight);
}
//Render this sprite
if (!visible) return;
if (sprite == null) return;
var ownerPos = Owner.GetComponent<ITransformComponent>().WorldPosition;
SetSpriteCenter(ownerPos * CluwneLib.Camera.PixelsPerMeter);
var bounds = sprite.TextureRect;
if (ownerPos.X + bounds.Left + bounds.Width < topLeft.X
|| ownerPos.X > bottomRight.X
|| ownerPos.Y + bounds.Top + bounds.Height < topLeft.Y
|| ownerPos.Y > bottomRight.Y)
return;
sprite.HorizontalFlip = HorizontalFlip;
sprite.Draw(Color);
//Render slaves above
IEnumerable<IRenderableComponent> renderablesAbove = from IRenderableComponent c in slaves
//FIXTHIS
orderby c.DrawDepth ascending
where c.DrawDepth >= DrawDepth
select c;
foreach (IRenderableComponent component in renderablesAbove.ToList())
{
component.Render(topLeft, bottomRight);
}
}
/// <inheritdoc />
public override void Update(float frameTime)
{
base.Update(frameTime);
if (sprite == null || IsSlaved())
return;
var worldRot = Owner.GetComponent<TransformComponent>().Rotation.ToVec();
// world2screen
worldRot = new Vector2(worldRot.X, worldRot.Y * -1);
//If the sprite is idle, it won't try to update Direction, meaning you stay facing the way you move
if (sprite.CurrentAnimationStateKey.Equals("idle"))
sprite.Update(frameTime);
else
{
sprite.Direction = worldRot.GetDir();
sprite.Update(frameTime);
}
}
public virtual void UpdateSlaves()
{
if (slaves.Any())
{
foreach (var slave in slaves.OfType<AnimatedSpriteComponent>())
{
slave.CopyAnimationInfoFrom(this);
}
}
}
protected void CopyAnimationInfoFrom(AnimatedSpriteComponent c)
{
if (sprite != null && c != null && c.sprite != null)
{
sprite.CopyStateFrom(c.sprite);
}
}
public void SetSpriteCenter(Vector2 center)
{
sprite.SetPosition(center.X - (sprite.TextureRect.Width / 2),
center.Y - (sprite.TextureRect.Height / 2));
}
public bool IsSlaved()
{
return master != null;
}
public void SetMaster(IEntity m)
{
if (m == null)
{
UnsetMaster();
return;
}
if (!m.HasComponent<ISpriteRenderableComponent>())
return;
var mastercompo = m.GetComponent<ISpriteRenderableComponent>();
//If there's no sprite component, then FUCK IT
if (mastercompo == null)
return;
mastercompo.AddSlave(this);
master = mastercompo;
}
public void SetMaster(int? mUid)
{
if (master != null)
{
if (mUid == null)
{
UnsetMaster();
}
else if (mUid != master.Owner.Uid)
{
UnsetMaster();
SetMaster(Owner.EntityManager.GetEntity((int)mUid));
}
}
else if (mUid != null)
{
SetMaster(Owner.EntityManager.GetEntity((int)mUid));
}
}
public void UnsetMaster()
{
if (master == null)
return;
master.RemoveSlave(this);
master = null;
}
public void AddSlave(IRenderableComponent slavecompo)
{
slaves.Add(slavecompo);
}
public void RemoveSlave(IRenderableComponent slavecompo)
{
if (slaves.Contains(slavecompo))
slaves.Remove(slavecompo);
}
/// <inheritdoc />
public override void HandleComponentState(ComponentState state)
{
var newState = (AnimatedSpriteComponentState)state;
DrawDepth = newState.DrawDepth;
visible = newState.Visible;
if (sprite.Name != newState.Name)
SetSprite(newState.Name);
if (sprite.CurrentAnimationStateKey != newState.CurrentAnimation)
sprite.SetAnimationState(newState.CurrentAnimation ?? "idle");
SetMaster(newState.MasterUid);
sprite.SetLoop(newState.Loop);
}
}
}