diff --git a/Symcol.Core/GameObjects/SymcolHitbox.cs b/Symcol.Core/GameObjects/SymcolHitbox.cs
new file mode 100644
index 0000000000..a4da576b68
--- /dev/null
+++ b/Symcol.Core/GameObjects/SymcolHitbox.cs
@@ -0,0 +1,80 @@
+using osu.Framework.Graphics;
+using OpenTK;
+using Symcol.Core.Graphics.Containers;
+
+namespace Symcol.Core.GameObjects
+{
+ public class SymcolHitbox : SymcolContainer
+ {
+ ///
+ /// whether we want to do hit detection
+ ///
+ public int Team { get; set; }
+
+ ///
+ /// whether we want to do hit detection
+ ///
+ public bool HitDetection { get; set; } = true;
+
+ ///
+ /// the shape of this object (used for hit detection)
+ ///
+ public Shape Shape { get; }
+
+ public SymcolHitbox(Vector2 size, Shape shape = Shape.Circle)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Shape = shape;
+ Size = size;
+
+ if (Shape == Shape.Circle)
+ Child = new SymcolContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ CornerRadius = Width / 2
+ };
+ else if (Shape == Shape.Rectangle)
+ Child = new SymcolContainer
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+
+ public bool HitDetect(SymcolHitbox hitbox1, SymcolHitbox hitbox2)
+ {
+ if (hitbox1.HitDetection && hitbox2.HitDetection && hitbox1.Team != hitbox2.Team)
+ {
+ if (hitbox1.Shape == Shape.Circle && hitbox2.Shape == Shape.Circle)
+ {
+ if (hitbox1.ScreenSpaceDrawQuad.AABB.IntersectsWith(hitbox2.ScreenSpaceDrawQuad.AABB))
+ return true;
+ }
+ else if (hitbox1.Shape == Shape.Circle && hitbox2.Shape == Shape.Rectangle || hitbox1.Shape == Shape.Rectangle && hitbox2.Shape == Shape.Circle)
+ {
+ if (hitbox1.ScreenSpaceDrawQuad.AABB.IntersectsWith(hitbox2.ScreenSpaceDrawQuad.AABB))
+ return true;
+ }
+ else if (hitbox1.Shape == Shape.Rectangle && hitbox2.Shape == Shape.Rectangle)
+ {
+ if (hitbox1.ScreenSpaceDrawQuad.AABB.IntersectsWith(hitbox2.ScreenSpaceDrawQuad.AABB))
+ return true;
+ }
+ else if (hitbox1.Shape == Shape.Complex || hitbox2.Shape == Shape.Complex)
+ foreach (SymcolContainer child1 in hitbox1.Children)
+ foreach (SymcolContainer child2 in hitbox2.Children)
+ if (child1.ScreenSpaceDrawQuad.AABB.IntersectsWith(child2.ScreenSpaceDrawQuad.AABB))
+ return true;
+ }
+ return false;
+ }
+ }
+
+ public enum Shape
+ {
+ Circle,
+ Rectangle,
+ Complex
+ }
+}
diff --git a/Symcol.Core/Graphics/Containers/SymcolClickableContainer.cs b/Symcol.Core/Graphics/Containers/SymcolClickableContainer.cs
new file mode 100644
index 0000000000..4e63117493
--- /dev/null
+++ b/Symcol.Core/Graphics/Containers/SymcolClickableContainer.cs
@@ -0,0 +1,12 @@
+using osu.Framework.Graphics.Containers;
+
+namespace Symcol.Core.Graphics.Containers
+{
+ ///
+ /// Will support base eden game functions (if we come up with any)
+ ///
+ public class SymcolClickableContainer : ClickableContainer
+ {
+
+ }
+}
diff --git a/Symcol.Core/Graphics/Containers/SymcolContainer.cs b/Symcol.Core/Graphics/Containers/SymcolContainer.cs
new file mode 100644
index 0000000000..e0bdf9503b
--- /dev/null
+++ b/Symcol.Core/Graphics/Containers/SymcolContainer.cs
@@ -0,0 +1,12 @@
+using osu.Framework.Graphics.Containers;
+
+namespace Symcol.Core.Graphics.Containers
+{
+ ///
+ /// Will support base eden game functions (if we come up with any)
+ ///
+ public class SymcolContainer : Container
+ {
+
+ }
+}
diff --git a/Symcol.Core/Graphics/Containers/SymcolDragContainer.cs b/Symcol.Core/Graphics/Containers/SymcolDragContainer.cs
new file mode 100644
index 0000000000..1bc2d86248
--- /dev/null
+++ b/Symcol.Core/Graphics/Containers/SymcolDragContainer.cs
@@ -0,0 +1,43 @@
+using osu.Framework.Input;
+using OpenTK;
+using OpenTK.Input;
+
+namespace Symcol.Core.Graphics.Containers
+{
+ public class SymcolDragContainer : SymcolContainer
+ {
+ protected override bool OnDragStart(InputState state) => true;
+
+ public bool AllowLeftClickDrag { get; set; } = true;
+
+ private bool drag;
+
+ private Vector2 startPosition;
+
+ protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
+ {
+ startPosition = Position;
+
+ if (args.Button == MouseButton.Left && AllowLeftClickDrag || args.Button == MouseButton.Right)
+ drag = true;
+
+ return base.OnMouseDown(state, args);
+ }
+
+ protected override bool OnDrag(InputState state)
+ {
+ if (drag)
+ Position = startPosition + state.Mouse.Position - state.Mouse.PositionMouseDown.GetValueOrDefault();
+
+ return base.OnDrag(state);
+ }
+
+ protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
+ {
+ if (args.Button == MouseButton.Left && AllowLeftClickDrag || args.Button == MouseButton.Right)
+ drag = false;
+
+ return base.OnMouseUp(state, args);
+ }
+ }
+}
diff --git a/Symcol.Core/Graphics/UserInterface/SpriteButton.cs b/Symcol.Core/Graphics/UserInterface/SpriteButton.cs
new file mode 100644
index 0000000000..a4db27a33c
--- /dev/null
+++ b/Symcol.Core/Graphics/UserInterface/SpriteButton.cs
@@ -0,0 +1,80 @@
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Input;
+using Symcol.Core.Graphics.Containers;
+
+namespace Symcol.Core.Graphics.UserInterface
+{
+ ///
+ /// just a Button with a sprite
+ ///
+ public class SpriteButton : SymcolClickableContainer
+ {
+ private readonly string textureName;
+
+ public string Text
+ {
+ get { return spriteText?.Text; }
+ set
+ {
+ if (spriteText != null)
+ spriteText.Text = value;
+ }
+ }
+
+ private readonly Sprite sprite;
+ private readonly SpriteText spriteText;
+
+ public SpriteButton(string textureName)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ this.textureName = textureName;
+ Masking = true;
+
+ Children = new Drawable[]
+ {
+ sprite = new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fill
+ },
+ spriteText = new SpriteText
+ {
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures)
+ {
+ sprite.Texture = textures.Get(textureName);
+ }
+
+ protected override bool OnClick(InputState state)
+ {
+ if (Enabled.Value)
+ {
+ var flash = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.5f
+ };
+
+ Add(flash);
+
+ flash.Blending = BlendingMode.Additive;
+ flash.FadeOut(200);
+ flash.Expire();
+ }
+
+ return base.OnClick(state);
+ }
+ }
+}
diff --git a/Symcol.Core/Graphics/UserInterface/SymcolWindow.cs b/Symcol.Core/Graphics/UserInterface/SymcolWindow.cs
new file mode 100644
index 0000000000..be6ee59c4e
--- /dev/null
+++ b/Symcol.Core/Graphics/UserInterface/SymcolWindow.cs
@@ -0,0 +1,158 @@
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input;
+using OpenTK;
+using OpenTK.Graphics;
+using OpenTK.Input;
+using Symcol.Core.Graphics.Containers;
+
+namespace Symcol.Core.Graphics.UserInterface
+{
+ public class SymcolWindow : SymcolContainer
+ {
+ ///
+ /// Put all your stuff in this
+ ///
+ public SymcolContainer WindowContent { get; set; }
+ public SpriteText WindowTitle;
+
+ private readonly SymcolContainer topBar;
+ private readonly SymcolClickableContainer minimize;
+
+ public SymcolWindow(Vector2 size)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ CornerRadius = 6;
+ Masking = true;
+ AutoSizeAxes = Axes.Both;
+
+ Children = new Drawable[]
+ {
+ topBar = new SymcolContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Height = 20,
+ Width = size.X,
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.5f
+ },
+ WindowTitle = new SpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ TextSize = 18
+ },
+ new SymcolClickableContainer
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.Y,
+ Width = 30,
+ Action = Close,
+
+ Child = new Box
+ {
+ Colour = Color4.Red,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.5f
+ }
+ },
+ minimize = new SymcolClickableContainer
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.Y,
+ Width = 30,
+ Position = new Vector2(-30, 0),
+ Action = Minimize,
+
+ Child = new Box
+ {
+ Colour = Color4.White,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.5f
+ }
+ }
+ }
+ },
+ WindowContent = new SymcolContainer
+ {
+ Size = size,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre
+ }
+ };
+
+ WindowContent.Position = new Vector2(0, topBar.Height);
+ }
+
+ protected void Close()
+ {
+ this.FadeOut(200);
+ }
+
+ protected void Open()
+ {
+ this.FadeIn(200);
+ }
+
+ public void Toggle()
+ {
+ if (Alpha > 0)
+ this.FadeOut(200);
+ else
+ this.FadeIn(200);
+ }
+
+ protected override bool OnDragStart(InputState state) => true;
+
+ private bool drag;
+
+ protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
+ {
+ if (args.Button == MouseButton.Left)
+ drag = true;
+
+ return base.OnMouseDown(state, args);
+ }
+
+ protected override bool OnDrag(InputState state)
+ {
+ if (drag)
+ Position += state.Mouse.Delta;
+
+ return base.OnDrag(state);
+ }
+
+ protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
+ {
+ if (args.Button == MouseButton.Left)
+ drag = false;
+
+ return base.OnMouseUp(state, args);
+ }
+
+ public void Maximize()
+ {
+ WindowContent.FadeIn(200);
+ WindowContent.ScaleTo(Vector2.One, 200);
+ minimize.Action = Minimize;
+ }
+
+ public void Minimize()
+ {
+ WindowContent.FadeOut(200);
+ WindowContent.ScaleTo(new Vector2(1, 0), 200);
+ minimize.Action = Maximize;
+ }
+ }
+}
diff --git a/Symcol.Core/Networking/BasicPacket.cs b/Symcol.Core/Networking/BasicPacket.cs
new file mode 100644
index 0000000000..248a0328bf
--- /dev/null
+++ b/Symcol.Core/Networking/BasicPacket.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+
+namespace Symcol.Core.Networking
+{
+ [Serializable]
+ public class BasicPacket : Packet
+ {
+ ///
+ /// Ask host if we can connect
+ ///
+ public bool Connect;
+
+ ///
+ /// Tell the host we are breaking up
+ ///
+ public bool Disconnect;
+
+ ///
+ /// Testing Connection
+ ///
+ public bool Test;
+
+ ///
+ /// Send a force exit to others
+ ///
+ public bool Abort;
+
+ ///
+ /// PreLoad the game
+ ///
+ public bool LoadGame;
+
+ ///
+ /// Request a list of all players from Host
+ ///
+ public bool RequestPlayerList;
+
+ ///
+ /// List of players in this match that we should account for
+ ///
+ public List PlayerList = new List();
+
+ ///
+ /// Tell Host we are PreLoaded
+ ///
+ public bool Loaded;
+
+ ///
+ /// Start the game already!
+ ///
+ public bool StartGame;
+
+ ///
+ /// Send to host when game started
+ ///
+ public bool GameStarted;
+
+ public BasicPacket(ClientInfo clientInfo) : base(clientInfo)
+ {
+ }
+ }
+}
diff --git a/Symcol.Core/Networking/ClientInfo.cs b/Symcol.Core/Networking/ClientInfo.cs
new file mode 100644
index 0000000000..08c2d7c1e8
--- /dev/null
+++ b/Symcol.Core/Networking/ClientInfo.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace Symcol.Core.Networking
+{
+ ///
+ /// Just a client signature basically
+ ///
+ [Serializable]
+ public class ClientInfo
+ {
+ public string IP;
+
+ public int Port;
+
+ public int Ping;
+
+ public int ConncetionTryCount;
+
+ public double LastConnectionTime;
+
+ public double StartedTestConnectionTime;
+ }
+}
diff --git a/Symcol.Core/Networking/NetworkingClient.cs b/Symcol.Core/Networking/NetworkingClient.cs
new file mode 100644
index 0000000000..1e9af36e6e
--- /dev/null
+++ b/Symcol.Core/Networking/NetworkingClient.cs
@@ -0,0 +1,119 @@
+using System.IO;
+using System.Net;
+using System.Net.Sockets;
+using System.Runtime.Serialization.Formatters.Binary;
+
+namespace Symcol.Core.Networking
+{
+ public class NetworkingClient
+ {
+ public UdpClient UdpClient;
+
+ public IPEndPoint EndPoint;
+
+ ///
+ /// if false we only receive
+ ///
+ public readonly bool Send;
+
+ public readonly int Port;
+
+ public readonly string IP;
+
+ public NetworkingClient(bool send, string ip, int port = 25570)
+ {
+ Port = port;
+ IP = ip;
+
+ if (send)
+ initializeSend();
+ else
+ initializeReceive();
+ }
+
+ private void initializeSend()
+ {
+ UdpClient = new UdpClient(IP, Port);
+ }
+
+ private void initializeReceive()
+ {
+ UdpClient = new UdpClient(Port);
+ EndPoint = new IPEndPoint(IPAddress.Any, Port);
+ }
+
+ private void sendByte(byte[] data)
+ {
+ UdpClient.Send(data, data.Length);
+ }
+
+ private byte[] receiveByte()
+ {
+ return UdpClient.Receive(ref EndPoint);
+ }
+
+ public static int SENTPACKETCOUNT;
+
+ ///
+ /// Send a Packet somewhere
+ ///
+ ///
+ public void SendPacket(Packet packet)
+ {
+ SENTPACKETCOUNT++;
+ using (MemoryStream stream = new MemoryStream())
+ {
+ BinaryFormatter formatter = new BinaryFormatter();
+ formatter.Serialize(stream, packet);
+
+ stream.Position = 0;
+
+ int i = packet.PacketSize;
+ retry:
+ byte[] data = new byte[i];
+
+ try
+ {
+ stream.Read(data, 0, (int)stream.Length);
+ }
+ catch
+ {
+ i *= 2;
+ goto retry;
+ }
+
+ sendByte(data);
+ }
+ }
+
+ ///
+ /// Receive a Packet from somewhere
+ ///
+ ///
+ public Packet ReceivePacket(bool force = false)
+ {
+ if (UdpClient.Available > 0 || force)
+ using (MemoryStream stream = new MemoryStream())
+ {
+ byte[] data = receiveByte();
+ stream.Write(data, 0, data.Length);
+
+ stream.Position = 0;
+
+ BinaryFormatter formatter = new BinaryFormatter();
+ Packet packet = (Packet)formatter.Deserialize(stream);
+ packet.ClientInfo.IP = EndPoint.Address.ToString();
+
+ return packet;
+ }
+ else
+ return null;
+ }
+
+ public void Clear()
+ {
+ if (UdpClient != null)
+ UdpClient.Dispose();
+ }
+ }
+}
diff --git a/Symcol.Core/Networking/NetworkingClientHandler.cs b/Symcol.Core/Networking/NetworkingClientHandler.cs
new file mode 100644
index 0000000000..4f4b5ccc87
--- /dev/null
+++ b/Symcol.Core/Networking/NetworkingClientHandler.cs
@@ -0,0 +1,637 @@
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Logging;
+using System;
+using System.Collections.Generic;
+
+namespace Symcol.Core.Networking
+{
+ //TODO: This NEEDS its own clock to avoid fuckery later on with DoubleTime and HalfTime
+ public class NetworkingClientHandler : Container
+ {
+ //30 Seconds by default
+ protected virtual double TimeOutTime => 30000;
+
+ protected readonly NetworkingClient ReceiveClient;
+
+ protected readonly NetworkingClient SendClient;
+
+ ///
+ /// Just a client signature basically
+ ///
+ public ClientInfo ClientInfo;
+
+ ///
+ /// All Connecting clients
+ ///
+ public readonly List ConnectingClients = new List();
+
+ ///
+ /// All Connected clients
+ ///
+ public readonly List ConncetedClients = new List();
+
+ ///
+ /// Clients waiting in our match
+ ///
+ public readonly List InMatchClients = new List();
+
+ ///
+ /// Clients loaded and ready to start
+ ///
+ public readonly List LoadedClients = new List();
+
+ ///
+ /// Clients ingame playing
+ ///
+ public readonly List InGameClients = new List();
+
+ ///
+ /// Gets hit when we get a Packet
+ ///
+ public Action OnPacketReceive;
+
+ ///
+ /// (Peer) Call this when we connect to a Host (Includes list of connected peers + Host)
+ ///
+ public Action> OnConnectedToHost;
+
+ ///
+ /// (Host) Whenever a new client Connects
+ ///
+ public Action OnClientConnect;
+
+ ///
+ /// (Host) Whenever a new client Disconnects
+ ///
+ public Action OnClientDisconnect;
+
+ ///
+ /// (Host/Peer) When a new Client joins the game
+ ///
+ public Action OnClientJoin;
+
+ ///
+ /// Receive a full player list
+ ///
+ public Action> OnReceivePlayerList;
+
+ ///
+ /// if we are connected and in a match
+ ///
+ public bool InMatch;
+
+ ///
+ /// Are we in a game
+ ///
+ public bool InGame;
+
+ ///
+ /// Are we loaded and ready to start?
+ ///
+ public bool Loaded;
+
+ ///
+ /// Called to leave an in-progress game
+ ///
+ public Action OnAbort;
+
+ ///
+ /// Called to load the game
+ ///
+ public Action> OnLoadGame;
+
+ ///
+ /// Called to start the game once loaded
+ ///
+ public Action StartGame;
+
+ public readonly ClientType ClientType;
+
+ public NetworkingClientHandler(ClientType type, string ip, int port = 25570, string thisLocalIp = "0.0.0.0")
+ {
+ AlwaysPresent = true;
+
+ ClientType = type;
+
+ switch (type)
+ {
+ case ClientType.Host:
+ ReceiveClient = new NetworkingClient(false, ip, port);
+ break;
+ case ClientType.Peer:
+ ReceiveClient = new NetworkingClient(false, thisLocalIp, port);
+ SendClient = new NetworkingClient(true, ip, port);
+ break;
+ case ClientType.Server:
+ throw new NotImplementedException();
+ }
+
+ Logger.Log("Created a RulesetNetworkingClientHandler", LoggingTarget.Network, LogLevel.Verbose);
+
+ if (ClientInfo == null)
+ ClientInfo = new ClientInfo
+ {
+ Port = port
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (ClientType == ClientType.Peer)
+ ConnectToHost();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ PacketRestart:
+ Packet p = null;
+
+ if (ReceiveClient.UdpClient.Available > 0)
+ p = ReceiveClient.ReceivePacket();
+
+ if (p is BasicPacket packet)
+ {
+ //Hosts
+ if (SendClient == null)
+ {
+ if (packet.Disconnect)
+ {
+ OnClientDisconnect?.Invoke(packet.ClientInfo);
+ foreach (ClientInfo client in ConnectingClients)
+ if (client.IP == packet.ClientInfo.IP)
+ {
+ ConnectingClients.Remove(client);
+ Logger.Log("A Connecting Client has Disconnected", LoggingTarget.Network, LogLevel.Verbose);
+ break;
+ }
+ foreach (ClientInfo client in ConncetedClients)
+ if (client.IP == packet.ClientInfo.IP)
+ {
+ ConncetedClients.Remove(client);
+ Logger.Log("A Client has Disconnected", LoggingTarget.Network, LogLevel.Verbose);
+ break;
+ }
+ foreach (ClientInfo client in InMatchClients)
+ if (client.IP == packet.ClientInfo.IP)
+ {
+ InMatchClients.Remove(client);
+ break;
+ }
+ foreach (ClientInfo client in LoadedClients)
+ if (client.IP == packet.ClientInfo.IP)
+ {
+ LoadedClients.Remove(client);
+ break;
+ }
+ foreach (ClientInfo client in InGameClients)
+ if (client.IP == packet.ClientInfo.IP)
+ {
+ InGameClients.Remove(client);
+ break;
+ }
+ }
+
+ if (packet.Connect)
+ {
+ packet.ClientInfo.StartedTestConnectionTime = Time.Current;
+ ConnectingClients.Add(packet.ClientInfo);
+
+ NetworkingClient client = new NetworkingClient(true, packet.ClientInfo.IP, packet.ClientInfo.Port);
+
+ List playerList = new List
+ {
+ ClientInfo
+ };
+
+ foreach (ClientInfo clientInfo in ConncetedClients)
+ playerList.Add(clientInfo);
+
+ client.SendPacket(new BasicPacket(ClientInfo)
+ {
+ PlayerList = playerList,
+ Connect = true
+ });
+
+ Logger.Log("A Client is Connecting. . .", LoggingTarget.Network, LogLevel.Verbose);
+ }
+
+ if (packet.RequestPlayerList)
+ {
+ NetworkingClient client = new NetworkingClient(true, packet.ClientInfo.IP, packet.ClientInfo.Port);
+
+ List playerList = new List
+ {
+ ClientInfo
+ };
+
+ foreach (ClientInfo clientInfo in ConncetedClients)
+ playerList.Add(clientInfo);
+
+ client.SendPacket(new BasicPacket(ClientInfo)
+ {
+ PlayerList = playerList,
+ RequestPlayerList = true
+ });
+
+ Logger.Log("A Client is Connecting. . .", LoggingTarget.Network, LogLevel.Verbose);
+ }
+
+ if (packet.Loaded)
+ foreach (ClientInfo client in InMatchClients)
+ if (client.IP == packet.ClientInfo.IP)
+ {
+ Logger.Log("A Client has Loaded and is ready to start", LoggingTarget.Network, LogLevel.Verbose);
+ InMatchClients.Remove(client);
+ LoadedClients.Add(client);
+ break;
+ }
+
+ if (packet.GameStarted)
+ foreach (ClientInfo client in LoadedClients)
+ if (client.IP == packet.ClientInfo.IP)
+ {
+ Logger.Log("A Client has started!", LoggingTarget.Network, LogLevel.Verbose);
+ LoadedClients.Remove(client);
+ InGameClients.Add(client);
+ break;
+ }
+
+ if (packet.Test)
+ {
+ foreach (ClientInfo client in ConnectingClients)
+ if (client.IP == packet.ClientInfo.IP)
+ {
+ client.Ping = (int)Time.Current - (int)client.StartedTestConnectionTime;
+ ConnectingClients.Remove(client);
+ ConncetedClients.Add(client);
+ InMatchClients.Add(client);
+ OnClientJoin?.Invoke(client);
+ client.LastConnectionTime = Time.Current;
+ client.ConncetionTryCount = 0;
+ Logger.Log("Successfully connected to a Client! Ping: " + client.Ping, LoggingTarget.Network, LogLevel.Verbose);
+ break;
+ }
+ foreach (ClientInfo client in ConncetedClients)
+ if (client.IP == packet.ClientInfo.IP)
+ {
+ client.Ping = (int)Time.Current - (int)client.StartedTestConnectionTime;
+ client.LastConnectionTime = Time.Current;
+ client.ConncetionTryCount = 0;
+ Logger.Log("Successfully maintained connection to a Client! Ping: " + client.Ping, LoggingTarget.Network, LogLevel.Verbose);
+ }
+ }
+ }
+
+ if (InMatchClients.Count == 0 && LoadedClients.Count > 0 && Loaded && !InGame)
+ SendStartGame();
+
+ //Peers
+ else if (SendClient != null)
+ {
+ if (packet.Connect)
+ {
+ if (!InGame && !InMatch)
+ {
+ InMatch = true;
+ OnConnectedToHost?.Invoke(packet.PlayerList);
+ }
+ Logger.Log("Connected to Host!", LoggingTarget.Network, LogLevel.Verbose);
+ }
+
+ if (packet.Test)
+ {
+ SendToHost(new BasicPacket(ClientInfo) { Test = true });
+ Logger.Log("Received connection test info from host, returning. . .", LoggingTarget.Network, LogLevel.Verbose);
+ }
+
+ if (packet.RequestPlayerList)
+ OnReceivePlayerList?.Invoke(packet.PlayerList);
+
+ if (packet.StartGame)
+ {
+ StartGame?.Invoke();
+ SendToHost(new BasicPacket(ClientInfo) { GameStarted = true });
+ InGame = true;
+ }
+
+ if (packet.Abort)
+ {
+ OnAbort?.Invoke();
+ InGame = false;
+ Loaded = false;
+ }
+
+ if (packet.LoadGame)
+ {
+ Logger.Log("Received instructions to LoadGame for " + packet.PlayerList.Count + " players", LoggingTarget.Network, LogLevel.Verbose);
+ OnLoadGame?.Invoke(packet.PlayerList);
+ }
+ }
+ }
+
+ if (p != null)
+ OnPacketReceive?.Invoke(p);
+
+ if (ReceiveClient.UdpClient.Available > 0)
+ goto PacketRestart;
+
+ foreach (ClientInfo client in ConnectingClients)
+ {
+ if (client.LastConnectionTime + TimeOutTime / 10 <= Time.Current && client.ConncetionTryCount == 0)
+ {
+ client.StartedTestConnectionTime = Time.Current;
+ TestConnection(client);
+ }
+
+ if (client.LastConnectionTime + TimeOutTime / 6 <= Time.Current && client.ConncetionTryCount == 1)
+ TestConnection(client);
+
+ if (client.LastConnectionTime + TimeOutTime / 3 <= Time.Current && client.ConncetionTryCount == 2)
+ TestConnection(client);
+
+ if (client.StartedTestConnectionTime + TimeOutTime <= Time.Current)
+ {
+ ConnectingClients.Remove(client);
+ Logger.Log("Connection to a connecting client lost! - " + client.IP + ":" + client.Port, LoggingTarget.Network, LogLevel.Error);
+ break;
+ }
+ }
+
+ foreach (ClientInfo client in ConncetedClients)
+ {
+ if (client.LastConnectionTime + TimeOutTime / 6 <= Time.Current && client.ConncetionTryCount == 0)
+ {
+ client.StartedTestConnectionTime = Time.Current;
+ TestConnection(client);
+ }
+
+ if (client.LastConnectionTime + TimeOutTime / 3 <= Time.Current && client.ConncetionTryCount == 1)
+ TestConnection(client);
+
+ if (client.LastConnectionTime + TimeOutTime / 2 <= Time.Current && client.ConncetionTryCount == 2)
+ TestConnection(client);
+
+ if (client.StartedTestConnectionTime + TimeOutTime <= Time.Current)
+ {
+ ConncetedClients.Remove(client);
+ InGameClients.Remove(client);
+ LoadedClients.Remove(client);
+ InGameClients.Remove(client);
+ Logger.Log("Connection to a connected client lost! - " + client.IP + ":" + client.Port, LoggingTarget.Network, LogLevel.Error);
+ break;
+ }
+ }
+ }
+
+ ///
+ /// Poke!
+ ///
+ ///
+ protected void TestConnection(ClientInfo clientInfo)
+ {
+ clientInfo.ConncetionTryCount++;
+ NetworkingClient client = new NetworkingClient(true, clientInfo.IP, clientInfo.Port);
+ client.SendPacket(new BasicPacket(ClientInfo) { Test = true });
+ Logger.Log("Testing a client's connection - " + clientInfo.IP + ":" + clientInfo.Port, LoggingTarget.Network, LogLevel.Verbose);
+ }
+
+ public void RequestPlayerList()
+ {
+ BasicPacket packet = new BasicPacket(ClientInfo) { RequestPlayerList = true };
+ SendToHost(packet);
+ }
+
+ ///
+ /// Tell peers to start loading game
+ ///
+ public virtual void StartLoadingGame()
+ {
+ if (SendClient == null)
+ {
+ BasicPacket packet = new BasicPacket(ClientInfo) { LoadGame = true };
+
+ foreach (ClientInfo client in InMatchClients)
+ packet.PlayerList.Add(client);
+ packet.PlayerList.Add(ClientInfo);
+
+ SendToInMatchClients(packet);
+
+ OnLoadGame?.Invoke(packet.PlayerList);
+ }
+ else
+ Logger.Log("Called StartLoadingGame - We are not the Host!", LoggingTarget.Network, LogLevel.Verbose);
+ }
+
+ ///
+ /// Call this when the game is Loaded and ready to be started
+ ///
+ public virtual void GameLoaded()
+ {
+ Loaded = true;
+ SendToHost(new BasicPacket(ClientInfo) { Loaded = true });
+ }
+
+ ///
+ /// Connects to the Host
+ ///
+ public virtual void ConnectToHost()
+ {
+ SendToHost(new BasicPacket(ClientInfo) { Connect = true });
+ Logger.Log("Attempting conection to Host. . .", LoggingTarget.Network, LogLevel.Verbose);
+ }
+
+ ///
+ /// Tell peers to start and starts ours
+ ///
+ public virtual void SendStartGame()
+ {
+ if (SendClient == null)
+ {
+ SendToLoadedClients(new BasicPacket(ClientInfo) { StartGame = true });
+ InGame = true;
+ Logger.Log("Sending Start Game", LoggingTarget.Network, LogLevel.Verbose);
+ }
+ StartGame?.Invoke();
+ }
+
+ ///
+ /// Send a Packet to the Host
+ ///
+ ///
+ public void SendToHost(Packet packet)
+ {
+ if (SendClient != null)
+ SendClient.SendPacket(packet);
+ }
+
+ ///
+ /// Send a Packet to all Connecting clients
+ ///
+ ///
+ public void SendToConnectingClients(Packet packet)
+ {
+ if (SendClient == null)
+ foreach (ClientInfo clientInfo in ConnectingClients)
+ {
+ NetworkingClient client = new NetworkingClient(true, clientInfo.IP, clientInfo.Port);
+ client.SendPacket(packet);
+ }
+ }
+
+ ///
+ /// Send a Packet to all clients Connected and waiting
+ ///
+ ///
+ public void SendToConnectedClients(Packet packet)
+ {
+ if (SendClient == null)
+ foreach (ClientInfo clientInfo in ConncetedClients)
+ {
+ NetworkingClient client = new NetworkingClient(true, clientInfo.IP, clientInfo.Port);
+ client.SendPacket(packet);
+ }
+ }
+
+ ///
+ /// Send a Packet to all clients In this Match
+ ///
+ ///
+ public void SendToInMatchClients(Packet packet)
+ {
+ if (SendClient == null)
+ foreach (ClientInfo clientInfo in InMatchClients)
+ {
+ NetworkingClient client = new NetworkingClient(true, clientInfo.IP, clientInfo.Port);
+ client.SendPacket(packet);
+ }
+ }
+
+ ///
+ /// Send a Packet to all clients Loaded
+ ///
+ ///
+ public void SendToLoadedClients(Packet packet)
+ {
+ if (SendClient == null)
+ foreach (ClientInfo clientInfo in LoadedClients)
+ {
+ NetworkingClient client = new NetworkingClient(true, clientInfo.IP, clientInfo.Port);
+ client.SendPacket(packet);
+ }
+ }
+
+ ///
+ /// Send a Packet to all clients InGame
+ ///
+ ///
+ public void SendToInGameClients(Packet packet)
+ {
+ if (SendClient == null)
+ foreach (ClientInfo clientInfo in InGameClients)
+ {
+ NetworkingClient client = new NetworkingClient(true, clientInfo.IP, clientInfo.Port);
+ client.SendPacket(packet);
+ }
+ }
+
+ ///
+ /// Send a Packet to ALL clients we know
+ ///
+ ///
+ public void SendToAllClients(Packet packet)
+ {
+ if (SendClient == null)
+ {
+ SendToConnectingClients(packet);
+ SendToConnectedClients(packet);
+ }
+ }
+
+ ///
+ /// Send tto all but the one that sent it
+ ///
+ ///
+ ///
+ public void ShareWithOtherPeers(Packet packet)
+ {
+ if (SendClient == null)
+ foreach (ClientInfo clientInfo in InGameClients)
+ if (packet.ClientInfo.IP != clientInfo.IP)
+ {
+ NetworkingClient client = new NetworkingClient(true, clientInfo.IP, clientInfo.Port);
+ client.SendPacket(packet);
+ }
+ }
+
+ public virtual void AbortGame()
+ {
+ SendToLoadedClients(new BasicPacket(ClientInfo) { Abort = true });
+ SendToInGameClients(new BasicPacket(ClientInfo) { Abort = true });
+
+ restart:
+ foreach (ClientInfo client in LoadedClients)
+ {
+ LoadedClients.Remove(client);
+ InMatchClients.Add(client);
+ goto restart;
+ }
+ foreach (ClientInfo client in InGameClients)
+ {
+ InGameClients.Remove(client);
+ InMatchClients.Add(client);
+ goto restart;
+ }
+
+ InGame = false;
+ Loaded = false;
+
+ OnAbort?.Invoke();
+ }
+
+ public virtual void Disconnect()
+ {
+ Packet packet = new BasicPacket(ClientInfo) { Disconnect = true };
+
+ OnAbort?.Invoke();
+ InMatch = false;
+ InGame = false;
+ Loaded = false;
+
+ if (SendClient == null)
+ {
+ SendToConnectingClients(packet);
+ SendToConnectedClients(packet);
+ }
+ else
+ SendToHost(packet);
+ }
+
+ ///
+ /// Die
+ ///
+ ///
+ protected override void Dispose(bool isDisposing)
+ {
+ ReceiveClient?.Clear();
+
+ if (SendClient != null)
+ {
+ SendToHost(new BasicPacket(ClientInfo) { Disconnect = true });
+ SendClient.Clear();
+ }
+
+ base.Dispose(isDisposing);
+ }
+ }
+
+ public enum ClientType
+ {
+ Host,
+ Peer,
+ Server
+ }
+}
diff --git a/Symcol.Core/Networking/Packet.cs b/Symcol.Core/Networking/Packet.cs
new file mode 100644
index 0000000000..22e4fa2921
--- /dev/null
+++ b/Symcol.Core/Networking/Packet.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace Symcol.Core.Networking
+{
+ [Serializable]
+ public class Packet
+ {
+ ///
+ /// Just a Signature
+ ///
+ public readonly ClientInfo ClientInfo;
+
+ ///
+ /// Specify starting size of packet for efficiency
+ ///
+ public virtual int PacketSize => 1024;
+
+ public Packet(ClientInfo clientInfo)
+ {
+ ClientInfo = clientInfo;
+ }
+ }
+}
diff --git a/Symcol.Core/Properties/AssemblyInfo.cs b/Symcol.Core/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..a3bfd12cc8
--- /dev/null
+++ b/Symcol.Core/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("symcol.Toys")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("symcol.Toys")]
+[assembly: AssemblyCopyright("Copyright © 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("f34ac16c-e590-4d70-a069-a748326852bf")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Symcol.Core/Symcol.Core.csproj b/Symcol.Core/Symcol.Core.csproj
new file mode 100644
index 0000000000..9076a3e53d
--- /dev/null
+++ b/Symcol.Core/Symcol.Core.csproj
@@ -0,0 +1,94 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {F34AC16C-E590-4D70-A069-A748326852BF}
+ Library
+ Properties
+ Symcol.Core
+ Symcol.Core
+ v4.6.1
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\packages\Cyotek.Drawing.BitmapFont.1.3.4-beta1\lib\net46\Cyotek.Drawing.BitmapFont.dll
+
+
+ ..\packages\ManagedBass.2.0.3\lib\net45\ManagedBass.dll
+
+
+
+ $(SolutionDir)\packages\Newtonsoft.Json.10.0.3\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+ $(SolutionDir)\packages\NUnit.3.8.1\lib\net45\nunit.framework.dll
+ True
+
+
+ $(SolutionDir)\packages\OpenTK.3.0.0-git00009\lib\net20\OpenTK.dll
+ True
+
+
+ $(SolutionDir)\packages\SQLite.Net.Core-PCL.3.1.1\lib\portable-win8+net45+wp8+wpa81+MonoAndroid1+MonoTouch1\SQLite.Net.dll
+ True
+
+
+ $(SolutionDir)\packages\SQLite.Net-PCL.3.1.1\lib\net40\SQLite.Net.Platform.Generic.dll
+ True
+
+
+ $(SolutionDir)\packages\SQLite.Net-PCL.3.1.1\lib\net4\SQLite.Net.Platform.Win32.dll
+ True
+
+
+ $(SolutionDir)\packages\SQLiteNetExtensions.1.3.0\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1\SQLiteNetExtensions.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {c76bf5b3-985e-4d39-95fe-97c9c879b83a}
+ osu.Framework
+
+
+
+
\ No newline at end of file
diff --git a/Symcol.Rulesets.Core/Containers/LinkText.cs b/Symcol.Rulesets.Core/Containers/LinkText.cs
new file mode 100644
index 0000000000..bba164e6dd
--- /dev/null
+++ b/Symcol.Rulesets.Core/Containers/LinkText.cs
@@ -0,0 +1,43 @@
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+namespace Symcol.Rulesets.Core.Containers
+{
+ public class LinkText : OsuSpriteText, IHasTooltip
+ {
+ public string TooltipText => Tooltip;
+
+ public virtual string Tooltip => "";
+
+ private readonly OsuHoverContainer content;
+
+ public override bool HandleKeyboardInput => content.Action != null;
+ public override bool HandleMouseInput => content.Action != null;
+
+ protected override Container Content => content ?? (Container)this;
+
+ public override IEnumerable FlowingChildren => Children;
+
+ public string Url
+ {
+ set
+ {
+ if (value != null)
+ content.Action = () => Process.Start(value);
+ }
+ }
+
+ public LinkText()
+ {
+ AddInternal(content = new OsuHoverContainer
+ {
+ AutoSizeAxes = Axes.Both,
+ });
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Containers/ProfileLink.cs b/Symcol.Rulesets.Core/Containers/ProfileLink.cs
new file mode 100644
index 0000000000..efc3be3a47
--- /dev/null
+++ b/Symcol.Rulesets.Core/Containers/ProfileLink.cs
@@ -0,0 +1,24 @@
+using osu.Game.Users;
+
+namespace Symcol.Rulesets.Core.Containers
+{
+ ///
+ /// TODO: make this more generic
+ ///
+ public class ProfileLink : LinkText
+ {
+ public override string Tooltip => "View profile in browser";
+
+ public ProfileLink(User user, bool maintainer = false)
+ {
+ if (!maintainer)
+ Text = "Ruleset Creator: " + user.Username;
+ else
+ Text = "Ruleset Maintainer: " + user.Username;
+
+ Url = $@"https://osu.ppy.sh/users/{user.Id}";
+ Font = @"Exo2.0-RegularItalic";
+ TextSize = 20;
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Networking/ChatPacket.cs b/Symcol.Rulesets.Core/Multiplayer/Networking/ChatPacket.cs
new file mode 100644
index 0000000000..841de303ca
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Networking/ChatPacket.cs
@@ -0,0 +1,22 @@
+using Symcol.Core.Networking;
+using System;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Networking
+{
+ [Serializable]
+ public class ChatPacket : Packet
+ {
+ public override int PacketSize => 4096;
+
+ public string Author;
+
+ public string AuthorColor;
+
+ public string Message;
+
+ public ChatPacket(ClientInfo clientInfo) : base(clientInfo)
+ {
+
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Networking/RulesetClientInfo.cs b/Symcol.Rulesets.Core/Multiplayer/Networking/RulesetClientInfo.cs
new file mode 100644
index 0000000000..d4cae8a947
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Networking/RulesetClientInfo.cs
@@ -0,0 +1,24 @@
+using Symcol.Core.Networking;
+using System;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Networking
+{
+ ///
+ /// Just a client signature basically
+ ///
+ [Serializable]
+ public class RulesetClientInfo : ClientInfo
+ {
+ public string Username = "";
+
+ public int UserID = -1;
+
+ public string UserPic;
+
+ public string UserBackground;
+
+ public string UserCountry;
+
+ public string CountryFlagName;
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Networking/RulesetNetworkingClientHandler.cs b/Symcol.Rulesets.Core/Multiplayer/Networking/RulesetNetworkingClientHandler.cs
new file mode 100644
index 0000000000..a02662489d
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Networking/RulesetNetworkingClientHandler.cs
@@ -0,0 +1,83 @@
+using osu.Framework.Allocation;
+using osu.Game;
+using osu.Game.Beatmaps;
+using osu.Game.Online.API;
+using Symcol.Core.Networking;
+using System;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Networking
+{
+ //TODO: This NEEDS its own clock to avoid fuckery later on with DoubleTime and HalfTime
+ public class RulesetNetworkingClientHandler : NetworkingClientHandler, IOnlineComponent
+ {
+ public RulesetClientInfo RulesetClientInfo;
+
+ public Action OnMapChange;
+
+ private OsuGame osu;
+
+ public RulesetNetworkingClientHandler(ClientType type, string ip, int port = 25570, string thisLocalIp = "0.0.0.0") : base(type, ip, port, thisLocalIp)
+ {
+ if (RulesetClientInfo == null)
+ {
+ RulesetClientInfo = new RulesetClientInfo
+ {
+ Port = port
+ };
+
+ ClientInfo = RulesetClientInfo;
+ }
+ }
+
+ ///
+ /// Send Map to Peers
+ ///
+ ///
+ public void SetMap(WorkingBeatmap map)
+ {
+ RulesetPacket packet;
+ try
+ {
+ packet = new RulesetPacket(RulesetClientInfo)
+ {
+ OnlineBeatmapSetID = (int)map.BeatmapSetInfo.OnlineBeatmapSetID,
+ OnlineBeatmapID = (int)map.BeatmapInfo.OnlineBeatmapID
+ };
+ SendToInMatchClients(packet);
+ OnMapChange?.Invoke(osu.Beatmap.Value);
+ }
+ catch
+ {
+ packet = new RulesetPacket(RulesetClientInfo);
+ SendToInMatchClients(packet);
+ return;
+ }
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(APIAccess api, OsuGame osu)
+ {
+ api.Register(this);
+ this.osu = osu;
+ }
+
+ public void APIStateChanged(APIAccess api, APIState state)
+ {
+ switch (state)
+ {
+ default:
+ RulesetClientInfo.Username = "";
+ RulesetClientInfo.UserID = -1;
+ break;
+ case APIState.Online:
+ RulesetClientInfo.Username = api.LocalUser.Value.Username;
+ RulesetClientInfo.UserID = (int)api.LocalUser.Value.Id;
+ RulesetClientInfo.UserCountry = api.LocalUser.Value.Country.FullName;
+ RulesetClientInfo.CountryFlagName = api.LocalUser.Value.Country.FlagName;
+ RulesetClientInfo.UserPic = api.LocalUser.Value.AvatarUrl;
+ RulesetClientInfo.UserBackground = api.LocalUser.Value.CoverUrl;
+ break;
+ }
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Networking/RulesetPacket.cs b/Symcol.Rulesets.Core/Multiplayer/Networking/RulesetPacket.cs
new file mode 100644
index 0000000000..ab2f510378
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Networking/RulesetPacket.cs
@@ -0,0 +1,28 @@
+using Symcol.Core.Networking;
+using System;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Networking
+{
+ [Serializable]
+ public class RulesetPacket : Packet
+ {
+ public new readonly RulesetClientInfo ClientInfo;
+
+ public override int PacketSize => 4096;
+
+ public int OnlineBeatmapSetID = -1;
+
+ public int OnlineBeatmapID = -1;
+
+ public bool HaveMap;
+
+ public string ChatContent;
+
+ //public string RulesetName = "";
+
+ public RulesetPacket(RulesetClientInfo rulesetClientInfo) : base(rulesetClientInfo)
+ {
+ ClientInfo = rulesetClientInfo;
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Options/MultiplayerDropdownEnumOption.cs b/Symcol.Rulesets.Core/Multiplayer/Options/MultiplayerDropdownEnumOption.cs
new file mode 100644
index 0000000000..a6eaa75b0c
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Options/MultiplayerDropdownEnumOption.cs
@@ -0,0 +1,43 @@
+using osu.Framework.Configuration;
+using osu.Game.Overlays.Settings;
+using osu.Framework.Graphics;
+using osu.Game.Graphics.UserInterface;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Options
+{
+ public class MultiplayerDropdownEnumOption : MultiplayerOption
+ where T : struct
+ {
+ public readonly Bindable BindableEnum;
+
+ public MultiplayerDropdownEnumOption(Bindable bindable, string name, int quadrant, bool sync = true) : base(name, quadrant, sync)
+ {
+ BindableEnum = bindable;
+
+ OptionContainer.Child = new BetterSettingsEnumDropdown
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ RelativeSizeAxes = Axes.X,
+ Bindable = bindable,
+ };
+ }
+
+ private class BetterSettingsEnumDropdown : SettingsEnumDropdown
+ {
+ protected override Drawable CreateControl() => new BetterOsuEnumDropdown
+ {
+ Margin = new MarginPadding { Top = 5 },
+ RelativeSizeAxes = Axes.X,
+ };
+
+ private class BetterOsuEnumDropdown : OsuEnumDropdown
+ {
+ public BetterOsuEnumDropdown()
+ {
+ Menu.MaxHeight = 160;
+ }
+ }
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Options/MultiplayerOption.cs b/Symcol.Rulesets.Core/Multiplayer/Options/MultiplayerOption.cs
new file mode 100644
index 0000000000..8130e6a151
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Options/MultiplayerOption.cs
@@ -0,0 +1,87 @@
+using OpenTK;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using System;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Options
+{
+ public abstract class MultiplayerOption : Container
+ {
+ protected readonly SpriteText Title;
+
+ protected readonly Container OptionContainer;
+
+ public MultiplayerOption(string name, int quadrant, bool sync = true)
+ {
+ if (quadrant == 1 | quadrant == 3 | quadrant == 5 | quadrant == 7)
+ {
+ switch (quadrant)
+ {
+ case 1:
+ quadrant = 0;
+ break;
+ case 3:
+ quadrant = 1;
+ break;
+ case 5:
+ quadrant = 2;
+ break;
+ case 7:
+ quadrant = 3;
+ break;
+ }
+
+ Anchor = Anchor.TopLeft;
+ Origin = Anchor.TopLeft;
+ Position = new Vector2(16, 4 + (64 * quadrant));
+ }
+ else if (quadrant == 2 | quadrant == 4 | quadrant == 6 | quadrant == 8)
+ {
+ switch (quadrant)
+ {
+ case 2:
+ quadrant = 0;
+ break;
+ case 4:
+ quadrant = 1;
+ break;
+ case 6:
+ quadrant = 2;
+ break;
+ case 8:
+ quadrant = 3;
+ break;
+ }
+
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopLeft;
+ Position = new Vector2(22, 4 + (64 * quadrant));
+ }
+ else
+ throw new Exception("Globglogabgalab");
+
+ RelativeSizeAxes = Axes.X;
+ Width = 0.49f;
+ Height = 80;
+
+ Children = new Drawable[]
+ {
+ Title = new SpriteText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ TextSize = 20,
+ Text = name
+ },
+ OptionContainer = new Container
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Position = new Vector2(-16, 18),
+ RelativeSizeAxes = Axes.Both,
+ }
+ };
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Options/MultiplayerToggleOption.cs b/Symcol.Rulesets.Core/Multiplayer/Options/MultiplayerToggleOption.cs
new file mode 100644
index 0000000000..619dd8c4af
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Options/MultiplayerToggleOption.cs
@@ -0,0 +1,27 @@
+using osu.Framework.Configuration;
+using osu.Game.Overlays.Settings;
+using osu.Framework.Graphics;
+using OpenTK;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Options
+{
+ public class MultiplayerToggleOption : MultiplayerOption
+ {
+ public readonly Bindable BindableBool;
+
+ public MultiplayerToggleOption(Bindable bindable, string name, int quadrant, bool sync = true) : base(name, quadrant, sync)
+ {
+ BindableBool = bindable;
+
+ Child = new SettingsCheckbox
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ RelativeSizeAxes = Axes.X,
+ Bindable = bindable,
+ LabelText = " " + name,
+ Position = new Vector2(-16, 18),
+ };
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Pieces/Chat.cs b/Symcol.Rulesets.Core/Multiplayer/Pieces/Chat.cs
new file mode 100644
index 0000000000..8844400b61
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Pieces/Chat.cs
@@ -0,0 +1,148 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Logging;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Users;
+using Symcol.Core.Networking;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Pieces
+{
+ public class Chat : Container, IOnlineComponent
+ {
+ private readonly RulesetNetworkingClientHandler rulesetNetworkingClientHandler;
+
+ private string playerColorHex = SymcolSettingsSubsection.SymcolConfigManager.GetBindable(SymcolSetting.PlayerColor);
+
+ private User user;
+
+ private readonly FillFlowContainer messageContainer;
+ private readonly OsuTextBox textBox;
+
+ public Chat(RulesetNetworkingClientHandler rulesetNetworkingClientHandler)
+ {
+ this.rulesetNetworkingClientHandler = rulesetNetworkingClientHandler;
+
+ rulesetNetworkingClientHandler.OnPacketReceive += (packet) =>
+ {
+ if (packet is ChatPacket chatPacket)
+ Add(chatPacket);
+ if (rulesetNetworkingClientHandler.ClientType == ClientType.Host)
+ rulesetNetworkingClientHandler.ShareWithOtherPeers(packet);
+ };
+
+ Anchor = Anchor.BottomCentre;
+ Origin = Anchor.BottomCentre;
+ RelativeSizeAxes = Axes.Both;
+ Height = 0.46f;
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ Alpha = 0.8f
+ },
+ new OsuScrollContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.9f,
+
+ Children = new Drawable[]
+ {
+ messageContainer = new FillFlowContainer
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }
+ }
+ },
+ textBox = new OsuTextBox
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.98f,
+ Height = 36,
+ Position = new Vector2(0, -12),
+ Colour = Color4.White,
+ Text = "Type here!"
+ }
+ };
+
+ textBox.OnCommit += (s, r) =>
+ {
+ AddMessage(textBox.Text);
+ textBox.Text = "";
+ };
+ }
+
+ public void Add(ChatPacket packet)
+ {
+ ChatMessage message = new ChatMessage(packet);
+ messageContainer.Add(message);
+ }
+
+ public void AddMessage(string message)
+ {
+ if (message == "" | message == " ")
+ return;
+
+ if (user != null)
+ {
+ try
+ {
+ OsuColour.FromHex(playerColorHex);
+ }
+ catch
+ {
+ playerColorHex = "#ffffff";
+ }
+
+ ChatPacket packet = new ChatPacket(rulesetNetworkingClientHandler.ClientInfo)
+ {
+ Author = user.Username,
+ AuthorColor = playerColorHex,
+ Message = message,
+ };
+
+ rulesetNetworkingClientHandler.SendToHost(packet);
+ rulesetNetworkingClientHandler.SendToInMatchClients(packet);
+ Add(packet);
+ }
+ else
+ Logger.Log("You must be logged in to message!", LoggingTarget.Network, LogLevel.Error);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(APIAccess api)
+ {
+ api.Register(this);
+ }
+
+ public void APIStateChanged(APIAccess api, APIState state)
+ {
+ switch (state)
+ {
+ default:
+ user = null;
+ break;
+ case APIState.Online:
+ user = api.LocalUser.Value;
+ break;
+ }
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Pieces/ChatMessage.cs b/Symcol.Rulesets.Core/Multiplayer/Pieces/ChatMessage.cs
new file mode 100644
index 0000000000..efca32349c
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Pieces/ChatMessage.cs
@@ -0,0 +1,42 @@
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Pieces
+{
+ public class ChatMessage : Container
+ {
+ public ChatMessage(ChatPacket packet)
+ {
+ Anchor = Anchor.TopLeft;
+ Origin = Anchor.TopLeft;
+
+ RelativeSizeAxes = Axes.X;
+ AutoSizeAxes = Axes.Y;
+
+ Children = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Colour = OsuColour.FromHex(packet.AuthorColor),
+ TextSize = 24,
+ Text = packet.Author + ":"
+ },
+ new OsuTextFlowContainer(t => { t.TextSize = 24; })
+ {
+ Position = new OpenTK.Vector2(140, 0),
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Text = packet.Message
+ }
+ };
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Pieces/MatchPlayer.cs b/Symcol.Rulesets.Core/Multiplayer/Pieces/MatchPlayer.cs
new file mode 100644
index 0000000000..2f4762f5b2
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Pieces/MatchPlayer.cs
@@ -0,0 +1,123 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.UserInterface;
+using osu.Framework.Input;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Users;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+using System.Diagnostics;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Pieces
+{
+ public class MatchPlayer : ClickableContainer, IHasContextMenu
+ {
+ public readonly RulesetClientInfo ClientInfo;
+
+ private readonly Box dim;
+
+ private readonly DrawableFlag countryFlag;
+ private readonly UserCoverBackground profileBackground;
+ private readonly UpdateableAvatar profilePicture;
+
+ public MatchPlayer(RulesetClientInfo clientInfo)
+ {
+ ClientInfo = clientInfo;
+
+ Alpha = 0;
+ Masking = true;
+ RelativeSizeAxes = Axes.X;
+ Height = 40f;
+ CornerRadius = 10;
+
+ Country country = new Country
+ {
+ FullName = ClientInfo.UserCountry,
+ FlagName = ClientInfo.CountryFlagName,
+ };
+
+ User user = new User
+ {
+ Username = ClientInfo.Username,
+ Id = ClientInfo.UserID,
+ Country = country,
+ AvatarUrl = ClientInfo.UserPic,
+ CoverUrl = ClientInfo.UserBackground,
+ };
+
+ Children = new Drawable[]
+ {
+ profileBackground = new UserCoverBackground(user)
+ {
+ RelativeSizeAxes = Axes.Both,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ FillMode = FillMode.Fill,
+ OnLoadComplete = d => d.FadeInFromZero(200),
+ },
+ dim = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ Alpha = 0.8f
+ },
+ profilePicture = new UpdateableAvatar
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Size = new Vector2(Height * 0.8f),
+ Position = new Vector2(6, 0),
+ User = user,
+ Masking = true,
+ CornerRadius = 6,
+ },
+ countryFlag = new DrawableFlag(country)
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Size = new Vector2(Height * 0.9f, (Height * 0.9f) * 0.66f),
+ Position = new Vector2(-10, 0)
+ },
+ new SpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Position = new Vector2(Height * 1.1f, 0),
+ TextSize = Height * 0.9f,
+ Text = user.Username
+ }
+ };
+
+ Action = () =>
+ {
+ Process.Start("https://osu.ppy.sh/users/" + user.Id);
+ };
+ }
+
+ protected override bool OnHover(InputState state)
+ {
+ dim.FadeTo(0.6f, 200);
+
+ return base.OnHover(state);
+ }
+
+ protected override void OnHoverLost(InputState state)
+ {
+ base.OnHoverLost(state);
+
+ dim.FadeTo(0.8f, 200);
+ }
+
+ public MenuItem[] ContextMenuItems => new MenuItem[]
+ {
+ new OsuMenuItem("View Profile", MenuItemType.Standard, () => { }),
+ new OsuMenuItem("Promote to Host", MenuItemType.Highlighted, () => { }),
+ new OsuMenuItem("Kick", MenuItemType.Destructive, () => { }),
+ new OsuMenuItem("Ban", MenuItemType.Destructive, () => { }),
+ };
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Pieces/MatchPlayerList.cs b/Symcol.Rulesets.Core/Multiplayer/Pieces/MatchPlayerList.cs
new file mode 100644
index 0000000000..0aa332c1fd
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Pieces/MatchPlayerList.cs
@@ -0,0 +1,110 @@
+using OpenTK.Graphics;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using System.Collections.Generic;
+using osu.Framework.Graphics;
+using OpenTK;
+using Symcol.Core.Networking;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Pieces
+{
+ public class MatchPlayerList : Container
+ {
+ private readonly RulesetNetworkingClientHandler rulesetNetworkingClientHandler;
+
+ public readonly List MatchPlayers = new List();
+
+ public readonly FillFlowContainer MatchPlayersContianer;
+
+ public MatchPlayerList(RulesetNetworkingClientHandler rulesetNetworkingClientHandler)
+ {
+ this.rulesetNetworkingClientHandler = rulesetNetworkingClientHandler;
+
+ Masking = true;
+ CornerRadius = 16;
+ Anchor = Anchor.TopLeft;
+ Origin = Anchor.TopLeft;
+ RelativeSizeAxes = Axes.Both;
+ Width = 0.49f;
+ Height = 0.45f;
+ Position = new Vector2(10);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black.Opacity(0.8f)
+ },
+ MatchPlayersContianer = new FillFlowContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.98f,
+ Height = 0.96f
+ }
+ };
+
+ rulesetNetworkingClientHandler.OnReceivePlayerList += (players) =>
+ {
+ restart:
+ foreach (MatchPlayer matchPlayer in MatchPlayers)
+ foreach (ClientInfo clientInfo in players)
+ if (clientInfo is RulesetClientInfo rulesetClientInfo)
+ if (rulesetClientInfo.IP + rulesetClientInfo.Port != matchPlayer.ClientInfo.IP + matchPlayer.ClientInfo.Port)
+ {
+ Add(rulesetClientInfo);
+ players.Remove(clientInfo);
+ goto restart;
+ }
+ };
+ rulesetNetworkingClientHandler.RequestPlayerList();
+
+ rulesetNetworkingClientHandler.OnClientJoin += (clientInfo) =>
+ {
+ foreach (MatchPlayer matchPlayer in MatchPlayers)
+ if (clientInfo is RulesetClientInfo rulesetClientInfo)
+ if (rulesetClientInfo.IP + rulesetClientInfo.Port != matchPlayer.ClientInfo.IP + matchPlayer.ClientInfo.Port)
+ {
+ Add(rulesetClientInfo);
+ break;
+ }
+ };
+
+ rulesetNetworkingClientHandler.OnClientDisconnect += (clientInfo) =>
+ {
+ foreach (MatchPlayer matchPlayer in MatchPlayers)
+ if (clientInfo is RulesetClientInfo rulesetClientInfo)
+ if (rulesetClientInfo.IP + rulesetClientInfo.Port == matchPlayer.ClientInfo.IP + matchPlayer.ClientInfo.Port)
+ {
+ Remove(matchPlayer);
+ break;
+ }
+ };
+ }
+
+ public void Add(RulesetClientInfo clientInfo)
+ {
+ MatchPlayer matchPlayer = new MatchPlayer(clientInfo);
+
+ Add(matchPlayer);
+ }
+
+ public void Add(MatchPlayer matchPlayer)
+ {
+ MatchPlayers.Add(matchPlayer);
+ MatchPlayersContianer.Add(matchPlayer);
+ matchPlayer.FadeInFromZero(200);
+ }
+
+ public void Remove(MatchPlayer matchPlayer)
+ {
+ MatchPlayers.Remove(matchPlayer);
+ matchPlayer.FadeOutFromOne(200)
+ .Expire();
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Pieces/MatchTools.cs b/Symcol.Rulesets.Core/Multiplayer/Pieces/MatchTools.cs
new file mode 100644
index 0000000000..8e8d10ff6b
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Pieces/MatchTools.cs
@@ -0,0 +1,372 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Configuration;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Input;
+using osu.Framework.MathUtils;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Screens.Symcol.Pieces;
+using Symcol.Rulesets.Core.Multiplayer.Options;
+using System;
+using System.Diagnostics;
+using System.Linq;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Pieces
+{
+ public class MatchTools : Container
+ {
+ public readonly Bindable Mode = new Bindable() { Default = MatchScreenMode.MapDetails };
+
+ public readonly Bindable GameMode = new Bindable() { Default = MatchGamemode.HeadToHead };
+
+ public readonly OsuTabControl TabControl;
+
+ public readonly Container SelectedContent;
+
+ public readonly Container MapDetails;
+
+ public Container RulesetSettings;
+
+ public readonly Container SoundBoard;
+
+ private WorkingBeatmap selectedBeatmap;
+
+ private int selectedBeatmapSetID;
+
+ public MatchTools()
+ {
+ Masking = true;
+ CornerRadius = 16;
+ Anchor = Anchor.TopRight;
+ Origin = Anchor.TopRight;
+ RelativeSizeAxes = Axes.Both;
+ Width = 0.49f;
+ Height = 0.45f;
+ Position = new Vector2(-10, 10);
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black.Opacity(0.8f)
+ },
+ TabControl = new OsuTabControl
+ {
+ Position = new Vector2(36, 0),
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.08f,
+ Width = 0.6f
+ },
+ SelectedContent = new Container
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.Both,
+ Height = 0.92f
+ }
+ };
+ TabControl.Current.Value = MatchScreenMode.MapDetails;
+
+ Mode.ValueChanged += (value) =>
+ {
+ switch (value)
+ {
+ case MatchScreenMode.MapDetails:
+ if (selectedBeatmap != null)
+ SelectedContent.Child = new MapDetailsSection(selectedBeatmap);
+ else if (selectedBeatmapSetID != 0)
+ SelectedContent.Child = new MapDetailsSection(selectedBeatmapSetID);
+ else
+ SelectedContent.Child = new MapDetailsSection(true);
+ break;
+ case MatchScreenMode.MatchSettings:
+ SelectedContent.Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+
+ Children = new Drawable[]
+ {
+ new MultiplayerDropdownEnumOption(GameMode, "Match Gamemode", 1)
+ }
+ };
+ break;
+ case MatchScreenMode.SoundBoard:
+ SelectedContent.Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Child = new HitSoundBoard
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ ButtonSize = 80
+ }
+ };
+ break;
+ }
+ };
+ Mode.BindTo(TabControl.Current);
+ }
+
+ public void MapChange(WorkingBeatmap workingBeatmap)
+ {
+ if (workingBeatmap == null)
+ {
+ MapChange(-1);
+ return;
+ }
+
+ selectedBeatmap = workingBeatmap;
+ selectedBeatmapSetID = (int)workingBeatmap.BeatmapSetInfo.OnlineBeatmapSetID;
+
+ if (Mode.Value == MatchScreenMode.MapDetails)
+ SelectedContent.Child = new MapDetailsSection(selectedBeatmap);
+ }
+
+ public void MapChange(int onlineBeatmapSetID)
+ {
+ selectedBeatmap = null;
+ selectedBeatmapSetID = onlineBeatmapSetID;
+
+ if (Mode.Value == MatchScreenMode.MapDetails)
+ {
+ if (selectedBeatmapSetID != 0 && selectedBeatmapSetID != -1)
+ SelectedContent.Child = new MapDetailsSection(selectedBeatmapSetID);
+ else
+ SelectedContent.Child = new MapDetailsSection(true);
+ }
+ }
+ }
+
+ public class MapDetailsSection : ClickableContainer
+ {
+ private Sprite beatmapBG;
+ private SpriteText name;
+ private SpriteText artist;
+ private SpriteText difficulty;
+ private SpriteText time;
+
+ private Box dim;
+
+ public MapDetailsSection(WorkingBeatmap workingBeatmap)
+ {
+ draw();
+
+ HitObject lastObject = workingBeatmap.Beatmap.HitObjects.LastOrDefault();
+ double endTime = (lastObject as IHasEndTime)?.EndTime ?? lastObject?.StartTime ?? 0;
+
+ beatmapBG.Texture = workingBeatmap.Background;
+ name.Text = workingBeatmap.BeatmapSetInfo.Metadata.Title;
+ artist.Text = "By: " + workingBeatmap.BeatmapSetInfo.Metadata.Artist;
+ difficulty.Text = workingBeatmap.BeatmapInfo.Version + " (" + Math.Round(workingBeatmap.BeatmapInfo.StarDifficulty, 2) + " stars) mapped by " + workingBeatmap.BeatmapInfo.Metadata.AuthorString;
+ time.Text = getBPMRange(workingBeatmap.Beatmap) + " bpm for " + TimeSpan.FromMilliseconds(endTime - workingBeatmap.Beatmap.HitObjects.First().StartTime).ToString(@"m\:ss");
+
+ BorderColour = getColour(workingBeatmap.BeatmapInfo);
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Radius = 16,
+ Type = EdgeEffectType.Shadow,
+ Colour = getColour(workingBeatmap.BeatmapInfo).Opacity(0.2f)
+ };
+ Action = () => Process.Start("https://osu.ppy.sh/beatmapsets/" + workingBeatmap.BeatmapSetInfo.OnlineBeatmapSetID);
+ }
+
+ public MapDetailsSection(int onlineBeatmapSetID)
+ {
+ draw();
+ name.Text = "Missing Map!";
+ artist.Text = "Click to open in Browser";
+ Action = () => Process.Start("https://osu.ppy.sh/beatmapsets/" + onlineBeatmapSetID);
+ }
+
+ public MapDetailsSection(bool invalid)
+ {
+ draw();
+ name.Text = "Invalid / No Map Selected!";
+ artist.Text = "Don't hit start, weird things might happen";
+ Action = () => Process.Start("https://osu.ppy.sh/home");
+ }
+
+ private void draw()
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ RelativeSizeAxes = Axes.Both;
+
+ Width = 0.95f;
+ Height = 0.9f;
+
+ Masking = true;
+ BorderColour = Color4.LightBlue;
+ BorderThickness = 4;
+ CornerRadius = 10;
+
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Radius = 16,
+ Type = EdgeEffectType.Shadow,
+ Colour = Color4.LightBlue.Opacity(0.2f)
+ };
+
+ Children = new Drawable[]
+ {
+ beatmapBG = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fill,
+ },
+ dim = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ Alpha = 0.6f
+ },
+ name = new SpriteText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Position = new Vector2(10, 0),
+ Font = @"Exo2.0-SemiBoldItalic",
+ TextSize = 40
+ },
+ artist = new SpriteText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Position = new Vector2(10, 38),
+ Font = @"Exo2.0-MediumItalic",
+ TextSize = 24
+ },
+ difficulty = new SpriteText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Position = new Vector2(10, 64),
+ Font = "Exo2.0-Bold",
+ TextSize = 16
+ },
+ time = new SpriteText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Position = new Vector2(10, 84),
+ TextSize = 16
+ }
+ };
+ }
+
+ protected override bool OnHover(InputState state)
+ {
+ dim.FadeTo(0.4f, 200);
+
+ return base.OnHover(state);
+ }
+
+ protected override void OnHoverLost(InputState state)
+ {
+ base.OnHoverLost(state);
+
+ dim.FadeTo(0.6f, 200);
+ }
+
+ //"Borrowed" stuff
+ private string getBPMRange(Beatmap beatmap)
+ {
+ double bpmMax = beatmap.ControlPointInfo.BPMMaximum;
+ double bpmMin = beatmap.ControlPointInfo.BPMMinimum;
+
+ if (Precision.AlmostEquals(bpmMin, bpmMax))
+ return $"{bpmMin:0}";
+
+ return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.ControlPointInfo.BPMMode:0})";
+ }
+
+ private enum DifficultyRating
+ {
+ Easy,
+ Normal,
+ Hard,
+ Insane,
+ Expert,
+ ExpertPlus
+ }
+
+ private DifficultyRating getDifficultyRating(BeatmapInfo beatmap)
+ {
+ if (beatmap == null)
+ throw new ArgumentNullException(nameof(beatmap));
+
+ var rating = beatmap.StarDifficulty;
+
+ if (rating < 1.5) return DifficultyRating.Easy;
+ if (rating < 2.25) return DifficultyRating.Normal;
+ if (rating < 3.75) return DifficultyRating.Hard;
+ if (rating < 5.25) return DifficultyRating.Insane;
+ if (rating < 6.75) return DifficultyRating.Expert;
+ return DifficultyRating.ExpertPlus;
+ }
+
+ private Color4 getColour(BeatmapInfo beatmap)
+ {
+ OsuColour palette = new OsuColour();
+ switch (getDifficultyRating(beatmap))
+ {
+ case DifficultyRating.Easy:
+ return palette.Green;
+ default:
+ case DifficultyRating.Normal:
+ return palette.Blue;
+ case DifficultyRating.Hard:
+ return palette.Yellow;
+ case DifficultyRating.Insane:
+ return palette.Pink;
+ case DifficultyRating.Expert:
+ return palette.Purple;
+ case DifficultyRating.ExpertPlus:
+ return palette.Gray0;
+ }
+ }
+ }
+
+ public enum MatchGamemode
+ {
+ [System.ComponentModel.Description("Head to Head")]
+ HeadToHead,
+ [System.ComponentModel.Description("Head to Head with Live Spectator")]
+ HeadToHeadSpectator,
+ [System.ComponentModel.Description("Team Versus")]
+ TeamVS,
+ [System.ComponentModel.Description("TAG4")]
+ TAG4,
+ [System.ComponentModel.Description("Team TAG4")]
+ TeamTAG4,
+ [System.ComponentModel.Description("Tourny Mode")]
+ Tournement,
+ }
+
+ public enum MatchScreenMode
+ {
+ [System.ComponentModel.Description("Map Details")]
+ MapDetails,
+ [System.ComponentModel.Description("Match Settings")]
+ MatchSettings,
+ [System.ComponentModel.Description("Ruleset Settings")]
+ RulesetSettings,
+ [System.ComponentModel.Description("Sound Board")]
+ SoundBoard
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Screens/MatchSongSelect.cs b/Symcol.Rulesets.Core/Multiplayer/Screens/MatchSongSelect.cs
new file mode 100644
index 0000000000..018e31315f
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Screens/MatchSongSelect.cs
@@ -0,0 +1,47 @@
+using osu.Game.Beatmaps;
+using osu.Game.Screens.Select;
+using System;
+using osu.Game.Screens;
+using osu.Framework.Screens;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Screens
+{
+ public class MatchSongSelect : SongSelect
+ {
+ public WorkingBeatmap SelectedMap;
+
+ private bool exiting;
+
+ protected override BackgroundScreen CreateBackground() => null;
+
+ public Action Action;
+
+ public readonly RulesetNetworkingClientHandler RulesetNetworkingClientHandler;
+
+ public MatchSongSelect(RulesetNetworkingClientHandler rulesetNetworkingClientHandler)
+ {
+ RulesetNetworkingClientHandler = rulesetNetworkingClientHandler;
+ }
+
+ protected override void OnEntering(Screen last)
+ {
+ Add(RulesetNetworkingClientHandler);
+ base.OnEntering(last);
+ }
+
+ protected override bool OnSelectionFinalised()
+ {
+ if (!exiting)
+ {
+ RulesetNetworkingClientHandler.OnMapChange?.Invoke(null);
+ SelectedMap = Beatmap.Value;
+ Action();
+ exiting = true;
+ Remove(RulesetNetworkingClientHandler);
+ Exit();
+ }
+ return true;
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Screens/MultiPlayer.cs b/Symcol.Rulesets.Core/Multiplayer/Screens/MultiPlayer.cs
new file mode 100644
index 0000000000..0503553883
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Screens/MultiPlayer.cs
@@ -0,0 +1,375 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using osu.Framework.Allocation;
+using osu.Framework.Audio;
+using osu.Framework.Audio.Sample;
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
+using osu.Framework.Input;
+using osu.Framework.Logging;
+using osu.Framework.Screens;
+using osu.Framework.Threading;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Cursor;
+using osu.Game.Online.API;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Screens.Backgrounds;
+using osu.Game.Screens.Ranking;
+using osu.Game.Storyboards.Drawables;
+using OpenTK;
+using osu.Game.Screens;
+using osu.Game.Screens.Play;
+using OpenTK.Input;
+using osu.Game.Screens.Play.BreaksOverlay;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Screens
+{
+ public class MultiPlayer : ScreenWithBeatmapBackground, IProvideCursor
+ {
+ public readonly RulesetNetworkingClientHandler RulesetNetworkingClientHandler;
+
+ protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap);
+
+ protected override float BackgroundParallaxAmount => 0.1f;
+
+ public override bool ShowOverlaysOnEnter => false;
+
+ public Action RestartRequested;
+
+ public override bool AllowBeatmapRulesetChange => false;
+
+ public bool HasFailed { get; private set; }
+
+ public bool AllowPause { get; set; } = false;
+ public bool AllowLeadIn { get; set; } = true;
+ public bool AllowResults { get; set; } = true;
+
+ public int RestartCount;
+
+ public CursorContainer Cursor => RulesetContainer.Cursor;
+ public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value;
+
+ private IAdjustableClock sourceClock;
+ private DecoupleableInterpolatingFramedClock adjustableClock;
+
+ private RulesetInfo ruleset;
+
+ private APIAccess api;
+
+ private ScoreProcessor scoreProcessor;
+ protected RulesetContainer RulesetContainer;
+
+ #region User Settings
+
+ private Bindable dimLevel;
+ private Bindable blurLevel;
+ private Bindable showStoryboard;
+ private Bindable mouseWheelDisabled;
+ private Bindable userAudioOffset;
+
+ private SampleChannel sampleRestart;
+
+ #endregion
+
+ private Container storyboardContainer;
+ private DrawableStoryboard storyboard;
+
+ private HUDOverlay hudOverlay;
+
+ private bool loadedSuccessfully => RulesetContainer?.Objects.Any() == true;
+
+ public MultiPlayer(RulesetNetworkingClientHandler rulesetNetworkingClientHandler)//, WorkingBeatmap beatmap = null)
+ {
+ RulesetNetworkingClientHandler = rulesetNetworkingClientHandler;
+ RulesetNetworkingClientHandler.OnAbort = () => Exit();
+ RulesetNetworkingClientHandler.StartGame = () => start();
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
+ {
+ this.api = api;
+
+ dimLevel = config.GetBindable(OsuSetting.DimLevel);
+ blurLevel = config.GetBindable(OsuSetting.BlurLevel);
+ showStoryboard = config.GetBindable(OsuSetting.ShowStoryboard);
+
+ mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel);
+
+ sampleRestart = audio.Sample.Get(@"Gameplay/restart");
+ userAudioOffset = config.GetBindable(OsuSetting.AudioOffset);
+
+ WorkingBeatmap working = Beatmap.Value;
+ Beatmap beatmap;
+
+ try
+ {
+ beatmap = working.Beatmap;
+
+ if (beatmap == null)
+ throw new InvalidOperationException("Beatmap was not loaded");
+
+ ruleset = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset;
+ var rulesetInstance = ruleset.CreateInstance();
+
+ try
+ {
+ RulesetContainer = rulesetInstance.CreateRulesetContainerWith(working, ruleset.ID == beatmap.BeatmapInfo.Ruleset.ID);
+ }
+ catch (BeatmapInvalidForRulesetException)
+ {
+ // we may fail to create a RulesetContainer if the beatmap cannot be loaded with the user's preferred ruleset
+ // let's try again forcing the beatmap's ruleset.
+ ruleset = beatmap.BeatmapInfo.Ruleset;
+ rulesetInstance = ruleset.CreateInstance();
+ RulesetContainer = rulesetInstance.CreateRulesetContainerWith(Beatmap, true);
+ }
+
+ if (!RulesetContainer.Objects.Any())
+ throw new InvalidOperationException("Beatmap contains no hit objects!");
+ }
+ catch (Exception e)
+ {
+ Logger.Error(e, "Could not load beatmap sucessfully!");
+
+ //couldn't load, hard abort!
+ Exit();
+ return;
+ }
+
+ sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock();
+ adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
+
+ var firstObjectTime = RulesetContainer.Objects.First().StartTime;
+ adjustableClock.Seek(AllowLeadIn
+ ? Math.Min(0, firstObjectTime - Math.Max(beatmap.ControlPointInfo.TimingPointAt(firstObjectTime).BeatLength * 4, beatmap.BeatmapInfo.AudioLeadIn))
+ : firstObjectTime);
+
+ adjustableClock.ProcessFrame();
+
+ // the final usable gameplay clock with user-set offsets applied.
+ var offsetClock = new FramedOffsetClock(adjustableClock);
+
+ userAudioOffset.ValueChanged += v => offsetClock.Offset = v;
+ userAudioOffset.TriggerChange();
+
+ scoreProcessor = RulesetContainer.CreateScoreProcessor();
+
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Clock = offsetClock,
+ Children = new Drawable[]
+ {
+ storyboardContainer = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0,
+ },
+ RulesetContainer,
+ hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, working, offsetClock, adjustableClock)
+ {
+ Clock = Clock, // hud overlay doesn't want to use the audio clock directly
+ ProcessCustomClock = false,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ },
+ new BreakOverlay(beatmap.BeatmapInfo.LetterboxInBreaks, scoreProcessor)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ ProcessCustomClock = false,
+ Breaks = beatmap.Breaks
+ }
+ }
+ }
+ };
+
+ if (showStoryboard)
+ initializeStoryboard(false);
+
+ // Bind ScoreProcessor to ourselves
+ scoreProcessor.AllJudged += onCompletion;
+ scoreProcessor.Failed += onFail;
+
+ foreach (var mod in Beatmap.Value.Mods.Value.OfType())
+ mod.ApplyToScoreProcessor(scoreProcessor);
+ }
+
+ private void applyRateFromMods()
+ {
+ if (sourceClock == null) return;
+
+ sourceClock.Rate = 1;
+ foreach (var mod in Beatmap.Value.Mods.Value.OfType())
+ mod.ApplyToClock(sourceClock);
+ }
+
+ private void initializeStoryboard(bool asyncLoad)
+ {
+ if (storyboardContainer == null)
+ return;
+
+ var beatmap = Beatmap.Value;
+
+ storyboard = beatmap.Storyboard.CreateDrawable();
+ storyboard.Masking = true;
+
+ if (asyncLoad)
+ LoadComponentAsync(storyboard, storyboardContainer.Add);
+ else
+ storyboardContainer.Add(storyboard);
+ }
+
+ private ScheduledDelegate onCompletionEvent;
+
+ private void onCompletion()
+ {
+ // Only show the completion screen if the player hasn't failed
+ if (scoreProcessor.HasFailed || onCompletionEvent != null)
+ return;
+
+ ValidForResume = false;
+
+ if (!AllowResults) return;
+
+ using (BeginDelayedSequence(1000))
+ {
+ onCompletionEvent = Schedule(delegate
+ {
+ if (!IsCurrentScreen) return;
+
+ var score = new Score
+ {
+ Beatmap = Beatmap.Value.BeatmapInfo,
+ Ruleset = ruleset
+ };
+ scoreProcessor.PopulateScore(score);
+ score.User = RulesetContainer.Replay?.User ?? api.LocalUser.Value;
+ Push(new Results(score));
+ });
+ }
+ }
+
+ private bool onFail()
+ {
+ if (Beatmap.Value.Mods.Value.OfType().Any(m => !m.AllowFail))
+ return false;
+
+ HasFailed = true;
+ return true;
+ }
+
+ protected override void OnEntering(Screen last)
+ {
+ base.OnEntering(last);
+
+ Add(RulesetNetworkingClientHandler);
+
+ if (!loadedSuccessfully)
+ return;
+
+ Content.Alpha = 0;
+ Content
+ .ScaleTo(0.7f)
+ .ScaleTo(1, 750, Easing.OutQuint)
+ .Delay(250)
+ .FadeIn(250);
+
+ Task.Run(() =>
+ {
+ sourceClock.Reset();
+
+ Schedule(() =>
+ {
+ adjustableClock.ChangeSource(sourceClock);
+ applyRateFromMods();
+
+ this.Delay(750).Schedule(() =>
+ {
+ Logger.Log("Client finnished loading", LoggingTarget.Network, LogLevel.Verbose);
+ RulesetNetworkingClientHandler.GameLoaded();
+ });
+ });
+ });
+ }
+
+ private void start()
+ {
+ adjustableClock.Start();
+ }
+
+ protected override void OnSuspending(Screen next)
+ {
+ fadeOut();
+ base.OnSuspending(next);
+ }
+
+ protected override bool OnExiting(Screen next)
+ {
+ Remove(RulesetNetworkingClientHandler);
+ fadeOut();
+ return base.OnExiting(next);
+ }
+
+ protected override void UpdateBackgroundElements()
+ {
+ if (!IsCurrentScreen) return;
+
+ base.UpdateBackgroundElements();
+
+ if (ShowStoryboard && storyboard == null)
+ initializeStoryboard(true);
+
+ var beatmap = Beatmap.Value;
+ var storyboardVisible = ShowStoryboard && beatmap.Storyboard.HasDrawable;
+
+ storyboardContainer?
+ .FadeColour(OsuColour.Gray(BackgroundOpacity), BACKGROUND_FADE_DURATION, Easing.OutQuint)
+ .FadeTo(storyboardVisible && BackgroundOpacity > 0 ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint);
+
+ if (storyboardVisible && beatmap.Storyboard.ReplacesBackground)
+ Background?.FadeTo(0, BACKGROUND_FADE_DURATION, Easing.OutQuint);
+ }
+
+ private void fadeOut()
+ {
+ const float fade_out_duration = 250;
+
+ RulesetContainer?.FadeOut(fade_out_duration);
+ Content.FadeOut(fade_out_duration);
+
+ hudOverlay?.ScaleTo(0.7f, fade_out_duration * 3, Easing.In);
+
+ Background?.FadeTo(1f, fade_out_duration);
+ }
+
+ protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
+ {
+ if (args.Key == Key.Escape)
+ BackOut();
+
+ return base.OnKeyDown(state, args);
+ }
+
+ public void BackOut()
+ {
+ RulesetNetworkingClientHandler.AbortGame();
+ RulesetNetworkingClientHandler.OnAbort();
+ }
+
+ protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value;
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Screens/RulesetLobbyScreen.cs b/Symcol.Rulesets.Core/Multiplayer/Screens/RulesetLobbyScreen.cs
new file mode 100644
index 0000000000..ba7ad6d0e8
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Screens/RulesetLobbyScreen.cs
@@ -0,0 +1,228 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Overlays.Settings;
+using osu.Game.Screens;
+using System;
+using osu.Framework.Screens;
+using System.Collections.Generic;
+using Symcol.Core.Networking;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Screens
+{
+ public abstract class RulesetLobbyScreen : OsuScreen
+ {
+ public abstract string RulesetName { get; }
+
+ public abstract RulesetMatchScreen MatchScreen { get; }
+
+ public RulesetNetworkingClientHandler RulesetNetworkingClientHandler;
+
+ public readonly SettingsButton HostGameButton;
+ public readonly SettingsButton DirectConnectButton;
+ public readonly SettingsButton JoinGameButton;
+
+ public readonly Container NewGame;
+ protected readonly TextBox HostIP;
+ protected readonly TextBox HostPort;
+ //protected readonly TextBox PublicIp;
+ protected readonly TextBox LocalIp;
+
+ public readonly Container JoinIP;
+
+ public RulesetLobbyScreen()
+ {
+ AlwaysPresent = true;
+ RelativeSizeAxes = Axes.Both;
+
+ Children = new Drawable[]
+ {
+ HostGameButton = new SettingsButton
+ {
+ Anchor = Anchor.BottomLeft,
+ Origin = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.3f,
+ Text = "Host Game",
+ Action = HostGame
+ },
+ DirectConnectButton = new SettingsButton
+ {
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.BottomCentre,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.3f,
+ Text = "Direct Connect",
+ Action = DirectConnect
+ },
+ JoinGameButton = new SettingsButton
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.3f,
+ Text = "Join Game"
+ },
+ NewGame = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ Size = new Vector2(400, 300),
+
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ Colour = Color4.Blue,
+ RelativeSizeAxes = Axes.Both
+ },
+ new Box
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Colour = Color4.Black,
+ Alpha = 0.9f,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.48f,
+ Height = 20,
+ },
+ HostIP = new TextBox
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.48f,
+ Height = 20,
+ Text = "Host IP Address"
+ },
+ new Box
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ Colour = Color4.Black,
+ Alpha = 0.9f,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.48f,
+ Height = 20,
+ },
+ HostPort = new TextBox
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.48f,
+ Height = 20,
+ Text = "25570"
+ },
+ /*
+ new Box
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Colour = Color4.Black,
+ Alpha = 0.9f,
+ RelativeSizeAxes = Axes.X,
+ Position = new Vector2(0, 22),
+ Width = 0.48f,
+ Height = 20,
+ },
+ PublicIp = new TextBox
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ RelativeSizeAxes = Axes.X,
+ Position = new Vector2(0, 22),
+ Width = 0.48f,
+ Height = 20,
+ Text = "You're Public IP Address"
+ },
+ */
+ new Box
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Colour = Color4.Black,
+ Alpha = 0.9f,
+ RelativeSizeAxes = Axes.X,
+ Position = new Vector2(0, 44),
+ Width = 0.48f,
+ Height = 20,
+ },
+ LocalIp = new TextBox
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ RelativeSizeAxes = Axes.X,
+ Position = new Vector2(0, 44),
+ Width = 0.48f,
+ Height = 20,
+ Text = "You're Local IP Address"
+ }
+ }
+ }
+ };
+ }
+
+ protected override void OnEntering(Screen last)
+ {
+ base.OnEntering(last);
+ MakeCurrent();
+ }
+
+ protected override void OnResuming(Screen last)
+ {
+ base.OnResuming(last);
+ MakeCurrent();
+ }
+
+ protected override bool OnExiting(Screen next)
+ {
+ if (RulesetNetworkingClientHandler != null)
+ {
+ Remove(RulesetNetworkingClientHandler);
+ RulesetNetworkingClientHandler.Dispose();
+ }
+
+ return base.OnExiting(next);
+ }
+
+ protected virtual void HostGame()
+ {
+ if (RulesetNetworkingClientHandler != null)
+ {
+ Remove(RulesetNetworkingClientHandler);
+ RulesetNetworkingClientHandler.Dispose();
+ }
+ Add(RulesetNetworkingClientHandler = new RulesetNetworkingClientHandler(ClientType.Host, LocalIp.Text, Int32.Parse(HostPort.Text)));
+
+ List list = new List();
+ list.Add(RulesetNetworkingClientHandler.RulesetClientInfo);
+
+ JoinMatch(list);
+ }
+
+ protected virtual void DirectConnect()
+ {
+ if (RulesetNetworkingClientHandler != null)
+ {
+ Remove(RulesetNetworkingClientHandler);
+ RulesetNetworkingClientHandler.Dispose();
+ }
+ Add(RulesetNetworkingClientHandler = new RulesetNetworkingClientHandler(ClientType.Peer, HostIP.Text, Int32.Parse(HostPort.Text), LocalIp.Text));
+
+ RulesetNetworkingClientHandler.OnConnectedToHost += (p) => JoinMatch(p);
+ }
+
+ protected virtual void JoinMatch(List clientInfos)
+ {
+ Remove(RulesetNetworkingClientHandler);
+ MakeCurrent();
+ Push(MatchScreen);
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Screens/RulesetMatchScreen.cs b/Symcol.Rulesets.Core/Multiplayer/Screens/RulesetMatchScreen.cs
new file mode 100644
index 0000000000..c9927e6b6b
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Screens/RulesetMatchScreen.cs
@@ -0,0 +1,150 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Screens;
+using osu.Game;
+using osu.Game.Beatmaps;
+using osu.Game.Overlays.Settings;
+using osu.Game.Screens;
+using Symcol.Core.Networking;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+using Symcol.Rulesets.Core.Multiplayer.Pieces;
+using System.Collections.Generic;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Screens
+{
+ public abstract class RulesetMatchScreen : OsuScreen
+ {
+ public readonly RulesetNetworkingClientHandler RulesetNetworkingClientHandler;
+
+ private readonly MatchPlayerList playerList;
+
+ private BeatmapManager beatmaps;
+
+ protected MatchTools MatchTools;
+
+ private readonly Chat chat;
+
+ public RulesetMatchScreen(RulesetNetworkingClientHandler rulesetNetworkingClientHandler)
+ {
+ RulesetNetworkingClientHandler = rulesetNetworkingClientHandler;
+
+ Children = new Drawable[]
+ {
+ new SettingsButton
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.35f,
+ Text = "Leave",
+ Action = () => Exit()
+ },
+ new SettingsButton
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.3f,
+ Text = "Open Song Select",
+ Action = () => openSongSelect()
+ },
+ new SettingsButton
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.35f,
+ Text = "Start Match",
+ Action = () => RulesetNetworkingClientHandler.StartLoadingGame()
+ },
+ playerList = new MatchPlayerList(RulesetNetworkingClientHandler),
+ MatchTools = new MatchTools(),
+ chat = new Chat(RulesetNetworkingClientHandler)
+ };
+
+ RulesetNetworkingClientHandler.OnPacketReceive += (Packet packet) =>
+ {
+ if (packet is RulesetPacket rulesetPacket && rulesetPacket.OnlineBeatmapID != -1)
+ foreach (BeatmapSetInfo beatmapSet in beatmaps.GetAllUsableBeatmapSets())
+ if (beatmapSet.OnlineBeatmapSetID == rulesetPacket.OnlineBeatmapSetID)
+ {
+ foreach (BeatmapInfo beatmap in beatmapSet.Beatmaps)
+ if (beatmap.OnlineBeatmapID == rulesetPacket.OnlineBeatmapID)
+ {
+ Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap, Beatmap.Value);
+ Beatmap.Value.Track.Start();
+ MatchTools.MapChange(Beatmap);
+ RulesetNetworkingClientHandler.OnMapChange?.Invoke(Beatmap);
+ break;
+ }
+ break;
+ }
+ else
+ MatchTools.MapChange(rulesetPacket.OnlineBeatmapSetID);
+ };
+
+ RulesetNetworkingClientHandler.OnMapChange += (beatmap) => MatchTools.MapChange(beatmap);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+ playerList.Add(RulesetNetworkingClientHandler.RulesetClientInfo);
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(BeatmapManager beatmaps)
+ {
+ this.beatmaps = beatmaps;
+ }
+
+ protected override void OnEntering(Screen last)
+ {
+ base.OnEntering(last);
+ MakeCurrent();
+ Add(RulesetNetworkingClientHandler);
+ RulesetNetworkingClientHandler.OnLoadGame = (i) => Load(i);
+ }
+
+ protected override void OnResuming(Screen last)
+ {
+ base.OnResuming(last);
+ MakeCurrent();
+ if (RulesetNetworkingClientHandler != null)
+ Add(RulesetNetworkingClientHandler);
+ }
+
+ protected override void OnSuspending(Screen next)
+ {
+ base.OnSuspending(next);
+ Remove(RulesetNetworkingClientHandler);
+ }
+
+ protected override bool OnExiting(Screen next)
+ {
+ RulesetNetworkingClientHandler.Disconnect();
+ Remove(RulesetNetworkingClientHandler);
+ RulesetNetworkingClientHandler.Dispose();
+
+ return base.OnExiting(next);
+ }
+
+ protected virtual void Load(List playerList)
+ {
+ MakeCurrent();
+ }
+
+ private void openSongSelect()
+ {
+ MatchSongSelect songSelect = new MatchSongSelect(RulesetNetworkingClientHandler);
+ MakeCurrent();
+ Push(songSelect);
+ songSelect.Action = () => RulesetNetworkingClientHandler.SetMap(songSelect.SelectedMap);
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Multiplayer/Screens/RulesetMultiplayerSelection.cs b/Symcol.Rulesets.Core/Multiplayer/Screens/RulesetMultiplayerSelection.cs
new file mode 100644
index 0000000000..5bda26ddfb
--- /dev/null
+++ b/Symcol.Rulesets.Core/Multiplayer/Screens/RulesetMultiplayerSelection.cs
@@ -0,0 +1,99 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Screens;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Screens;
+using osu.Game.Screens.Symcol;
+
+namespace Symcol.Rulesets.Core.Multiplayer.Screens
+{
+ public class RulesetMultiplayerSelection : OsuScreen
+ {
+ public static readonly FillFlowContainer LobbyItems = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Width = 0.85f,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ };
+
+ public RulesetMultiplayerSelection()
+ {
+ RelativeSizeAxes = Axes.Both;
+
+ Add(LobbyItems);
+ }
+
+ protected override void OnEntering(Screen last)
+ {
+ base.OnEntering(last);
+ foreach (RulesetLobbyItem item in LobbyItems)
+ item.Action = () => Push(item.RulesetLobbyScreen);
+ }
+
+ protected override bool OnExiting(Screen next)
+ {
+ Remove(LobbyItems);
+ SymcolSettingsSubsection.RulesetMultiplayerSelection = new RulesetMultiplayerSelection();
+ SymcolMenu.RulesetMultiplayerScreen = SymcolSettingsSubsection.RulesetMultiplayerSelection;
+ return base.OnExiting(next);
+ }
+ }
+
+ public abstract class RulesetLobbyItem : ClickableContainer
+ {
+ public abstract Texture Icon { get; }
+
+ public abstract string RulesetName { get; }
+
+ public virtual Texture Background { get; }
+
+ public abstract RulesetLobbyScreen RulesetLobbyScreen { get; }
+
+ public RulesetLobbyItem()
+ {
+ CornerRadius = 20;
+ Masking = true;
+ RelativeSizeAxes = Axes.X;
+ Height = 100;
+
+ Children = new Drawable[]
+ {
+ new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fill,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = Background
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ Alpha = 0.5f,
+ },
+ new Sprite
+ {
+ Size = new Vector2(Height),
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ Texture = Icon
+ },
+ new OsuSpriteText
+ {
+ Anchor = Anchor.BottomRight,
+ Origin = Anchor.BottomRight,
+ Text = RulesetName,
+ TextSize = 60,
+ Position = new Vector2(-20, 0)
+ }
+ };
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Properties/AssemblyInfo.cs b/Symcol.Rulesets.Core/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..08debf866f
--- /dev/null
+++ b/Symcol.Rulesets.Core/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Symcol.Rulesets.Core")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Symcol.Rulesets.Core")]
+[assembly: AssemblyCopyright("Copyright © Shawdooow 2018")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("552b5940-c647-4060-aa4d-61baac415c72")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Symcol.Rulesets.Core/Skinning/SkinElement.cs b/Symcol.Rulesets.Core/Skinning/SkinElement.cs
new file mode 100644
index 0000000000..f5ab9a5d13
--- /dev/null
+++ b/Symcol.Rulesets.Core/Skinning/SkinElement.cs
@@ -0,0 +1,97 @@
+using osu.Framework.Configuration;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.IO.Stores;
+using osu.Framework.Platform;
+
+namespace Symcol.Rulesets.Core.Skinning
+{
+ public abstract class SkinElement : Container
+ {
+ private static string loadedSkin;
+
+ private static ResourceStore skinResources;
+ private static TextureStore skinTextures;
+
+ ///
+ /// Will attempt to get a skin element fron the skin, if no element is found return the default element
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static Texture GetSkinElement(TextureStore stockTextures, Bindable skin, string fileName, Storage storage)
+ {
+ Texture texture = null;
+
+ string fileNameHd = fileName + "@2x";
+
+ Storage skinStorage = storage.GetStorageForDirectory("Skins\\" + skin);
+
+ if (skin.Value == "default")
+ {
+ texture = stockTextures.Get(fileName + ".png");
+
+ if (texture == null)
+ texture = stockTextures.Get(fileNameHd + ".png");
+
+ return texture;
+ }
+
+ if (loadedSkin != skin.ToString())
+ {
+ loadedSkin = skin.ToString();
+ skinResources = new ResourceStore(new StorageBackedResourceStore(skinStorage));
+ skinTextures = new TextureStore(new RawTextureLoaderStore(skinResources));
+ }
+
+ if (skinStorage.Exists(fileNameHd + ".png"))
+ texture = skinTextures.Get(fileNameHd + ".png");
+ else if (skinStorage.Exists(fileName + ".png"))
+ {
+ texture = skinTextures.Get(fileName + ".png");
+ texture.ScaleAdjust = 1f;
+ }
+ else
+ texture = stockTextures.Get(fileNameHd + ".png");
+
+ return texture;
+ }
+
+ ///
+ /// Will attempt to get a skin element from the skin, if no element is found return null
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static Texture GetElement(Bindable skin, string fileName, Storage storage)
+ {
+ Texture texture = null;
+
+ string fileNameHd = fileName + "@2x";
+
+ Storage skinStorage = storage.GetStorageForDirectory("Skins\\" + skin);
+
+ if (loadedSkin != skin.ToString())
+ {
+ loadedSkin = skin.ToString();
+ skinResources = new ResourceStore(new StorageBackedResourceStore(skinStorage));
+ skinTextures = new TextureStore(new RawTextureLoaderStore(skinResources));
+ }
+
+ if (skinStorage.Exists(fileNameHd + ".png"))
+ texture = skinTextures.Get(fileNameHd + ".png");
+ else if (skinStorage.Exists(fileName + ".png"))
+ {
+ texture = skinTextures.Get(fileName + ".png");
+ texture.ScaleAdjust = 1f;
+ }
+ else
+ texture = null;
+
+ return texture;
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Skinning/SkinIniReader.cs b/Symcol.Rulesets.Core/Skinning/SkinIniReader.cs
new file mode 100644
index 0000000000..56a3c752a0
--- /dev/null
+++ b/Symcol.Rulesets.Core/Skinning/SkinIniReader.cs
@@ -0,0 +1,25 @@
+using osu.Framework.Configuration;
+using osu.Framework.Platform;
+
+namespace Symcol.Rulesets.Core.Skinning
+{
+ public class SkinConfigReader : IniConfigManager
+ where T : struct
+ {
+ protected override string Filename => @"skin.ini";
+
+ public SkinConfigReader(Storage storage) : base(storage) { }
+
+ protected override bool PerformSave() { return false; }
+ }
+
+ //wildly incomplete
+ public enum ClassicIniParameters
+ {
+ Name,
+ Author,
+ CursorRotate,
+ CursorExpand,
+ CursorCentre
+ }
+}
diff --git a/Symcol.Rulesets.Core/Symcol.Rulesets.Core.csproj b/Symcol.Rulesets.Core/Symcol.Rulesets.Core.csproj
new file mode 100644
index 0000000000..ef0b7530ca
--- /dev/null
+++ b/Symcol.Rulesets.Core/Symcol.Rulesets.Core.csproj
@@ -0,0 +1,98 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {552B5940-C647-4060-AA4D-61BAAC415C72}
+ Library
+ Properties
+ Symcol.Rulesets.Core
+ Symcol.Rulesets.Core
+ v4.6.1
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {c76bf5b3-985e-4d39-95fe-97c9c879b83a}
+ osu.Framework
+
+
+ {d9a367c9-4c1a-489f-9b05-a0cea2b53b58}
+ osu.Game.Resources
+
+
+ {2a66dd92-adb1-4994-89e2-c94e04acda0d}
+ osu.Game
+
+
+ {F34AC16C-E590-4D70-A069-A748326852BF}
+ Symcol.Core
+
+
+
+
\ No newline at end of file
diff --git a/Symcol.Rulesets.Core/SymcolConfigManager.cs b/Symcol.Rulesets.Core/SymcolConfigManager.cs
new file mode 100644
index 0000000000..267096c3aa
--- /dev/null
+++ b/Symcol.Rulesets.Core/SymcolConfigManager.cs
@@ -0,0 +1,22 @@
+using osu.Framework.Configuration;
+using osu.Framework.Platform;
+
+namespace Symcol.Rulesets.Core
+{
+ public class SymcolConfigManager : IniConfigManager
+ {
+ protected override string Filename => "symcol.ini";
+
+ public SymcolConfigManager(Storage storage) : base(storage) { }
+
+ protected override void InitialiseDefaults()
+ {
+ Set(SymcolSetting.PlayerColor, "#ffffff");
+ }
+ }
+
+ public enum SymcolSetting
+ {
+ PlayerColor
+ }
+}
diff --git a/Symcol.Rulesets.Core/SymcolInputManager.cs b/Symcol.Rulesets.Core/SymcolInputManager.cs
new file mode 100644
index 0000000000..d374c79a17
--- /dev/null
+++ b/Symcol.Rulesets.Core/SymcolInputManager.cs
@@ -0,0 +1,19 @@
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Bindings;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.UI;
+using Symcol.Rulesets.Core.VectorVideos;
+
+namespace Symcol.Rulesets.Core
+{
+ public class SymcolInputManager : RulesetInputManager
+ where T : struct
+ {
+ protected virtual bool VectorVideo => false;
+
+ public SymcolInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) : base(ruleset, variant, unique)
+ {
+ Child = new VectorVideo();
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/SymcolPlayfield.cs b/Symcol.Rulesets.Core/SymcolPlayfield.cs
new file mode 100644
index 0000000000..7d5f223cbf
--- /dev/null
+++ b/Symcol.Rulesets.Core/SymcolPlayfield.cs
@@ -0,0 +1,15 @@
+using OpenTK;
+using osu.Game.Rulesets.UI;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+
+namespace Symcol.Rulesets.Core
+{
+ public class SymcolPlayfield : Playfield
+ {
+ public static RulesetNetworkingClientHandler RulesetNetworkingClientHandler;
+
+ public SymcolPlayfield(Vector2 size) : base(size.X)
+ {
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/SymcolSettingsSubsection.cs b/Symcol.Rulesets.Core/SymcolSettingsSubsection.cs
new file mode 100644
index 0000000000..ee9f457668
--- /dev/null
+++ b/Symcol.Rulesets.Core/SymcolSettingsSubsection.cs
@@ -0,0 +1,50 @@
+using osu.Framework.Allocation;
+using osu.Game;
+using osu.Game.Overlays.Settings;
+using Symcol.Rulesets.Core.Wiki;
+using osu.Game.Screens.Symcol;
+using Symcol.Rulesets.Core.Multiplayer.Screens;
+using osu.Framework.Platform;
+
+namespace Symcol.Rulesets.Core
+{
+ public abstract class SymcolSettingsSubsection : SettingsSubsection
+ {
+ public virtual WikiOverlay Wiki => null;
+
+ public virtual RulesetLobbyItem RulesetLobbyItem => null;
+
+ public static RulesetMultiplayerSelection RulesetMultiplayerSelection;
+
+ public static SymcolConfigManager SymcolConfigManager;
+
+ private OsuGame osu;
+
+ public SymcolSettingsSubsection()
+ {
+ if (RulesetLobbyItem != null)
+ RulesetMultiplayerSelection.LobbyItems.Add(RulesetLobbyItem);
+
+ if (RulesetMultiplayerSelection == null)
+ RulesetMultiplayerSelection = new RulesetMultiplayerSelection();
+ SymcolMenu.RulesetMultiplayerScreen = RulesetMultiplayerSelection;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGame osu, Storage storage)
+ {
+ this.osu = osu;
+
+ if (SymcolConfigManager == null)
+ SymcolConfigManager = new SymcolConfigManager(storage);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (Wiki != null)
+ osu.Add(Wiki);
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/VectorVideos/VectorVideo.cs b/Symcol.Rulesets.Core/VectorVideos/VectorVideo.cs
new file mode 100644
index 0000000000..47ed04bf9d
--- /dev/null
+++ b/Symcol.Rulesets.Core/VectorVideos/VectorVideo.cs
@@ -0,0 +1,59 @@
+using OpenTK;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Logging;
+using osu.Framework.Platform;
+using osu.Game.Graphics.Containers;
+
+namespace Symcol.Rulesets.Core.VectorVideos
+{
+ public class VectorVideo : BeatSyncedContainer
+ {
+ public const string FILE_NAME = "VectorVideo.symcol";
+
+ [BackgroundDependencyLoader]
+ private void load(Storage storage)
+ {
+ }
+
+ protected void LoadContent(string args)
+ {
+ string[] parameters = args.Split(',');
+
+ ObjectType objectType = ObjectType.LogoVisualizer;
+ Anchor anchor = Anchor.Centre;
+ Anchor origin = Anchor.Centre;
+
+ bool checkingType = false;
+
+ foreach (string parameter in parameters)
+ {
+ string[] subParameters = parameter.Split('=');
+
+ foreach (string subParameter in subParameters)
+ {
+ if (subParameter == "Type")
+ checkingType = true;
+
+ if (checkingType)
+ switch (subParameter)
+ {
+ case "LogoVisualizer":
+ objectType = ObjectType.LogoVisualizer;
+ break;
+ }
+ }
+ }
+ }
+
+ private void loadLogoVisualizer()
+ {
+
+ }
+ }
+
+ public enum ObjectType
+ {
+ LogoVisualizer
+ }
+}
diff --git a/Symcol.Rulesets.Core/Wiki/WikiHeader.cs b/Symcol.Rulesets.Core/Wiki/WikiHeader.cs
new file mode 100644
index 0000000000..0bd0ab9185
--- /dev/null
+++ b/Symcol.Rulesets.Core/Wiki/WikiHeader.cs
@@ -0,0 +1,114 @@
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Users;
+using osu.Game.Graphics.Containers;
+using OpenTK;
+using osu.Framework.Graphics.Shapes;
+using OpenTK.Graphics;
+using Symcol.Rulesets.Core.Containers;
+
+namespace Symcol.Rulesets.Core.Wiki
+{
+ public abstract class WikiHeader : Container
+ {
+ protected abstract Texture RulesetIcon { get; }
+
+ protected abstract string RulesetName { get; }
+
+ protected abstract string RulesetDescription { get; }
+
+ protected virtual string RulesetUrl => $@"https://osu.ppy.sh/home";
+
+ protected virtual User Creator => null;
+
+ protected virtual User Maintainer => null;
+
+ protected virtual string DiscordInvite => $@"https://discord.gg/ppy";
+
+ protected virtual Texture HeaderBackground => null;
+
+ private const float description_height = 150;
+ private const float description_width = 220;
+ private const float icon_size = 200;
+ private const float header_margin = 50;
+ private const float rulesetname_height = 60;
+
+ public WikiHeader()
+ {
+ Masking = true;
+ RelativeSizeAxes = Axes.X;
+ Height = header_margin + icon_size + rulesetname_height;
+
+
+ var user = Creator;
+ bool maintainer = false;
+ string userTitle = "Creator";
+ if (Creator == null)
+ {
+ user = Maintainer;
+ maintainer = true;
+ userTitle = "Maintainer";
+ }
+
+ Children = new Drawable[]
+ {
+ new Sprite
+ {
+ RelativeSizeAxes = Axes.Both,
+ FillMode = FillMode.Fill,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Texture = HeaderBackground
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4.Black,
+ Alpha = 0.5f,
+ },
+ new Sprite
+ {
+ Size = new Vector2(icon_size),
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Texture = RulesetIcon
+ },
+ new LinkText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Position = new Vector2(10, icon_size),
+ Text = RulesetName,
+ Url = RulesetUrl,
+ Font = @"Exo2.0-RegularItalic",
+ TextSize = rulesetname_height
+ },
+ new ProfileLink(user, maintainer)
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Position = new Vector2(10, icon_size + rulesetname_height),
+ },
+ new LinkText
+ {
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ Position = new Vector2(10, icon_size + rulesetname_height + 20),
+ Text = userTitle + "'s Discord server",
+ Url = DiscordInvite,
+ Font = @"Exo2.0-RegularItalic",
+ TextSize = 16
+ },
+ new OsuTextFlowContainer(t => { t.TextSize = 20; })
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Size = new Vector2(description_width, description_height),
+ Text = RulesetDescription
+ }
+ };
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Wiki/WikiOptionEnumExplanation.cs b/Symcol.Rulesets.Core/Wiki/WikiOptionEnumExplanation.cs
new file mode 100644
index 0000000000..761114b58c
--- /dev/null
+++ b/Symcol.Rulesets.Core/Wiki/WikiOptionEnumExplanation.cs
@@ -0,0 +1,78 @@
+using OpenTK;
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Overlays.Settings;
+
+namespace Symcol.Rulesets.Core.Wiki
+{
+ public class WikiOptionEnumExplanation : Container
+ where T : struct
+ {
+ public OsuTextFlowContainer Description;
+
+ public WikiOptionEnumExplanation(Bindable bindable)
+ {
+ OsuColour osu = new OsuColour();
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+ AutoSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
+ Masking = true;
+
+ Children = new Drawable[]
+ {
+ new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = osu.Yellow,
+ Masking = true,
+ RelativeSizeAxes = Axes.Y,
+ Size = new Vector2(10, 0.98f),
+ CornerRadius = 5,
+
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ new Container
+ {
+ Position = new Vector2(-10, 0),
+ Anchor = Anchor.TopLeft,
+ Origin = Anchor.TopLeft,
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.45f,
+
+ Child = new SettingsEnumDropdown
+ {
+ Bindable = bindable
+ }
+ },
+ new Container
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ RelativeSizeAxes = Axes.X,
+ Width = 0.45f,
+ AutoSizeAxes = Axes.Y,
+ AutoSizeDuration = 100,
+ AutoSizeEasing = Easing.OutQuint,
+
+ Child = Description = new OsuTextFlowContainer(t => { t.TextSize = 20; })
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Wiki/WikiOverlay.cs b/Symcol.Rulesets.Core/Wiki/WikiOverlay.cs
new file mode 100644
index 0000000000..ede85c054f
--- /dev/null
+++ b/Symcol.Rulesets.Core/Wiki/WikiOverlay.cs
@@ -0,0 +1,147 @@
+using OpenTK.Graphics;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Containers;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Overlays;
+using System.Linq;
+
+namespace Symcol.Rulesets.Core.Wiki
+{
+ public abstract class WikiOverlay : WaveOverlayContainer
+ {
+ protected abstract WikiHeader Header { get; }
+ protected abstract WikiSection[] Sections { get; }
+
+ private WikiSection lastSection;
+ private SectionsContainer sectionsContainer;
+ private WikiTabControl tabs;
+
+ public const float CONTENT_X_MARGIN = 100;
+
+ public WikiOverlay()
+ {
+ FirstWaveColour = OsuColour.Gray(0.4f);
+ SecondWaveColour = OsuColour.Gray(0.3f);
+ ThirdWaveColour = OsuColour.Gray(0.2f);
+ FourthWaveColour = OsuColour.Gray(0.1f);
+
+ RelativeSizeAxes = Axes.Both;
+ RelativePositionAxes = Axes.Both;
+ Width = 0.85f;
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+
+ Masking = true;
+ AlwaysPresent = true;
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Colour = Color4.Black.Opacity(0),
+ Type = EdgeEffectType.Shadow,
+ Radius = 10
+ };
+
+ tabs = new WikiTabControl
+ {
+ RelativeSizeAxes = Axes.X,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Height = 30
+ };
+
+ Add(new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = OsuColour.Gray(0.2f)
+ });
+
+ Add(sectionsContainer = new SectionsContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ ExpandableHeader = Header,
+ FixedHeader = tabs,
+ HeaderBackground = new Box
+ {
+ Colour = OsuColour.Gray(34),
+ RelativeSizeAxes = Axes.Both
+ }
+ });
+
+ sectionsContainer.SelectedSection.ValueChanged += s =>
+ {
+ if (lastSection != s)
+ {
+ lastSection = s;
+ tabs.Current.Value = lastSection;
+ }
+ };
+
+ tabs.Current.ValueChanged += s =>
+ {
+ if (lastSection == null)
+ {
+ lastSection = sectionsContainer.Children.FirstOrDefault();
+ if (lastSection != null)
+ tabs.Current.Value = lastSection;
+ return;
+ }
+ if (lastSection != s)
+ {
+ lastSection = s;
+ sectionsContainer.ScrollTo(lastSection);
+ }
+ };
+
+ foreach (WikiSection sec in Sections)
+ {
+ if (sec != null)
+ {
+ sectionsContainer.Add(sec);
+ tabs.AddItem(sec);
+ }
+ }
+
+ sectionsContainer.ScrollToTop();
+ }
+
+ protected override void PopIn()
+ {
+ base.PopIn();
+ FadeEdgeEffectTo(0.5f, APPEAR_DURATION, Easing.In);
+ }
+
+ protected override void PopOut()
+ {
+ base.PopOut();
+ FadeEdgeEffectTo(0, DISAPPEAR_DURATION, Easing.Out);
+ }
+
+ private class WikiTabControl : PageTabControl
+ {
+ public WikiTabControl()
+ {
+ TabContainer.RelativeSizeAxes &= ~Axes.X;
+ TabContainer.AutoSizeAxes |= Axes.X;
+ TabContainer.Anchor |= Anchor.x1;
+ TabContainer.Origin |= Anchor.x1;
+ }
+
+ protected override TabItem CreateTabItem(WikiSection value) => new WikiTabItem(value);
+
+ protected override Dropdown CreateDropdown() => null;
+
+ private class WikiTabItem : PageTabItem
+ {
+ public WikiTabItem(WikiSection value) : base(value)
+ {
+ Text.Text = value.Title;
+ }
+ }
+ }
+
+ }
+}
diff --git a/Symcol.Rulesets.Core/Wiki/WikiParagraph.cs b/Symcol.Rulesets.Core/Wiki/WikiParagraph.cs
new file mode 100644
index 0000000000..0914f99c9b
--- /dev/null
+++ b/Symcol.Rulesets.Core/Wiki/WikiParagraph.cs
@@ -0,0 +1,63 @@
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Containers;
+
+namespace Symcol.Rulesets.Core.Wiki
+{
+ public class WikiParagraph : Container
+ {
+ public WikiParagraph(string text, float textsize = 20)
+ {
+ paragraphNoMarkdown(text, textsize);
+ }
+
+ public WikiParagraph(string text, bool markdown)
+ {
+ if (!markdown)
+ paragraphNoMarkdown(text, 20);
+ else
+ paragraphMarkdown(text, 20);
+ }
+ public WikiParagraph(string text, float textsize, bool markdown)
+ {
+ if (!markdown)
+ paragraphNoMarkdown(text, textsize);
+ else
+ paragraphMarkdown(text, textsize);
+ }
+
+ private void paragraphNoMarkdown(string text, float textsize)
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+ AutoSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
+
+ Child = new OsuTextFlowContainer(t => { t.TextSize = textsize; })
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Text = text
+ };
+ }
+
+ private void paragraphMarkdown(string text, float textsize)
+ {
+ Anchor = Anchor.TopCentre;
+ Origin = Anchor.TopCentre;
+ AutoSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
+
+ Child = new OsuTextFlowContainer(t => { t.TextSize = textsize; })
+ {
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Text = text
+ };
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Wiki/WikiSection.cs b/Symcol.Rulesets.Core/Wiki/WikiSection.cs
new file mode 100644
index 0000000000..7a298f2f0d
--- /dev/null
+++ b/Symcol.Rulesets.Core/Wiki/WikiSection.cs
@@ -0,0 +1,59 @@
+using OpenTK;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace Symcol.Rulesets.Core.Wiki
+{
+ public abstract class WikiSection : FillFlowContainer
+ {
+ public abstract string Title { get; }
+
+ private readonly FillFlowContainer content;
+
+ protected override Container Content => content;
+
+ protected WikiSection()
+ {
+ OsuColour osu = new OsuColour();
+ Direction = FillDirection.Vertical;
+ AutoSizeAxes = Axes.Y;
+ RelativeSizeAxes = Axes.X;
+ InternalChildren = new Drawable[]
+ {
+ new OsuSpriteText
+ {
+ Colour = osu.Yellow,
+ Text = Title,
+ TextSize = 32,
+ Font = @"Exo2.0-Bold",
+ Margin = new MarginPadding
+ {
+ Horizontal = WikiOverlay.CONTENT_X_MARGIN,
+ Vertical = 12
+ }
+ },
+ content = new FillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Padding = new MarginPadding
+ {
+ Horizontal = WikiOverlay.CONTENT_X_MARGIN,
+ Bottom = 20
+ }
+ },
+ new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 1,
+ Colour = OsuColour.Gray(34),
+ EdgeSmoothness = new Vector2(1)
+ }
+ };
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Wiki/WikiSubSectionHeader.cs b/Symcol.Rulesets.Core/Wiki/WikiSubSectionHeader.cs
new file mode 100644
index 0000000000..16245ba4e8
--- /dev/null
+++ b/Symcol.Rulesets.Core/Wiki/WikiSubSectionHeader.cs
@@ -0,0 +1,22 @@
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+
+namespace Symcol.Rulesets.Core.Wiki
+{
+ public class WikiSubSectionHeader : OsuSpriteText
+ {
+ public WikiSubSectionHeader(string text)
+ {
+ OsuColour osu = new OsuColour();
+ Colour = osu.Pink;
+ Text = text;
+ TextSize = 24;
+ Font = @"Exo2.0-BoldItalic";
+ Margin = new MarginPadding
+ {
+ Vertical = 10
+ };
+ }
+ }
+}
diff --git a/Symcol.Rulesets.Core/Wiki/WikiSubSectionLinkHeader.cs b/Symcol.Rulesets.Core/Wiki/WikiSubSectionLinkHeader.cs
new file mode 100644
index 0000000000..58e8e004f2
--- /dev/null
+++ b/Symcol.Rulesets.Core/Wiki/WikiSubSectionLinkHeader.cs
@@ -0,0 +1,28 @@
+using osu.Framework.Graphics;
+using osu.Game.Graphics;
+using Symcol.Rulesets.Core.Containers;
+
+namespace Symcol.Rulesets.Core.Wiki
+{
+ public class WikiSubSectionLinkHeader : LinkText
+ {
+ public override string Tooltip => tooltip;
+
+ private string tooltip = "";
+
+ public WikiSubSectionLinkHeader(string text, string url, string tooltip = "")
+ {
+ this.tooltip = tooltip;
+ Url = url;
+ OsuColour osu = new OsuColour();
+ Colour = osu.Pink;
+ Text = text;
+ TextSize = 24;
+ Font = @"Exo2.0-BoldItalic";
+ Margin = new MarginPadding
+ {
+ Vertical = 10
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/deathSound.mp3 b/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/deathSound.mp3
new file mode 100644
index 0000000000..88fd4bb713
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/deathSound.mp3 differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/gasterFire.mp3 b/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/gasterFire.mp3
new file mode 100644
index 0000000000..fee9d63be9
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/gasterFire.mp3 differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/gasterReady.mp3 b/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/gasterReady.mp3
new file mode 100644
index 0000000000..198c443c21
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/gasterReady.mp3 differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/shootSound.mp3 b/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/shootSound.mp3
new file mode 100644
index 0000000000..65ad88941e
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Audio/Samples/shootSound.mp3 differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Font/vitaruFont.fnt b/osu.Game.Rulesets.Vitaru/Assets/Font/vitaruFont.fnt
new file mode 100644
index 0000000000..cb9eed3f34
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Assets/Font/vitaruFont.fnt
@@ -0,0 +1,5 @@
+info face="vitaru!font" size=100 bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=4 padding=0,0,0,0 spacing=4,4 outline=0
+common lineHeight=100 base=83 scaleW=1024 scaleH=1024 pages=1 packed=0 alphaChnl=0 redChnl=4 greenChnl=4 blueChnl=4
+page id=0 file="vitaruFont_0.png"
+chars count=1
+char id=57419 x=495 y=537 width=88 height=89 xoffset=0 yoffset=8 xadvance=89 page=0 chnl=15
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Font/vitaruFont_0.png b/osu.Game.Rulesets.Vitaru/Assets/Font/vitaruFont_0.png
new file mode 100644
index 0000000000..d5d80c2ad0
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Font/vitaruFont_0.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/Vitaru@2x.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/Vitaru@2x.png
new file mode 100644
index 0000000000..50b3e38755
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/Vitaru@2x.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/VitaruTouhosuModeTrue2560x1440.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/VitaruTouhosuModeTrue2560x1440.png
new file mode 100644
index 0000000000..b4d07f4b4a
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/VitaruTouhosuModeTrue2560x1440.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/bulletKiai.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/bulletKiai.png
new file mode 100644
index 0000000000..26969224e1
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/bulletKiai.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/chen.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/chen.png
new file mode 100644
index 0000000000..c9feb548e8
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/chen.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/chenKiai.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/chenKiai.png
new file mode 100644
index 0000000000..1bcc766f20
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/chenKiai.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/chenKiaiRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/chenKiaiRight.png
new file mode 100644
index 0000000000..9d8deda6aa
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/chenKiaiRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/chenRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/chenRight.png
new file mode 100644
index 0000000000..46640cc908
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/chenRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/crystal.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/crystal.png
new file mode 100644
index 0000000000..902e079008
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/crystal.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/enemy.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/enemy.png
new file mode 100644
index 0000000000..019131bc94
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/enemy.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/enemyKiai.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/enemyKiai.png
new file mode 100644
index 0000000000..0c2ca58e17
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/enemyKiai.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/icon.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/icon.png
new file mode 100644
index 0000000000..1ddedfbefc
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/icon.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguya.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguya.png
new file mode 100644
index 0000000000..f405cf7ce2
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguya.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguyaKiai.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguyaKiai.png
new file mode 100644
index 0000000000..25e341ffc8
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguyaKiai.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguyaKiaiRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguyaKiaiRight.png
new file mode 100644
index 0000000000..3f92d6051f
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguyaKiaiRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguyaRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguyaRight.png
new file mode 100644
index 0000000000..6595c3e07f
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/kaguyaRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/marisa.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/marisa.png
new file mode 100644
index 0000000000..ccffd932cd
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/marisa.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/marisaKiai.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/marisaKiai.png
new file mode 100644
index 0000000000..3074869540
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/marisaKiai.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/marisaKiaiRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/marisaKiaiRight.png
new file mode 100644
index 0000000000..c2e5f77f55
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/marisaKiaiRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/marisaRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/marisaRight.png
new file mode 100644
index 0000000000..f2ac6b515e
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/marisaRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/nue.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/nue.png
new file mode 100644
index 0000000000..849fd35780
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/nue.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/nueKiai.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/nueKiai.png
new file mode 100644
index 0000000000..ae50d8c467
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/nueKiai.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/nueKiaiRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/nueKiaiRight.png
new file mode 100644
index 0000000000..452d275ca4
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/nueKiaiRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/nueRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/nueRight.png
new file mode 100644
index 0000000000..f53c922a16
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/nueRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/player.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/player.png
new file mode 100644
index 0000000000..696f68f075
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/player.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/playerKiai.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/playerKiai.png
new file mode 100644
index 0000000000..21bc20677a
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/playerKiai.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/playerKiaiRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/playerKiaiRight.png
new file mode 100644
index 0000000000..a9dcd12577
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/playerKiaiRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/playerRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/playerRight.png
new file mode 100644
index 0000000000..4ab761f611
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/playerRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/reimu.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/reimu.png
new file mode 100644
index 0000000000..696f68f075
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/reimu.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/reimuKiai.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/reimuKiai.png
new file mode 100644
index 0000000000..21bc20677a
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/reimuKiai.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/reimuKiaiRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/reimuKiaiRight.png
new file mode 100644
index 0000000000..a9dcd12577
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/reimuKiaiRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/reimuRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/reimuRight.png
new file mode 100644
index 0000000000..4ab761f611
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/reimuRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/ring.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/ring.png
new file mode 100644
index 0000000000..8b2c1187d3
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/ring.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuya.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuya.png
new file mode 100644
index 0000000000..0d20edb20d
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuya.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuyaKiai.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuyaKiai.png
new file mode 100644
index 0000000000..0bce3affa6
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuyaKiai.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuyaKiaiRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuyaKiaiRight.png
new file mode 100644
index 0000000000..928ddb57bf
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuyaKiaiRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuyaRight.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuyaRight.png
new file mode 100644
index 0000000000..a906323b0f
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/sakuyaRight.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/sign.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/sign.png
new file mode 100644
index 0000000000..6be20f4e50
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/sign.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Assets/Textures/vortex.png b/osu.Game.Rulesets.Vitaru/Assets/Textures/vortex.png
new file mode 100644
index 0000000000..12e145101e
Binary files /dev/null and b/osu.Game.Rulesets.Vitaru/Assets/Textures/vortex.png differ
diff --git a/osu.Game.Rulesets.Vitaru/Beatmaps/VitaruBeatmapConverter.cs b/osu.Game.Rulesets.Vitaru/Beatmaps/VitaruBeatmapConverter.cs
new file mode 100644
index 0000000000..7065909a4f
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Beatmaps/VitaruBeatmapConverter.cs
@@ -0,0 +1,273 @@
+using OpenTK;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Vitaru.Objects;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Objects.Types;
+using System;
+using osu.Game.Audio;
+using System.Linq;
+using osu.Game.Rulesets.Vitaru.Settings;
+
+namespace osu.Game.Rulesets.Vitaru.Beatmaps
+{
+ internal class VitaruBeatmapConverter : BeatmapConverter
+ {
+ private VitaruGamemode currentGameMode = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GameMode);
+
+ public static List HitObjectList = new List();
+
+ protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasPosition) };
+
+ private float ar;
+ private float cs;
+
+ protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap)
+ {
+ var curveData = original as IHasCurve;
+ var endTimeData = original as IHasEndTime;
+ var positionData = original as IHasPosition;
+ var comboData = original as IHasCombo;
+
+ List samples = original.Samples;
+
+ int difficulty = 2;
+ if (currentGameMode == VitaruGamemode.Dodge)
+ difficulty = 1;
+
+ ar = calculateAr(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate);
+ cs = calculateCs(beatmap.BeatmapInfo.BaseDifficulty.CircleSize);
+
+ bool isLine = samples.Any(s => s.Name == SampleInfo.HIT_WHISTLE);
+ bool isTriangleWave = samples.Any(s => s.Name == SampleInfo.HIT_FINISH);
+ bool isCoolWave = samples.Any(s => s.Name == SampleInfo.HIT_CLAP);
+
+ Pattern p;
+ Pattern a;
+
+ if (curveData != null)
+ {
+ if (isLine)
+ {
+ p = new Pattern
+ {
+ Ar = ar,
+ Cs = cs,
+ StartTime = original.StartTime,
+ Position = positionData?.Position ?? Vector2.Zero,
+ Samples = original.Samples,
+ PatternID = 2,
+ PatternAngleDegree = 180,
+ PatternSpeed = 0.25f,
+ PatternDifficulty = difficulty,
+ PatternBulletDiameter = 8f * difficulty,
+ PatternTeam = 1,
+ EnemyHealth = 60,
+ ControlPoints = curveData.ControlPoints,
+ CurveType = curveData.CurveType,
+ Distance = curveData.Distance,
+ RepeatSamples = curveData.RepeatSamples,
+ RepeatCount = curveData.RepeatCount,
+ NewCombo = comboData?.NewCombo ?? false,
+ IsSlider = true,
+ };
+ }
+ else if (isTriangleWave)
+ {
+ p = new Pattern
+ {
+ Ar = ar,
+ Cs = cs,
+ StartTime = original.StartTime,
+ Position = positionData?.Position ?? Vector2.Zero,
+ Samples = original.Samples,
+ PatternID = 3,
+ PatternAngleDegree = 180,
+ PatternSpeed = 0.25f,
+ PatternDifficulty = difficulty,
+ PatternBulletDiameter = 8f * difficulty,
+ PatternTeam = 1,
+ EnemyHealth = 60,
+ ControlPoints = curveData.ControlPoints,
+ CurveType = curveData.CurveType,
+ Distance = curveData.Distance,
+ RepeatSamples = curveData.RepeatSamples,
+ RepeatCount = curveData.RepeatCount,
+ NewCombo = comboData?.NewCombo ?? false,
+ IsSlider = true,
+ };
+ }
+ else if (isCoolWave)
+ {
+ p = new Pattern
+ {
+ Ar = ar,
+ Cs = cs,
+ StartTime = original.StartTime,
+ Position = positionData?.Position ?? Vector2.Zero,
+ Samples = original.Samples,
+ PatternID = 4,
+ PatternAngleDegree = 180,
+ PatternSpeed = 0.25f,
+ PatternDifficulty = difficulty,
+ PatternBulletDiameter = 6f * difficulty,
+ PatternTeam = 1,
+ EnemyHealth = 60,
+ ControlPoints = curveData.ControlPoints,
+ CurveType = curveData.CurveType,
+ Distance = curveData.Distance,
+ RepeatSamples = curveData.RepeatSamples,
+ RepeatCount = curveData.RepeatCount,
+ NewCombo = comboData?.NewCombo ?? false,
+ IsSlider = true,
+ };
+ }
+ else
+ {
+ p = new Pattern
+ {
+ Ar = ar,
+ Cs = cs,
+ StartTime = original.StartTime,
+ Position = positionData?.Position ?? Vector2.Zero,
+ Samples = original.Samples,
+ PatternID = 1,
+ PatternAngleDegree = 180,
+ PatternSpeed = 0.20f,
+ PatternDifficulty = difficulty,
+ PatternBulletDiameter = 8f * difficulty,
+ PatternTeam = 1,
+ EnemyHealth = 60,
+ ControlPoints = curveData.ControlPoints,
+ CurveType = curveData.CurveType,
+ Distance = curveData.Distance,
+ RepeatSamples = curveData.RepeatSamples,
+ RepeatCount = curveData.RepeatCount,
+ NewCombo = comboData?.NewCombo ?? false,
+ IsSlider = true,
+ };
+ }
+ }
+ else if (endTimeData != null)
+ {
+ p = new Pattern
+ {
+ Ar = ar,
+ Cs = cs,
+ StartTime = original.StartTime,
+ Position = positionData?.Position ?? Vector2.Zero,
+ Samples = original.Samples,
+ IsSpinner = true,
+ PatternSpeed = 0.25f,
+ PatternBulletDiameter = 8f * difficulty,
+ PatternTeam = 1,
+ EnemyHealth = 120,
+ PatternDamage = 5,
+ PatternID = 5,
+ EndTime = endTimeData.EndTime,
+ PatternDifficulty = 2 * difficulty,
+ };
+ }
+ else
+ {
+ if (isLine)
+ {
+ p = new Pattern
+ {
+ Ar = ar,
+ Cs = cs,
+ StartTime = original.StartTime,
+ Position = positionData?.Position ?? Vector2.Zero,
+ Samples = original.Samples,
+ PatternID = 2,
+ PatternAngleDegree = 180,
+ PatternSpeed = 0.2f,
+ PatternDifficulty = difficulty * 2,
+ PatternDamage = 8,
+ PatternBulletDiameter = 10f * difficulty,
+ PatternTeam = 1,
+ NewCombo = comboData?.NewCombo ?? false
+ };
+ }
+ else if (isTriangleWave)
+ {
+ p = new Pattern
+ {
+ Ar = ar,
+ Cs = cs,
+ StartTime = original.StartTime,
+ Position = positionData?.Position ?? Vector2.Zero,
+ Samples = original.Samples,
+ PatternID = 3,
+ PatternAngleDegree = 180,
+ PatternSpeed = 0.3f,
+ PatternDifficulty = difficulty,
+ PatternBulletDiameter = 10f * difficulty,
+ PatternTeam = 1,
+ NewCombo = comboData?.NewCombo ?? false
+ };
+ }
+ else if (isCoolWave)
+ {
+ p = new Pattern
+ {
+ Ar = ar,
+ Cs = cs,
+ StartTime = original.StartTime,
+ Position = positionData?.Position ?? Vector2.Zero,
+ Samples = original.Samples,
+ PatternID = 4,
+ PatternAngleDegree = 180,
+ PatternSpeed = 0.18f,
+ PatternDifficulty = difficulty,
+ PatternBulletDiameter = 10f * difficulty,
+ PatternTeam = 1,
+ NewCombo = comboData?.NewCombo ?? false
+ };
+ }
+ else
+ {
+ p = new Pattern
+ {
+ Ar = ar,
+ Cs = cs,
+ StartTime = original.StartTime,
+ Position = positionData?.Position ?? Vector2.Zero,
+ Samples = original.Samples,
+ PatternID = 1,
+ PatternAngleDegree = 180,
+ PatternSpeed = 0.28f,
+ PatternDifficulty = difficulty,
+ PatternBulletDiameter = 12f * difficulty,
+ PatternTeam = 1,
+ NewCombo = comboData?.NewCombo ?? false
+ };
+ }
+ }
+
+ a = p;
+ HitObjectList.Add(a);
+ yield return p;
+ }
+
+ private float calculateAr(float ar)
+ {
+ if (ar >= 5)
+ {
+ this.ar = 1200 - ((ar - 5) * 150);
+ return this.ar;
+ }
+ else
+ {
+ this.ar = 1800 - (ar * 120);
+ return this.ar;
+ }
+ }
+
+ private float calculateCs(float cs)
+ {
+ this.cs = cs / 4;
+ return this.cs;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Beatmaps/VitaruBeatmapProcessor.cs b/osu.Game.Rulesets.Vitaru/Beatmaps/VitaruBeatmapProcessor.cs
new file mode 100644
index 0000000000..1f3f197170
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Beatmaps/VitaruBeatmapProcessor.cs
@@ -0,0 +1,29 @@
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Vitaru.Objects;
+
+namespace osu.Game.Rulesets.Vitaru.Beatmaps
+{
+ internal class VitaruBeatmapProcessor : BeatmapProcessor
+ {
+ public override void PostProcess(Beatmap beatmap)
+ {
+ if (beatmap.ComboColors.Count == 0)
+ return;
+
+ int comboIndex = 0;
+ int colourIndex = 0;
+
+ foreach (var obj in beatmap.HitObjects)
+ {
+ if (obj.NewCombo)
+ {
+ comboIndex = 0;
+ colourIndex = (colourIndex + 1) % beatmap.ComboColors.Count;
+ }
+
+ obj.ComboIndex = comboIndex++;
+ obj.ComboColour = beatmap.ComboColors[colourIndex];
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Beatmaps/VitaruDifficultyCalculator.cs b/osu.Game.Rulesets.Vitaru/Beatmaps/VitaruDifficultyCalculator.cs
new file mode 100644
index 0000000000..40a82e97bb
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Beatmaps/VitaruDifficultyCalculator.cs
@@ -0,0 +1,140 @@
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Vitaru.Objects;
+using System;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Mods;
+
+namespace osu.Game.Rulesets.Vitaru.Beatmaps
+{
+ ///
+ /// Most of this is copied from OsuDifficultyCalculator ATM
+ ///
+ public class VitaruDifficultyCalculator : DifficultyCalculator
+ {
+ private const double star_scaling_factor = 0.0675;
+
+ internal List DifficultyHitObjects = new List();
+
+ public VitaruDifficultyCalculator(Beatmap beatmap, Mod[] mods) : base(beatmap, mods) { }
+
+ protected override void PreprocessHitObjects()
+ {
+ //foreach (Pattern h in Beatmap.HitObjects)
+ //h.Curve?.Calculate();
+ }
+
+ protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new VitaruBeatmapConverter();
+
+ public override double Calculate(Dictionary categoryDifficulty = null)
+ {
+ // Fill our custom DifficultyHitObject class, that carries additional information
+ DifficultyHitObjects.Clear();
+
+ foreach (VitaruHitObject hitObject in Beatmap.HitObjects)
+ DifficultyHitObjects.Add(new VitaruHitObjectDifficulty(hitObject));
+
+ // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
+ DifficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
+
+ if (!CalculateStrainValues()) return 0;
+
+ double speedDifficulty = CalculateDifficulty(DifficultyType.Speed) * 0.75f;
+ double aimDifficulty = CalculateDifficulty(DifficultyType.Aim) * 1.5f;
+
+ double speedStars = Math.Sqrt(speedDifficulty) * star_scaling_factor;
+ double aimStars = Math.Sqrt(aimDifficulty) * star_scaling_factor;
+
+ double starRating = aimStars + speedStars + Math.Abs(aimStars - speedStars) / 2;
+
+ if (categoryDifficulty != null)
+ {
+ categoryDifficulty.Add("Aim", aimStars);
+ categoryDifficulty.Add("Speed", speedStars);
+ }
+
+ return starRating;
+ }
+
+ protected bool CalculateStrainValues()
+ {
+ // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
+ using (List.Enumerator hitObjectsEnumerator = DifficultyHitObjects.GetEnumerator())
+ {
+
+ if (!hitObjectsEnumerator.MoveNext()) return false;
+
+ VitaruHitObjectDifficulty current = hitObjectsEnumerator.Current;
+
+ // First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject.
+ while (hitObjectsEnumerator.MoveNext())
+ {
+ var next = hitObjectsEnumerator.Current;
+ next?.CalculateStrains(current, TimeRate);
+ current = next;
+ }
+
+ return true;
+ }
+ }
+
+ protected const double STRAIN_STEP = 200;
+ protected const double DECAY_WEIGHT = 0.75;
+
+ protected double CalculateDifficulty(DifficultyType type)
+ {
+ double actualStrainStep = STRAIN_STEP * TimeRate;
+
+ List highestStrains = new List();
+ double intervalEndTime = actualStrainStep;
+ double maximumStrain = 0;
+
+ VitaruHitObjectDifficulty previousHitObject = null;
+ foreach (VitaruHitObjectDifficulty hitObject in DifficultyHitObjects)
+ {
+ while (hitObject.BaseHitObject.StartTime > intervalEndTime)
+ {
+ highestStrains.Add(maximumStrain);
+
+ // The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay
+ // until the beginning of the next interval.
+ if (previousHitObject == null)
+ {
+ maximumStrain = 0;
+ }
+ else
+ {
+ double decay = Math.Pow(VitaruHitObjectDifficulty.DECAY_BASE[(int)type], (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000);
+ maximumStrain = previousHitObject.Strains[(int)type] * decay;
+ }
+
+ // Go to the next time interval
+ intervalEndTime += actualStrainStep;
+ }
+
+ // Obtain maximum strain
+ maximumStrain = Math.Max(hitObject.Strains[(int)type], maximumStrain);
+
+ previousHitObject = hitObject;
+ }
+
+ // Build the weighted sum over the highest strains for each interval
+ double difficulty = 0;
+ double weight = 1;
+ highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
+
+ foreach (double strain in highestStrains)
+ {
+ difficulty += weight * strain;
+ weight *= DECAY_WEIGHT;
+ }
+
+ return difficulty;
+ }
+
+ public enum DifficultyType
+ {
+ Speed = 0,
+ Aim,
+ };
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Edit/Pieces/PatternEditor.cs b/osu.Game.Rulesets.Vitaru/Edit/Pieces/PatternEditor.cs
new file mode 100644
index 0000000000..4e1015b328
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Edit/Pieces/PatternEditor.cs
@@ -0,0 +1,23 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using Symcol.Core.Graphics.UserInterface;
+
+namespace osu.Game.Rulesets.Vitaru.Edit.Pieces
+{
+ public class PatternEditor : SymcolWindow
+ {
+ public PatternEditor() : base(new Vector2(300, 400))
+ {
+ Scale = new Vector2(2);
+ WindowTitle.Text = "Pattern Editor";
+ WindowContent.Child = new Box
+ {
+ Colour = Color4.Black,
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.25f
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Edit/VitaruEditPlayfield.cs b/osu.Game.Rulesets.Vitaru/Edit/VitaruEditPlayfield.cs
new file mode 100644
index 0000000000..cd9a9e9211
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Edit/VitaruEditPlayfield.cs
@@ -0,0 +1,24 @@
+using osu.Framework.Graphics.Cursor;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Vitaru.Edit.Pieces;
+using osu.Game.Rulesets.Vitaru.UI;
+
+namespace osu.Game.Rulesets.Vitaru.Edit
+{
+ public class VitaruEditPlayfield : VitaruPlayfield
+ {
+ public override bool LoadPlayer => false;
+
+ //public override bool ProvidingUserCursor => false;
+
+ protected override CursorContainer CreateCursor() => null;
+
+ private readonly PatternEditor patternEditor;
+
+ public VitaruEditPlayfield()
+ {
+ Add(patternEditor = new PatternEditor());
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Edit/VitaruEditRulesetContainer.cs b/osu.Game.Rulesets.Vitaru/Edit/VitaruEditRulesetContainer.cs
new file mode 100644
index 0000000000..b0055cf9ce
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Edit/VitaruEditRulesetContainer.cs
@@ -0,0 +1,16 @@
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.Vitaru.UI;
+
+namespace osu.Game.Rulesets.Vitaru.Edit
+{
+ public class VitaruEditRulesetContainer : VitaruRulesetContainer
+ {
+ public VitaruEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset)
+ : base(ruleset, beatmap, isForCurrentRuleset)
+ {
+ }
+
+ protected override Playfield CreatePlayfield() => new VitaruEditPlayfield();
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Edit/VitaruHitObjectComposer.cs b/osu.Game.Rulesets.Vitaru/Edit/VitaruHitObjectComposer.cs
new file mode 100644
index 0000000000..899b19f326
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Edit/VitaruHitObjectComposer.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.Vitaru.Objects;
+
+namespace osu.Game.Rulesets.Vitaru.Edit
+{
+ public class VitaruHitObjectComposer : HitObjectComposer
+ {
+ public VitaruHitObjectComposer(Ruleset ruleset) : base(ruleset) { }
+
+ protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new VitaruEditRulesetContainer(ruleset, beatmap, true);
+
+ protected override IReadOnlyList CompositionTools => new ICompositionTool[]
+ {
+ new HitObjectCompositionTool(),
+ new HitObjectCompositionTool(),
+ new HitObjectCompositionTool()
+ };
+ }
+
+ public enum EditorConfiguration
+ {
+ Simple,
+ Complex
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Judgements/VitaruJudgement.cs b/osu.Game.Rulesets.Vitaru/Judgements/VitaruJudgement.cs
new file mode 100644
index 0000000000..ef506eb989
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Judgements/VitaruJudgement.cs
@@ -0,0 +1,16 @@
+using OpenTK;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Vitaru.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Vitaru.Judgements
+{
+ public class VitaruJudgement : Judgement
+ {
+ ///
+ /// The positional hit offset.
+ ///
+ public Vector2 PositionOffset;
+
+ public ComboResult Combo;
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Mods/VitaruMod.cs b/osu.Game.Rulesets.Vitaru/Mods/VitaruMod.cs
new file mode 100644
index 0000000000..536f69221a
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Mods/VitaruMod.cs
@@ -0,0 +1,81 @@
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Vitaru.Objects;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Users;
+using osu.Game.Rulesets.Vitaru.Replays;
+
+namespace osu.Game.Rulesets.Vitaru.Mods
+{
+ public class VitaruModNoFail : ModNoFail
+ {
+
+ }
+
+ public class VitaruModEasy : ModEasy
+ {
+
+ }
+
+ public class VitaruModHidden : ModHidden
+ {
+ public override string Description => @"Play with bullets dissapearing once they leave enemies immediate area";
+ public override double ScoreMultiplier => 1.32;
+ }
+
+ public class VitaruModHardRock : ModHardRock
+ {
+ public override double ScoreMultiplier => 1.12;
+ }
+
+ public class VitaruModSuddenDeath : ModSuddenDeath
+ {
+ public override string Description => "Don't get hit";
+ }
+
+ public class VitaruModPerfect : ModPerfect
+ {
+ public override string Description => "Leave no survivors";
+ }
+
+ public class VitaruModDaycore : ModDaycore
+ {
+ public override double ScoreMultiplier => 0.4;
+ }
+
+ public class VitaruModDoubleTime : ModDoubleTime
+ {
+ public override double ScoreMultiplier => 1.08;
+ }
+
+ public class VitaruModHalfTime : ModHalfTime
+ {
+ public override double ScoreMultiplier => 0.4;
+ }
+
+ public class VitaruModNightcore : ModNightcore
+ {
+ public override double ScoreMultiplier => 1.08;
+ }
+
+ public class VitaruModFlashlight : ModFlashlight
+ {
+ public override string Description => @"Play with bullets only appearing when they are close";
+ public override double ScoreMultiplier => 1.18;
+ }
+
+ public class VitaruRelax : ModRelax
+ {
+ public override string Description => @"Player moves to the cursor instantly";
+ public override bool Ranked => false;
+ }
+
+ public class VitaruModAutoplay : ModAutoplay
+ {/*
+ protected override Score CreateReplayScore(Beatmap beatmap) => new Score
+ {
+ User = new User { Username = "reimosu!" },
+ Replay = new VitaruAutoGenerator(beatmap).Generate(),
+ };
+ */}
+}
diff --git a/osu.Game.Rulesets.Vitaru/Multi/VitaruClientInfo.cs b/osu.Game.Rulesets.Vitaru/Multi/VitaruClientInfo.cs
new file mode 100644
index 0000000000..96bbe1cc9d
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Multi/VitaruClientInfo.cs
@@ -0,0 +1,11 @@
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+using System;
+
+namespace osu.Game.Rulesets.Vitaru.Multi
+{
+ [Serializable]
+ public class VitaruClientInfo : RulesetClientInfo
+ {
+ public VitaruPlayerInformation PlayerInformation;
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Multi/VitaruInMatchPacket.cs b/osu.Game.Rulesets.Vitaru/Multi/VitaruInMatchPacket.cs
new file mode 100644
index 0000000000..4bfc8e4486
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Multi/VitaruInMatchPacket.cs
@@ -0,0 +1,21 @@
+using Symcol.Core.Networking;
+using System;
+
+namespace osu.Game.Rulesets.Vitaru.Multi
+{
+ [Serializable]
+ public class VitaruInMatchPacket : Packet
+ {
+ ///
+ /// This player's information
+ ///
+ public VitaruPlayerInformation PlayerInformation;
+
+ public override int PacketSize => 2048;
+
+ public VitaruInMatchPacket(ClientInfo clientInfo) : base(clientInfo)
+ {
+
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Multi/VitaruLobbyItem.cs b/osu.Game.Rulesets.Vitaru/Multi/VitaruLobbyItem.cs
new file mode 100644
index 0000000000..37ec055f25
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Multi/VitaruLobbyItem.cs
@@ -0,0 +1,16 @@
+using osu.Framework.Graphics.Textures;
+using Symcol.Rulesets.Core.Multiplayer.Screens;
+
+namespace osu.Game.Rulesets.Vitaru.Multi
+{
+ public class VitaruLobbyItem : RulesetLobbyItem
+ {
+ public override Texture Icon => VitaruRuleset.VitaruTextures.Get("Vitaru@2x");
+
+ public override string RulesetName => "Vitaru!";
+
+ public override Texture Background => VitaruRuleset.VitaruTextures.Get("VitaruTouhosuModeTrue2560x1440");
+
+ public override RulesetLobbyScreen RulesetLobbyScreen => new VitaruLobbyScreen();
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Multi/VitaruLobbyScreen.cs b/osu.Game.Rulesets.Vitaru/Multi/VitaruLobbyScreen.cs
new file mode 100644
index 0000000000..3803924138
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Multi/VitaruLobbyScreen.cs
@@ -0,0 +1,67 @@
+using osu.Framework.Configuration;
+using osu.Game.Rulesets.Vitaru.Settings;
+using Symcol.Core.Networking;
+using Symcol.Rulesets.Core.Multiplayer.Screens;
+using System;
+using System.Collections.Generic;
+
+namespace osu.Game.Rulesets.Vitaru.Multi
+{
+ public class VitaruLobbyScreen : RulesetLobbyScreen
+ {
+ public override string RulesetName => "vitaru";
+
+ public VitaruNetworkingClientHandler VitaruNetworkingClientHandler;
+
+ public override RulesetMatchScreen MatchScreen => new VitaruMatchScreen(VitaruNetworkingClientHandler);
+
+ private readonly Bindable hostIP = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.HostIP);
+ private readonly Bindable localIP = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.LocalIP);
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ HostIP.Text = hostIP;
+ LocalIp.Text = localIP;
+ }
+
+ protected override void HostGame()
+ {
+ if (RulesetNetworkingClientHandler != null)
+ {
+ Remove(RulesetNetworkingClientHandler);
+ VitaruNetworkingClientHandler.Dispose();
+ }
+ VitaruNetworkingClientHandler = new VitaruNetworkingClientHandler(ClientType.Host, LocalIp.Text, Int32.Parse(HostPort.Text));
+ RulesetNetworkingClientHandler = VitaruNetworkingClientHandler;
+ Add(RulesetNetworkingClientHandler);
+
+ List list = new List();
+ list.Add(RulesetNetworkingClientHandler.RulesetClientInfo);
+
+ JoinMatch(list);
+ }
+
+ protected override void DirectConnect()
+ {
+ if (RulesetNetworkingClientHandler != null)
+ {
+ Remove(RulesetNetworkingClientHandler);
+ VitaruNetworkingClientHandler.Dispose();
+ }
+ VitaruNetworkingClientHandler = new VitaruNetworkingClientHandler(ClientType.Peer, HostIP.Text, Int32.Parse(HostPort.Text), LocalIp.Text);
+ VitaruNetworkingClientHandler.OnConnectedToHost += (p) => JoinMatch(p);
+ RulesetNetworkingClientHandler = VitaruNetworkingClientHandler;
+ Add(RulesetNetworkingClientHandler);
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ hostIP.Value = HostIP.Text;
+ localIP.Value = LocalIp.Text;
+
+ base.Dispose(isDisposing);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Multi/VitaruMatchScreen.cs b/osu.Game.Rulesets.Vitaru/Multi/VitaruMatchScreen.cs
new file mode 100644
index 0000000000..c0b06cf143
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Multi/VitaruMatchScreen.cs
@@ -0,0 +1,82 @@
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Screens;
+using osu.Game.Rulesets.Vitaru.Objects.Characters;
+using osu.Game.Rulesets.Vitaru.Scoring;
+using osu.Game.Rulesets.Vitaru.Settings;
+using osu.Game.Rulesets.Vitaru.UI;
+using Symcol.Core.Networking;
+using Symcol.Rulesets.Core;
+using Symcol.Rulesets.Core.Multiplayer.Options;
+using Symcol.Rulesets.Core.Multiplayer.Pieces;
+using Symcol.Rulesets.Core.Multiplayer.Screens;
+using System.Collections.Generic;
+
+namespace osu.Game.Rulesets.Vitaru.Multi
+{
+ public class VitaruMatchScreen : RulesetMatchScreen
+ {
+ private readonly Bindable currentCharacter = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.Characters);
+ private readonly Bindable currentGraphics = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GraphicsPresets);
+ private readonly Bindable currentScoringMetric = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.ScoringMetric);
+ private readonly Bindable currentGameMode = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GameMode);
+ private readonly Bindable comboFire = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.ComboFire);
+
+ public readonly VitaruNetworkingClientHandler VitaruNetworkingClientHandler;
+
+ public VitaruMatchScreen(VitaruNetworkingClientHandler vitaruNetworkingClientHandler) : base(vitaruNetworkingClientHandler)
+ {
+ VitaruNetworkingClientHandler = vitaruNetworkingClientHandler;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ MatchTools.Mode.ValueChanged += (value) =>
+ {
+ if (value == MatchScreenMode.RulesetSettings)
+ MatchTools.SelectedContent.Child = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new MultiplayerToggleOption(comboFire, "Enable Combo Fire", 5, false),
+ new MultiplayerDropdownEnumOption(currentGraphics, "Graphics", 3, false),
+ new MultiplayerDropdownEnumOption(currentScoringMetric, "Scoring Metric", 4),
+ new MultiplayerDropdownEnumOption(currentGameMode, "Vitaru Gamemode", 1),
+ new MultiplayerDropdownEnumOption(currentCharacter, "Character", 2, false),
+ }
+ };
+ };
+ }
+
+ protected override void OnEntering(Screen last)
+ {
+ base.OnEntering(last);
+
+ VitaruPlayfield.LoadPlayerList = new List();
+ SymcolPlayfield.RulesetNetworkingClientHandler = VitaruNetworkingClientHandler;
+ MakeCurrent();
+ VitaruNetworkingClientHandler.OnLoadGame = (i) => Load(i);
+ }
+
+ protected override void OnResuming(Screen last)
+ {
+ base.OnResuming(last);
+ VitaruPlayfield.LoadPlayerList = new List();
+ }
+
+ protected override void Load(List playerList)
+ {
+ base.Load(playerList);
+
+ foreach (ClientInfo client in playerList)
+ if (client is VitaruClientInfo vitaruClientInfo)
+ VitaruPlayfield.LoadPlayerList.Add(vitaruClientInfo);
+
+ Push(new MultiPlayer(VitaruNetworkingClientHandler));
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Multi/VitaruNetworkingClientHandler.cs b/osu.Game.Rulesets.Vitaru/Multi/VitaruNetworkingClientHandler.cs
new file mode 100644
index 0000000000..7e924f74e2
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Multi/VitaruNetworkingClientHandler.cs
@@ -0,0 +1,77 @@
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Game.Online.API;
+using osu.Game.Rulesets.Vitaru.Objects.Characters;
+using osu.Game.Rulesets.Vitaru.Settings;
+using Symcol.Core.Networking;
+using Symcol.Rulesets.Core.Multiplayer.Networking;
+
+namespace osu.Game.Rulesets.Vitaru.Multi
+{
+ public class VitaruNetworkingClientHandler : RulesetNetworkingClientHandler, IOnlineComponent
+ {
+ private readonly Bindable currentCharacter = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.Characters);
+
+ public readonly VitaruClientInfo VitaruClientInfo;
+
+ public VitaruNetworkingClientHandler(ClientType type, string ip, int port = 25570, string thisLocalIp = "0.0.0.0") : base(type, ip, port, thisLocalIp)
+ {
+ VitaruClientInfo = new VitaruClientInfo()
+ {
+ PlayerInformation = new VitaruPlayerInformation(),
+ Port = port
+ };
+
+ RulesetClientInfo = VitaruClientInfo;
+ ClientInfo = RulesetClientInfo;
+
+ currentCharacter.ValueChanged += character =>
+ {
+ VitaruClientInfo.PlayerInformation.Character = character;
+ SendToHost(new VitaruPacket(VitaruClientInfo) { ChangeCharacter = true });
+ };
+ currentCharacter.TriggerChange();
+
+ OnPacketReceive += (Packet packet) =>
+ {
+ if (packet is VitaruPacket vitaruPacket)
+ if (vitaruPacket.ChangeCharacter)
+ foreach(ClientInfo clientInfo in ConncetedClients)
+ if (vitaruPacket.ClientInfo.IP == clientInfo.IP)
+ {
+ ConncetedClients.Remove(clientInfo);
+ InMatchClients.Remove(clientInfo);
+ ConncetedClients.Add(vitaruPacket.ClientInfo);
+ InMatchClients.Add(vitaruPacket.ClientInfo);
+ break;
+ }
+ };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(APIAccess api)
+ {
+ api.Register(this);
+ }
+
+ public new void APIStateChanged(APIAccess api, APIState state)
+ {
+ switch (state)
+ {
+ default:
+ VitaruClientInfo.Username = "";
+ VitaruClientInfo.UserID = -1;
+ break;
+ case APIState.Online:
+ VitaruClientInfo.Username = api.LocalUser.Value.Username;
+ VitaruClientInfo.UserID = (int)api.LocalUser.Value.Id;
+ VitaruClientInfo.UserCountry = api.LocalUser.Value.Country.FullName;
+ VitaruClientInfo.CountryFlagName = api.LocalUser.Value.Country.FlagName;
+ VitaruClientInfo.UserPic = api.LocalUser.Value.AvatarUrl;
+ VitaruClientInfo.UserBackground = api.LocalUser.Value.CoverUrl;
+ break;
+ }
+ VitaruClientInfo.PlayerInformation.PlayerID = VitaruClientInfo.IP + VitaruClientInfo.UserID;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Multi/VitaruPacket.cs b/osu.Game.Rulesets.Vitaru/Multi/VitaruPacket.cs
new file mode 100644
index 0000000000..da3faa7df2
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Multi/VitaruPacket.cs
@@ -0,0 +1,23 @@
+using Symcol.Core.Networking;
+using System;
+
+namespace osu.Game.Rulesets.Vitaru.Multi
+{
+ [Serializable]
+ public class VitaruPacket : Packet
+ {
+ public new readonly VitaruClientInfo ClientInfo;
+
+ public override int PacketSize => 8192;
+
+ ///
+ /// Changing Character?
+ ///
+ public bool ChangeCharacter;
+
+ public VitaruPacket(VitaruClientInfo vitaruClientInfo) : base(vitaruClientInfo)
+ {
+ ClientInfo = vitaruClientInfo;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Multi/VitaruPlayerInformation.cs b/osu.Game.Rulesets.Vitaru/Multi/VitaruPlayerInformation.cs
new file mode 100644
index 0000000000..37cac0ec48
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Multi/VitaruPlayerInformation.cs
@@ -0,0 +1,28 @@
+using osu.Game.Rulesets.Vitaru.Objects.Characters;
+using System;
+using System.Collections.Generic;
+
+namespace osu.Game.Rulesets.Vitaru.Multi
+{
+ [Serializable]
+ public class VitaruPlayerInformation
+ {
+ public string PlayerID = "0";
+
+ public Characters Character;
+
+ public float PlayerX;
+
+ public float PlayerY;
+
+ public float MouseX;
+
+ public float MouseY;
+
+ public float ClockSpeed;
+
+ public Dictionary Actions;
+
+ public VitaruAction PressedAction;
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Bullet.cs b/osu.Game.Rulesets.Vitaru/Objects/Bullet.cs
new file mode 100644
index 0000000000..12b4732c08
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Bullet.cs
@@ -0,0 +1,24 @@
+namespace osu.Game.Rulesets.Vitaru.Objects
+{
+ public class Bullet : VitaruHitObject
+ {
+ public override HitObjectType Type => HitObjectType.Bullet;
+
+ ///
+ /// Basically just bypasses all hitobject functionality (useful for player bullets)
+ ///
+ public bool DummyMode { get; set; }
+
+ public float BulletDamage { get; set; } = 10;
+ public float BulletSpeed { get; set; } = 1f;
+ public float BulletDiameter { get; set; } = 16f;
+ public float BulletAngleRadian { get; set; }
+ public bool DynamicBulletVelocity { get; set; }
+ // ReSharper disable once UnusedMember.Global
+ public bool Piercing { get; set; } = false;
+ public int Team { get; set; } = -1;
+ public bool ShootPlayer { get; set; }
+ public bool ObeyBoundries { get; } = true;
+ public bool Ghost { get; set; } = false;
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Characters/Enemy.cs b/osu.Game.Rulesets.Vitaru/Objects/Characters/Enemy.cs
new file mode 100644
index 0000000000..9009060700
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Characters/Enemy.cs
@@ -0,0 +1,121 @@
+using OpenTK;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.Vitaru.UI;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Vitaru.Objects.Drawables;
+using osu.Game.Rulesets.Vitaru.Settings;
+using osu.Framework.Platform;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Characters
+{
+ public class Enemy : VitaruCharacter
+ {
+ private readonly GraphicsPresets currentSkin = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GraphicsPresets);
+
+ public static int EnemyCount;
+ private readonly DrawablePattern drawablePattern;
+
+ public Enemy(Container parent, Pattern pattern, DrawablePattern drawablePattern) : base(parent)
+ {
+ this.drawablePattern = drawablePattern;
+ AlwaysPresent = true;
+ CharacterName = "enemy";
+ Team = 1;
+ CharacterColor = pattern.ComboColour;
+ HitboxWidth = 27;
+ }
+
+ protected override void LoadComplete()
+ {
+ EnemyCount++;
+
+ if (currentSkin == GraphicsPresets.StandardCompetitive)
+ VisibleHitbox.Alpha = 0.2f;
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ if (isDisposing)
+ EnemyCount--;
+ }
+
+ protected override void MovementAnimations()
+ {
+ if (CharacterLeftSprite.Texture == null && CharacterRightSprite != null)
+ {
+ CharacterLeftSprite.Texture = CharacterRightSprite.Texture;
+ CharacterLeftSprite.Size = new Vector2(-CharacterLeftSprite.Size.X, CharacterLeftSprite.Size.Y);
+ }
+ if (CharacterKiaiLeftSprite.Texture == null && CharacterKiaiRightSprite != null)
+ {
+ CharacterKiaiLeftSprite.Texture = CharacterKiaiRightSprite.Texture;
+ CharacterKiaiLeftSprite.Size = new Vector2(-CharacterKiaiLeftSprite.Size.X, CharacterKiaiLeftSprite.Size.Y);
+ }
+ if (Position.X > LastX)
+ {
+ if (CharacterLeftSprite.Texture != null)
+ CharacterLeftSprite.Alpha = 0;
+ if (CharacterRightSprite?.Texture != null)
+ CharacterRightSprite.Alpha = 1;
+ if (CharacterStillSprite.Texture != null)
+ CharacterStillSprite.Alpha = 0;
+ if (CharacterKiaiLeftSprite.Texture != null)
+ CharacterKiaiLeftSprite.Alpha = 0;
+ if (CharacterKiaiRightSprite?.Texture != null)
+ CharacterKiaiRightSprite.Alpha = 1;
+ if (CharacterKiaiStillSprite.Texture != null)
+ CharacterKiaiStillSprite.Alpha = 0;
+ }
+ else if (Position.X < LastX)
+ {
+ if (CharacterLeftSprite.Texture != null)
+ CharacterLeftSprite.Alpha = 1;
+ if (CharacterRightSprite?.Texture != null)
+ CharacterRightSprite.Alpha = 0;
+ if (CharacterStillSprite.Texture != null)
+ CharacterStillSprite.Alpha = 0;
+ if (CharacterKiaiLeftSprite.Texture != null)
+ CharacterKiaiLeftSprite.Alpha = 1;
+ if (CharacterKiaiRightSprite?.Texture != null)
+ CharacterKiaiRightSprite.Alpha = 0;
+ if (CharacterKiaiStillSprite.Texture != null)
+ CharacterKiaiStillSprite.Alpha = 0;
+ }
+ LastX = Position.X;
+ }
+
+ protected override void LoadAnimationSprites(TextureStore textures, Storage storage)
+ {
+ base.LoadAnimationSprites(textures, storage);
+ CharacterRightSprite.Texture = VitaruSkinElement.LoadSkinElement(CharacterName, storage);
+ CharacterKiaiRightSprite.Texture = VitaruSkinElement.LoadSkinElement(CharacterName + "Kiai", storage);
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ {
+ base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
+
+ if (effectPoint.KiaiMode && CharacterSprite.Alpha == 1)
+ {
+ CharacterSprite.FadeOutFromOne(timingPoint.BeatLength / 4);
+ CharacterKiai.FadeInFromZero(timingPoint.BeatLength / 4);
+ }
+ if (!effectPoint.KiaiMode && CharacterSprite.Alpha == 0)
+ {
+ CharacterSprite.FadeInFromZero(timingPoint.BeatLength);
+ CharacterKiai.FadeOutFromOne(timingPoint.BeatLength);
+ }
+ }
+
+ public override void Death()
+ {
+ Dead = true;
+ drawablePattern.PrepPop();
+ Hitbox.HitDetection = false;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Crystal.cs b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Crystal.cs
new file mode 100644
index 0000000000..e71a43cb1e
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Crystal.cs
@@ -0,0 +1,35 @@
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.MathUtils;
+using osu.Game.Graphics.Containers;
+using OpenTK;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Characters.Pieces
+{
+ public class Crystal : BeatSyncedContainer
+ {
+ public Crystal()
+ {
+ Alpha = 0;
+ Child = new Sprite
+ {
+ Alpha = 0.8f,
+ Scale = new Vector2((float)RNG.NextDouble(100, 200) / 300),
+ Texture = VitaruRuleset.VitaruTextures.Get("crystal")
+ };
+ }
+
+ public void Pop(double duration, Easing easing = Easing.OutQuart)
+ {
+ this.MoveTo(new Vector2((float)RNG.NextDouble(-200, 200), (float)RNG.NextDouble(-200, 200)), duration, easing)
+ .FadeIn(duration / 8);
+ }
+
+ public void ReCollect(double duration, Easing easing = Easing.InQuart)
+ {
+ this.MoveTo(Vector2.Zero, duration, easing)
+ .Delay(duration - duration / 8)
+ .FadeOut(duration / 8);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Mask.cs b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Mask.cs
new file mode 100644
index 0000000000..7a6c1e80ee
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Mask.cs
@@ -0,0 +1,9 @@
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Characters.Pieces
+{
+ public class Mask : BeatSyncedContainer
+ {
+
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Metranome.cs b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Metranome.cs
new file mode 100644
index 0000000000..7ddad5bd77
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Metranome.cs
@@ -0,0 +1,84 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Graphics.Backgrounds;
+using osu.Game.Graphics.Containers;
+using osu.Game.Screens.Menu;
+using System;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Characters.Pieces
+{
+ public class Metranome : BeatSyncedContainer
+ {
+ private readonly Sprite sign;
+ private readonly LogoVisualisation visualizer;
+
+ public Metranome()
+ {
+ AlwaysPresent = true;
+ Size = new Vector2(120);
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ Alpha = 0;
+
+ Children = new Drawable[]
+ {
+ new CircularContainer
+ {
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.9f),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+
+ Child = new Triangles
+ {
+ ColourDark = Color4.Pink,
+ ColourLight = Color4.Cyan,
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ sign = new Sprite
+ {
+ Colour = Color4.Cyan,
+ RelativeSizeAxes = Axes.Both,
+ Texture = VitaruRuleset.VitaruTextures.Get("sign"),
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ },
+ visualizer = new LogoVisualisation
+ {
+ Colour = Color4.DeepPink,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.Both,
+ Size = new Vector2(0.96f)
+ }
+ };
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ {
+ base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
+
+ float amplitudeAdjust = Math.Min(1, 0.4f + amplitudes.Maximum);
+
+ const double beat_in_time = 60;
+
+ this.ScaleTo(1 - 0.05f * amplitudeAdjust, beat_in_time, Easing.Out);
+ using (BeginDelayedSequence(beat_in_time))
+ this.ScaleTo(1, timingPoint.BeatLength * 2, Easing.OutQuint);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ sign.RotateTo(-(float)(Clock.CurrentTime / 1000 * 90) / 2);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Rift.cs b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Rift.cs
new file mode 100644
index 0000000000..e769d9eedb
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Rift.cs
@@ -0,0 +1,31 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Graphics.Sprites;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Characters.Pieces
+{
+ public class Rift : Sprite
+ {
+ public Rift LinkedRift;
+
+ public Rift(Color4 color)
+ {
+ AlwaysPresent = true;
+
+ Anchor = Framework.Graphics.Anchor.TopLeft;
+ Origin = Framework.Graphics.Anchor.Centre;
+
+ Alpha = 0;
+ Colour = color;
+ Size = new Vector2(80);
+ Texture = VitaruRuleset.VitaruTextures.Get("vortex");
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ Rotation = (float)(Clock.CurrentTime / -1000 * 90);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Totem.cs b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Totem.cs
new file mode 100644
index 0000000000..c00de25746
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/Totem.cs
@@ -0,0 +1,48 @@
+using OpenTK;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Vitaru.Objects.Drawables;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Characters.Pieces
+{
+ public class Totem : BeatSyncedContainer
+ {
+ public readonly VitaruCharacter ParentCharacter;
+ public float StartAngle { get; set; } = 0;
+
+ public Totem(VitaruCharacter vitaruCharacter)
+ {
+ ParentCharacter = vitaruCharacter;
+ }
+
+ public void Shoot()
+ {
+ DrawableSeekingBullet s;
+ ParentCharacter.Parent.Add(s = new DrawableSeekingBullet(ParentCharacter.Parent, new SeekingBullet
+ {
+ Team = ParentCharacter.Team,
+ BulletSpeed = 0.8f,
+ BulletDamage = 5,
+ ComboColour = ParentCharacter.CharacterColor,
+ StartAngle = StartAngle,
+ }));
+ s.MoveTo(ToSpaceOfOtherDrawable(new Vector2(0, 0), s));
+ }
+
+ protected override void LoadComplete()
+ {
+ Masking = true;
+ Size = new Vector2(6);
+ Origin = Anchor.Centre;
+ Anchor = Anchor.Centre;
+ BorderThickness = 2;
+ BorderColour = ParentCharacter.CharacterColor;
+ CornerRadius = 3;
+ Child= new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/UFO.cs b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/UFO.cs
new file mode 100644
index 0000000000..1e5f873604
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Characters/Pieces/UFO.cs
@@ -0,0 +1,69 @@
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Characters.Pieces
+{
+ public class UFO : BeatSyncedContainer
+ {
+ public readonly VitaruPlayer ParentNue;
+ public readonly UFOType UFOType;
+ private readonly Color4 color;
+
+ public VitaruPlayer AttachedPlayer;
+
+ public UFO(VitaruPlayer player, UFOType type)
+ {
+ ParentNue = player;
+ UFOType = type;
+ AttachedPlayer = player;
+
+ switch (type)
+ {
+ case UFOType.Mark:
+ color = Color4.Purple;
+ break;
+ case UFOType.Health:
+ color = Color4.Green;
+ break;
+ case UFOType.Energy:
+ color = Color4.Blue;
+ break;
+ case UFOType.Damage:
+ color = Color4.Red;
+ break;
+ }
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ Masking = true;
+ Colour = color;
+ Size = new Vector2(10);
+ CornerRadius = Size.X / 3;
+ Alpha = 0.5f;
+
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Radius = Width,
+ Colour = color.Opacity(0.5f)
+ };
+ }
+ }
+
+ public enum UFOType
+ {
+ Mark,
+ Health,
+ Energy,
+ Damage
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Characters/VitaruCharacter.cs b/osu.Game.Rulesets.Vitaru/Objects/Characters/VitaruCharacter.cs
new file mode 100644
index 0000000000..78fce10d79
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Characters/VitaruCharacter.cs
@@ -0,0 +1,346 @@
+using OpenTK;
+using osu.Framework.Graphics;
+using OpenTK.Graphics;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.Graphics.Textures;
+using osu.Framework.Allocation;
+using osu.Game.Rulesets.Vitaru.Objects.Drawables;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Vitaru.UI;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Extensions.Color4Extensions;
+using Container = osu.Framework.Graphics.Containers.Container;
+using Symcol.Core.GameObjects;
+using System.ComponentModel;
+using osu.Framework.Platform;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Characters
+{
+ public abstract class VitaruCharacter : BeatSyncedContainer
+ {
+ protected Sprite CharacterStillSprite;
+ protected Sprite CharacterRightSprite;
+ protected Sprite CharacterLeftSprite;
+ protected Sprite CharacterKiaiStillSprite;
+ protected Sprite CharacterKiaiRightSprite;
+ protected Sprite CharacterKiaiLeftSprite;
+ protected Sprite CharacterSign;
+ protected Container CharacterKiai;
+ protected Container CharacterSprite;
+ public Color4 CharacterColor;
+ protected string CharacterName = "null";
+ public float HitboxWidth { get; set; } = 4;
+ protected CircularContainer VisibleHitbox;
+ public SymcolHitbox Hitbox;
+ public bool CanHeal = false;
+ protected float LastX;
+
+ ///
+ /// Should be assigned to only in ctor, and is essential for hit detection
+ ///
+ public new readonly Container Parent;
+
+ protected VitaruCharacter(Container parent)
+ {
+ Parent = parent;
+ }
+
+ ///
+ /// Does animations to better give the illusion of movement (could likely be cleaned up)
+ ///
+ protected virtual void MovementAnimations()
+ {
+ if (CharacterLeftSprite.Texture == null && CharacterRightSprite != null)
+ {
+ CharacterLeftSprite.Texture = CharacterRightSprite.Texture;
+ CharacterLeftSprite.Size = new Vector2(-CharacterLeftSprite.Size.X, CharacterLeftSprite.Size.Y);
+ }
+ if (CharacterKiaiLeftSprite.Texture == null && CharacterKiaiRightSprite != null)
+ {
+ CharacterKiaiLeftSprite.Texture = CharacterKiaiRightSprite.Texture;
+ CharacterKiaiLeftSprite.Size = new Vector2(-CharacterKiaiLeftSprite.Size.X, CharacterKiaiLeftSprite.Size.Y);
+ }
+ if (Position.X > LastX)
+ {
+ if (CharacterLeftSprite.Texture != null)
+ CharacterLeftSprite.Alpha = 0;
+ if (CharacterRightSprite?.Texture != null)
+ CharacterRightSprite.Alpha = 1;
+ if (CharacterStillSprite.Texture != null)
+ CharacterStillSprite.Alpha = 0;
+ if (CharacterKiaiLeftSprite.Texture != null)
+ CharacterKiaiLeftSprite.Alpha = 0;
+ if (CharacterKiaiRightSprite?.Texture != null)
+ CharacterKiaiRightSprite.Alpha = 1;
+ if (CharacterKiaiStillSprite.Texture != null)
+ CharacterKiaiStillSprite.Alpha = 0;
+ }
+ else if (Position.X < LastX)
+ {
+ if (CharacterLeftSprite.Texture != null)
+ CharacterLeftSprite.Alpha = 1;
+ if (CharacterRightSprite?.Texture != null)
+ CharacterRightSprite.Alpha = 0;
+ if (CharacterStillSprite.Texture != null)
+ CharacterStillSprite.Alpha = 0;
+ if (CharacterKiaiLeftSprite.Texture != null)
+ CharacterKiaiLeftSprite.Alpha = 1;
+ if (CharacterKiaiRightSprite?.Texture != null)
+ CharacterKiaiRightSprite.Alpha = 0;
+ if (CharacterKiaiStillSprite.Texture != null)
+ CharacterKiaiStillSprite.Alpha = 0;
+ }
+ else
+ {
+ if (CharacterLeftSprite.Texture != null)
+ CharacterLeftSprite.Alpha = 0;
+ if (CharacterRightSprite?.Texture != null)
+ CharacterRightSprite.Alpha = 0;
+ if (CharacterStillSprite.Texture != null)
+ CharacterStillSprite.Alpha = 1;
+ if (CharacterKiaiLeftSprite.Texture != null)
+ CharacterKiaiLeftSprite.Alpha = 0;
+ if (CharacterKiaiRightSprite?.Texture != null)
+ CharacterKiaiRightSprite.Alpha = 0;
+ if (CharacterKiaiStillSprite.Texture != null)
+ CharacterKiaiStillSprite.Alpha = 1;
+ }
+ LastX = Position.X;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (Health <= 0 && !Dead)
+ Death();
+
+ foreach (Drawable draw in Parent)
+ {
+ DrawableBullet bullet = draw as DrawableBullet;
+ if (bullet?.Hitbox != null)
+ {
+ ParseBullet(bullet);
+ if (Hitbox.HitDetect(Hitbox, bullet.Hitbox))
+ {
+ Damage(bullet.Bullet.BulletDamage);
+ bullet.Bullet.BulletDamage = 0;
+ bullet.Hit = true;
+ }
+ }
+
+ DrawableSeekingBullet seekingBullet = draw as DrawableSeekingBullet;
+ if (seekingBullet?.Hitbox != null)
+ {
+ if (Hitbox.HitDetect(Hitbox, seekingBullet.Hitbox))
+ {
+ Damage(seekingBullet.SeekingBullet.BulletDamage);
+ seekingBullet.SeekingBullet.BulletDamage = 0;
+ seekingBullet.Hit = true;
+ }
+ }
+
+ DrawableLaser laser = draw as DrawableLaser;
+ if (laser?.Hitbox != null)
+ {
+ if (Hitbox.HitDetect(Hitbox, laser.Hitbox))
+ {
+ Damage(laser.Laser.LaserDamage * (1000 / (float)Clock.ElapsedFrameTime));
+ laser.Hit = true;
+ }
+ }
+ }
+
+ MovementAnimations();
+ }
+
+ ///
+ /// Gets called just before hit detection
+ ///
+ protected virtual void ParseBullet(DrawableBullet bullet) { }
+
+ protected virtual void LoadAnimationSprites(TextureStore textures, Storage storage) { }
+
+ ///
+ /// Child loading for all Characters (Enemies, Player, Bosses)
+ ///
+ [BackgroundDependencyLoader]
+ private void load(TextureStore textures, Storage storage)
+ {
+ Health = MaxHealth;
+ //Drawable stuff loading
+ Origin = Anchor.Centre;
+ Anchor = Anchor.TopLeft;
+ Children = new Drawable[]
+ {
+ CharacterSign = new Sprite
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = CharacterColor,
+ },
+ CharacterSprite = new Container
+ {
+ Colour = CharacterColor,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 1,
+ Children = new Drawable[]
+ {
+ CharacterStillSprite = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 1,
+ },
+ CharacterRightSprite = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 0,
+ },
+ CharacterLeftSprite = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 0,
+ },
+ }
+ },
+ CharacterKiai = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 0,
+ Children = new Drawable[]
+ {
+ CharacterKiaiStillSprite = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 1,
+ },
+ CharacterKiaiRightSprite = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 0,
+ },
+ CharacterKiaiLeftSprite = new Sprite
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 0,
+ },
+ }
+ },
+ VisibleHitbox = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 0,
+ Size = new Vector2(HitboxWidth),
+ BorderColour = CharacterColor,
+ BorderThickness = HitboxWidth / 3,
+ Masking = true,
+
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ },
+ EdgeEffect = new EdgeEffectParameters
+ {
+
+ Radius = HitboxWidth,
+ Type = EdgeEffectType.Shadow,
+ Colour = CharacterColor.Opacity(0.5f)
+ }
+ }
+ };
+
+ Add(Hitbox = new SymcolHitbox(new Vector2(HitboxWidth)) { Team = Team });
+
+ if (CharacterName == "player" || CharacterName == "enemy")
+ CharacterKiai.Colour = CharacterColor;
+
+ CharacterStillSprite.Texture = VitaruSkinElement.LoadSkinElement(CharacterName, storage);
+ CharacterKiaiStillSprite.Texture = VitaruSkinElement.LoadSkinElement(CharacterName + "Kiai", storage);
+ CharacterSign.Texture = VitaruSkinElement.LoadSkinElement("sign", storage);
+ LoadAnimationSprites(textures, storage);
+ }
+
+ #region eden.Game.GamePieces.Character.cs
+ ///
+ /// Maximum health this charcter can have
+ ///
+ public float MaxHealth = 100;
+
+ ///
+ /// The team this character is on, used mostly for Hitbox
+ ///
+ public int Team { get; set; }
+
+ ///
+ /// If this character has hit 0 health
+ ///
+ public bool Dead;
+
+ ///
+ /// the amount of health this character has
+ ///
+ public float Health;
+
+ ///
+ /// Removes "damage"
+ ///
+ ///
+ ///
+ public virtual float Damage(float damage)
+ {
+ Health -= damage;
+
+ if (Health < 0)
+ {
+ Health = 0;
+ Death();
+ }
+
+ return Health;
+ }
+
+ ///
+ /// Adds "health"
+ ///
+ ///
+ ///
+ public virtual float Heal(float health)
+ {
+ if (Health <= 0 && health > 0)
+ Revive();
+
+ Health += health;
+
+ if (Health > MaxHealth)
+ Health = MaxHealth;
+
+ return Health;
+ }
+
+ ///
+ /// Called when this character runs out of health
+ ///
+ public virtual void Death()
+ {
+ Dead = true;
+ Expire();
+ }
+
+ public virtual void Revive()
+ {
+ Dead = false;
+ }
+ #endregion
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Characters/VitaruPlayer.cs b/osu.Game.Rulesets.Vitaru/Objects/Characters/VitaruPlayer.cs
new file mode 100644
index 0000000000..e6e9a1532b
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Characters/VitaruPlayer.cs
@@ -0,0 +1,1801 @@
+using OpenTK;
+using osu.Framework.Graphics;
+using System.Collections.Generic;
+using OpenTK.Graphics;
+using System;
+using osu.Game.Rulesets.Vitaru.Objects.Drawables;
+using osu.Framework.Audio.Track;
+using osu.Framework.Graphics.Textures;
+using osu.Game.Rulesets.Vitaru.Settings;
+using osu.Game.Rulesets.Vitaru.Scoring;
+using osu.Framework.Audio;
+using osu.Game.Rulesets.Vitaru.UI;
+using osu.Framework.Timing;
+using static osu.Game.Rulesets.Vitaru.UI.Cursor.GameplayCursor;
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Framework.MathUtils;
+using osu.Game.Beatmaps;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Framework.Input.Bindings;
+using osu.Game.Graphics.Containers;
+using osu.Framework.Platform;
+using osu.Game.Rulesets.Vitaru.Objects.Characters.Pieces;
+using osu.Game.Rulesets.Vitaru.Multi;
+using Symcol.Core.Networking;
+using System.ComponentModel;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Characters
+{
+ public class VitaruPlayer : VitaruCharacter, IKeyBindingHandler
+ {
+ #region Fields
+ private readonly Characters currentCharacter;
+ private readonly GraphicsPresets currentSkin = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GraphicsPresets);
+ private readonly ScoringMetric currentScoringMetric = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.ScoringMetric);
+ private readonly VitaruGamemode currentGameMode = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GameMode);
+
+ public int ScoreZone = 100;
+
+ public Dictionary Actions = new Dictionary();
+
+ public VitaruNetworkingClientHandler VitaruNetworkingClientHandler { get; set; }
+
+ public string PlayerID;
+
+ //(MinX,MaxX,MinY,MaxY)
+ private Vector4 playerBounds = new Vector4(0, 512, 0, 820);
+
+ private const float player_speed = 0.25f;
+
+ public bool Invert { get; set; }
+
+ //Is not Human
+ public bool Bot { get; set; }
+
+ //Has a parent Player?
+ public bool Clone { get; set; }
+
+ ///
+ /// Are we a slave online?
+ ///
+ public bool Puppet { get; set; }
+
+ private readonly static List playerList = new List();
+
+ private readonly Bindable workingBeatmap = new Bindable();
+ private List cloneList = new List();
+ private readonly List crystalList = new List();
+ private VitaruPlayer parentPlayer;
+ private const float field_of_view = 60;
+ public float SpeedMultiplier = 1;
+ private OsuTextFlowContainer textContainer;
+
+ private List ufoList = new List();
+ private Framework.Graphics.Containers.Container ufoContainer;
+ private UFO ufoMark;
+ private UFO ufoHealth;
+ private UFO ufoEnergy;
+ private UFO ufoDamage;
+ private readonly float originalMaxHealth;
+
+ private bool riftActive;
+ private Rift riftStart;
+ private Rift riftEnd;
+ private double warpTime = double.MinValue;
+
+ private bool vampuric;
+
+ private DrawableLaser drawableLaser;
+
+ private Totem leftTotem;
+ private Totem rightTotem;
+
+ private Metranome metranome;
+ public int Combo;
+ private float damageMultiplier = 1;
+ private const float hitwindow = 40;
+
+ //Automatic play, ignores player input
+ public bool Auto { get; set; }
+
+ private double packetTime = double.MinValue;
+ private double lastQuarterBeat = -1;
+ private double nextHalfBeat = -1;
+ private double nextQuarterBeat = -1;
+ private double beatLength = 1000;
+ private bool leader;
+ private double reFrozenTime = double.MaxValue;
+ private double timeFreezeEndTime = double.MinValue;
+ private double reFreezeTime = double.MaxValue;
+ private float originalRate;
+ public float SetRate = 0.2f;
+ private float currentRate = 1;
+ private bool timeFreezeActive;
+ private bool tabooActive;
+ private bool ghostActive;
+ private bool shattered;
+ private readonly float energyRequired = 50;
+ private readonly float energyRequiredPerSecond;
+ private readonly float maxEnergy = 100;
+ private float healingMultiplier = 1;
+ private readonly float energyGainMultiplier = 1;
+ public float Energy;
+
+ //For debug ui only
+ public static float Energystored;
+ #endregion
+
+ #region Loading Stuff
+ public VitaruPlayer(Framework.Graphics.Containers.Container parent, Characters characterOverride, VitaruPlayer parentPlayer = null) : base(parent)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ playerList.Add(this);
+
+ currentCharacter = characterOverride;
+
+ if (parentPlayer != null)
+ this.parentPlayer = parentPlayer;
+
+ Actions[VitaruAction.Up] = false;
+ Actions[VitaruAction.Down] = false;
+ Actions[VitaruAction.Left] = false;
+ Actions[VitaruAction.Right] = false;
+ Actions[VitaruAction.Slow] = false;
+ Actions[VitaruAction.Fast] = false;
+ Actions[VitaruAction.Shoot] = false;
+
+ CharacterName = "player";
+ Team = 0;
+ MaxHealth = 100;
+ Position = new Vector2(256, 700);
+
+ switch (currentCharacter)
+ {
+ default:
+ CharacterColor = Color4.White;
+ break;
+ /*
+ case Characters.Alex:
+ energyRequired = 20;
+ maxEnergy = 40;
+ CharacterColor = Color4.Gold;
+ //CharacterName = "arysa";
+ break;
+ */
+ case Characters.ReimuHakurei:
+ CharacterColor = Color4.Red;
+ CharacterName = "reimu";
+ break;
+ case Characters.MarisaKirisame:
+ CharacterColor = Color4.Black;
+ CharacterName = "marisa";
+ energyRequired = 10;
+ break;
+ case Characters.SakuyaIzayoi:
+ CharacterColor = Color4.Navy;
+ energyRequired = 2;
+ energyRequiredPerSecond = 4;
+ maxEnergy = 24;
+ CharacterName = "sakuya";
+ break;
+ case Characters.HongMeiling:
+
+ if (!resurrected)
+ MaxHealth = 0;
+ else
+ MaxHealth = 20;
+
+ maxEnergy = 36;
+ leader = true;
+ CharacterColor = Color4.Orange;
+ break;
+ case Characters.FlandreScarlet:
+ maxEnergy = 80;
+ energyRequired = 40;
+ CharacterColor = Color4.Red;
+ break;
+ case Characters.RemiliaScarlet:
+ CharacterColor = Color4.Pink;
+ vampuric = true;
+ maxEnergy = 60;
+ MaxHealth = 60;
+ energyRequired = 1;
+ break;
+ case Characters.Cirno:
+ MaxHealth = 80;
+ maxEnergy = 40;
+ energyRequired = 40;
+ CharacterColor = Color4.Blue;
+ break;
+ case Characters.TenshiHinanai:
+ CharacterColor = Color4.DarkBlue;
+ break;
+ case Characters.YuyukoSaigyouji:
+ CharacterColor = Color4.LightBlue;
+ break;
+ case Characters.YukariYakumo:
+ CharacterColor = Color4.DarkViolet;
+ maxEnergy = 24;
+ energyRequiredPerSecond = 4;
+ MaxHealth = 80;
+ energyRequired = 4;
+ break;
+ case Characters.Chen:
+ CharacterColor = Color4.Green;
+ CharacterName = "chen";
+ break;
+ case Characters.KokoroHatano:
+ CharacterColor = Color4.Cyan;
+ maxEnergy = 36;
+ break;
+ case Characters.Kaguya:
+ CharacterColor = Color4.DarkRed;
+ CharacterName = "kaguya";
+ maxEnergy = 24;
+ energyRequired = 2;
+ energyRequiredPerSecond = 2;
+ break;
+ case Characters.IbarakiKasen:
+ CharacterColor = Color4.YellowGreen;
+ maxEnergy = 8;
+ energyRequired = 2;
+ MaxHealth = 40;
+ break;
+ case Characters.NueHoujuu:
+ CharacterColor = Color4.DarkGray;
+ CharacterName = "nue";
+ MaxHealth = 80;
+ maxEnergy = 24;
+ energyRequired = 0;
+ break;
+ case Characters.AliceMuyart:
+ MaxHealth = 200;
+ healingMultiplier = 2;
+ energyGainMultiplier = 2;
+ maxEnergy = 200;
+ energyRequired = 10;
+ energyRequiredPerSecond = 4;
+ CharacterColor = Color4.SkyBlue;
+ break;
+ case Characters.ArysaMuyart:
+ break;
+ }
+
+ originalMaxHealth = MaxHealth;
+
+ if (currentGameMode == VitaruGamemode.Dodge)
+ playerBounds = new Vector4(0, 512, 0, 384);
+ }
+
+ protected override void LoadAnimationSprites(TextureStore textures, Storage storage)
+ {
+ base.LoadAnimationSprites(textures, storage);
+
+ CharacterRightSprite.Texture = VitaruSkinElement.LoadSkinElement(CharacterName + "Right", storage);
+ CharacterKiaiRightSprite.Texture = VitaruSkinElement.LoadSkinElement(CharacterName + "KiaiRight", storage);
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ if (VitaruNetworkingClientHandler != null)
+ VitaruNetworkingClientHandler.OnPacketReceive += (p) => packetReceived(p);
+
+ if (Invert)
+ Rotation += 180;
+
+ if (currentSkin == GraphicsPresets.StandardCompetitive || currentSkin == GraphicsPresets.HighPerformanceCompetitive)
+ VisibleHitbox.Alpha = 1;
+
+ if (currentGameMode == VitaruGamemode.Touhosu)
+ {
+ if (currentCharacter == Characters.ReimuHakurei | currentCharacter == Characters.MarisaKirisame)
+ {
+ AddRange(new Drawable[]
+ {
+ leftTotem = new Totem(this)
+ {
+ Position = new Vector2(-20, -30),
+ StartAngle = -20,
+ },
+ rightTotem = new Totem(this)
+ {
+ Position = new Vector2(20, -30),
+ StartAngle = 20,
+ }
+ });
+ }
+
+ Add(textContainer = new OsuTextFlowContainer(t => { t.TextSize = 24; })
+ {
+ Alpha = 0,
+ Position = new Vector2(0, 48),
+ Anchor = Anchor.BottomCentre,
+ Origin = Anchor.TopCentre,
+ Width = 100,
+ AutoSizeAxes = Axes.Both,
+ Text = ""
+ });
+
+ if (currentCharacter == Characters.Cirno)
+ {
+ for (int i = 0; i < 20; i++)
+ {
+ Crystal c = new Crystal { Position = new Vector2((float)RNG.NextDouble(-20, 20), (float)RNG.NextDouble(-40, 40)) };
+ crystalList.Add(c);
+ Add(c);
+ }
+ }
+
+ if (currentCharacter == Characters.YukariYakumo)
+ {
+ Parent.AddRange(new Drawable[]
+ {
+ riftStart = new Rift(Color4.DarkViolet),
+ riftEnd = new Rift(Color4.DarkRed)
+ });
+
+ riftStart.LinkedRift = riftEnd;
+ riftEnd.LinkedRift = riftStart;
+ }
+
+ if (currentCharacter == Characters.KokoroHatano)
+ {
+ Add(metranome = new Metranome());
+ Remove(CharacterSign);
+ }
+
+ Add(ufoContainer = new Framework.Graphics.Containers.Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre
+ });
+
+ if (currentCharacter == Characters.NueHoujuu)
+ {
+ ufoMark = new UFO(this, UFOType.Mark) { Position = new Vector2(0, -60) };
+ ufoHealth = new UFO(this, UFOType.Health) { Position = new Vector2(-60, 0) };
+ ufoEnergy = new UFO(this, UFOType.Energy) { Position = new Vector2(60, 0) };
+ ufoDamage = new UFO(this, UFOType.Damage) { Position = new Vector2(0, 60) };
+
+ ufoList.Add(ufoMark);
+ ufoList.Add(ufoHealth);
+ ufoList.Add(ufoEnergy);
+ ufoList.Add(ufoDamage);
+
+ foreach (UFO ufo in ufoList)
+ ufoContainer.Add(ufo);
+ }
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ if (isDisposing)
+ playerList.Remove(this);
+ base.Dispose(isDisposing);
+ }
+ #endregion
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ {
+ base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
+
+ float amplitudeAdjust = Math.Min(1, 0.4f + amplitudes.Maximum);
+
+ beatLength = timingPoint.BeatLength;
+
+ if (!Clone && Bot && currentGameMode == VitaruGamemode.Touhosu)
+ spell();
+
+ if (Actions[VitaruAction.Shoot] && currentGameMode != VitaruGamemode.Dodge && currentCharacter == Characters.MarisaKirisame | currentCharacter == Characters.ReimuHakurei)
+ {
+ leftTotem.Shoot();
+ rightTotem.Shoot();
+ }
+
+ onHalfBeat();
+ lastQuarterBeat = Time.Current;
+ nextHalfBeat = Time.Current + timingPoint.BeatLength / 2;
+ nextQuarterBeat = Time.Current + timingPoint.BeatLength / 4;
+
+ const double beat_in_time = 60;
+
+ CharacterSign.ScaleTo(1 - 0.02f * amplitudeAdjust, beat_in_time, Easing.Out);
+ using (CharacterSign.BeginDelayedSequence(beat_in_time))
+ CharacterSign.ScaleTo(1, beatLength * 2, Easing.OutQuint);
+
+ if (effectPoint.KiaiMode && currentGameMode != VitaruGamemode.Touhosu)
+ {
+ CharacterSign.FadeTo(0.25f * amplitudeAdjust, beat_in_time, Easing.Out);
+ using (CharacterSign.BeginDelayedSequence(beat_in_time))
+ CharacterSign.FadeOut(beatLength);
+ }
+
+ if (effectPoint.KiaiMode && CharacterSprite.Alpha == 1)
+ {
+ if (!Dead)
+ {
+ CharacterKiai.FadeInFromZero(timingPoint.BeatLength / 4);
+ CharacterSprite.FadeOutFromOne(timingPoint.BeatLength / 4);
+ }
+
+ if (currentGameMode != VitaruGamemode.Touhosu)
+ CharacterSign.FadeTo(0.15f , timingPoint.BeatLength / 4);
+ }
+ if(!effectPoint.KiaiMode && CharacterKiai.Alpha == 1)
+ {
+ if (!Dead)
+ {
+ CharacterSprite.FadeInFromZero(timingPoint.BeatLength);
+ CharacterKiai.FadeOutFromOne(timingPoint.BeatLength);
+ }
+
+ if (currentGameMode != VitaruGamemode.Touhosu)
+ CharacterSign.FadeTo(0f, timingPoint.BeatLength);
+ }
+ }
+
+ private void onHalfBeat()
+ {
+ nextHalfBeat = -1;
+
+ if (Actions[VitaruAction.Shoot] && currentGameMode != VitaruGamemode.Dodge && currentCharacter == Characters.Cirno && !shattered)
+ patternWave();
+ else if (Actions[VitaruAction.Shoot] && currentGameMode != VitaruGamemode.Dodge && currentCharacter != Characters.Cirno)
+ patternWave();
+
+ if (CanHeal)
+ {
+ CanHeal = false;
+
+ Heal(1 * healingMultiplier);
+
+ if (currentGameMode != VitaruGamemode.Touhosu)
+ {
+ CharacterSign.Alpha = 0.2f;
+ CharacterSign.FadeOut(beatLength / 2);
+ }
+ }
+ }
+
+ private void onQuarterBeat()
+ {
+ lastQuarterBeat = nextQuarterBeat;
+ nextQuarterBeat += beatLength / 4;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (currentGameMode == VitaruGamemode.Touhosu)
+ {
+ speakingUpdate();
+ spellUpdate();
+ }
+
+ playerInput();
+ checkScoreZone();
+
+ if (nextHalfBeat <= Time.Current && nextHalfBeat != -1)
+ onHalfBeat();
+
+ if (nextQuarterBeat <= Time.Current && nextQuarterBeat != -1)
+ onQuarterBeat();
+
+ if (CharacterSign.Alpha > 0)
+ CharacterSign.RotateTo((float)(Clock.CurrentTime / 1000 * 90));
+
+ if (VitaruNetworkingClientHandler != null && packetTime + 250 <= Time.Current)
+ {
+ packetTime = Time.Current;
+ sendPacket();
+ }
+ }
+
+ protected override void ParseBullet(DrawableBullet bullet)
+ {
+ base.ParseBullet(bullet);
+
+ //Not sure why this offset is needed atm
+ Vector2 object2Pos = bullet.ToSpaceOfOtherDrawable(Vector2.Zero, this) + new Vector2(6);
+ float distance = (float)Math.Sqrt(Math.Pow(object2Pos.X, 2) + Math.Pow(object2Pos.Y, 2));
+ float edgeDistance = distance - (bullet.Width / 2 + Hitbox.Width / 2);
+
+ if (currentCharacter == Characters.Kaguya && ghostActive)
+ {
+ Hitbox.HitDetection = true;
+ if (Hitbox.HitDetect(Hitbox, bullet.Hitbox) && bullet.Bullet.Ghost)
+ {
+ Damage(bullet.Bullet.BulletDamage);
+ bullet.Bullet.BulletDamage = 0;
+ bullet.Hit = true;
+ }
+ Hitbox.HitDetection = false;
+ }
+
+ if (edgeDistance < 48 && bullet.Bullet.Team != Team)
+ CanHeal = true;
+
+ if (currentScoringMetric == ScoringMetric.Graze)
+ {
+ if (currentGameMode == VitaruGamemode.Dodge)
+ distance *= 1.5f;
+ if (distance <= 64 && bullet.ScoreZone < 300)
+ bullet.ScoreZone = 300;
+ else if (distance <= 128 && bullet.ScoreZone < 200)
+ bullet.ScoreZone = 200;
+ else if (distance <= 256 && bullet.ScoreZone < 100)
+ bullet.ScoreZone = 100;
+ else if (bullet.ScoreZone < 50)
+ bullet.ScoreZone = 50;
+ }
+ }
+
+ ///
+ /// Check to see what kinda points we should award the player
+ ///
+ private void checkScoreZone()
+ {
+ if (currentScoringMetric != ScoringMetric.Graze)
+ {
+ var scoreZone = new Vector2(256, -512);
+ var distance = (float)Math.Sqrt(Math.Pow(Position.X - scoreZone.X, 2) + Math.Pow(Position.Y - scoreZone.Y, 2));
+
+ if (distance <= 1024 - 256 - 128)
+ ScoreZone = 0;
+ else if (distance <= 1024 - 256)
+ ScoreZone = 100;
+ else if (distance <= 1024 - 128)
+ ScoreZone = 200;
+ else if (distance <= 1024)
+ ScoreZone = 300;
+ else if (distance <= 1024 + 256)
+ ScoreZone = 200;
+ else
+ ScoreZone = 100;
+ }
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuGameBase game)
+ {
+ workingBeatmap.BindTo(game.Beatmap);
+ }
+
+ #region Spell Stuff
+
+ private void spell(bool keyUp = false, VitaruAction action = VitaruAction.Spell)
+ {
+ if (Energy >= energyRequired && currentGameMode == VitaruGamemode.Touhosu && !keyUp || currentCharacter == Characters.AliceMuyart && currentGameMode == VitaruGamemode.Touhosu && !keyUp)
+ {
+ //if (currentCharacter == Characters.Alex && action == VitaruAction.Spell)
+ //alexSpell();
+ if (currentCharacter == Characters.ReimuHakurei && action == VitaruAction.Spell)
+ reimuaSpell();
+ else if (currentCharacter == Characters.MarisaKirisame && action == VitaruAction.Spell)
+ marisaSpell();
+ else if (currentCharacter == Characters.SakuyaIzayoi && action == VitaruAction.Spell)
+ sakuyaSpell();
+ else if (currentCharacter == Characters.FlandreScarlet && !Clone && !tabooActive && action == VitaruAction.Spell)
+ flandereSpell();
+ else if (currentCharacter == Characters.RemiliaScarlet && action == VitaruAction.Spell)
+ {
+
+ }
+ else if (currentCharacter == Characters.YuyukoSaigyouji && action == VitaruAction.Spell)
+ {
+
+ }
+ else if (currentCharacter == Characters.YukariYakumo && action == VitaruAction.Spell)
+ yukariSpell();
+ else if (currentCharacter == Characters.Chen && action == VitaruAction.Spell)
+ {
+
+ }
+ else if (currentCharacter == Characters.Kaguya && !Clone && !ghostActive && action == VitaruAction.Spell)
+ kaguyaSpell();
+ else if (currentCharacter == Characters.IbarakiKasen && action == VitaruAction.Spell)
+ ibarakiSpell();
+ else if (currentCharacter == Characters.NueHoujuu && action == VitaruAction.Spell | action == VitaruAction.Spell2 | action == VitaruAction.Spell3 | action == VitaruAction.Spell4)
+ nueSpell(action);
+ else if (currentCharacter == Characters.AliceMuyart && !Clone)
+ {
+ switch (action)
+ {
+ case VitaruAction.Spell when Energystored > 2:
+ ibarakiSpell(2);
+ patternCircle();
+ break;
+ case VitaruAction.Spell3 when SetRate != 1 && Energystored > 6:
+ sakuyaSpell(6);
+ break;
+ case VitaruAction.Spell2 when !ghostActive && Energystored > 8:
+ kaguyaSpell(8);
+ break;
+ }
+ }
+ }
+ else if (keyUp)
+ {
+ switch (currentCharacter)
+ {
+ case Characters.SakuyaIzayoi when action == VitaruAction.Spell:
+ timeFreezeActive = false;
+ break;
+ case Characters.YukariYakumo when action == VitaruAction.Spell:
+ riftActive = false;
+ break;
+ case Characters.Kaguya when action == VitaruAction.Spell:
+ ghostActive = false;
+ break;
+ case Characters.AliceMuyart:
+ if (action == VitaruAction.Spell3)
+ timeFreezeActive = false;
+ else if (action == VitaruAction.Spell2)
+ ghostActive = false;
+ break;
+ }
+ }
+ }
+
+ private void reimuaSpell(float energyOverride = -1)
+ {
+
+ }
+
+ private void marisaSpell(float energyOverride = -1)
+ {
+ if (energyOverride == -1)
+ Energy -= energyRequired;
+ else
+ Energy -= energyOverride;
+
+ Parent.Add(drawableLaser = new DrawableLaser(Parent, new Laser
+ {
+ LaserSize = new Vector2(80, 400),
+ Team = Team,
+ ComboColour = CharacterColor,
+ StartTime = Time.Current,
+ EndTime = Time.Current + 2000
+ }));
+ drawableLaser.Position = Position;
+ }
+
+ private void sakuyaSpell(float energyOverride = -1)
+ {
+ if (energyOverride == -1)
+ Energy -= energyRequired;
+ else
+ Energy -= energyOverride;
+
+ timeFreezeActive = true;
+
+ if (originalRate == 0)
+ originalRate = (float)workingBeatmap.Value.Track.Rate;
+
+ currentRate = originalRate * SetRate;
+ applyToClock(workingBeatmap.Value.Track, currentRate);
+
+ timeFreezeEndTime = Time.Current + 1000;
+ }
+
+ private void flandereSpell(float energyOverride = -1)
+ {
+ if (energyOverride == -1)
+ Energy -= energyRequired;
+ else
+ Energy -= energyOverride;
+
+ tabooActive = true;
+ for (int i = 1; i < 4; i++)
+ {
+ Vector2 position = new Vector2(-40, -20);
+ if (i == 2)
+ {
+ position = new Vector2(40, 0);
+ }
+ else if (i == 3)
+ {
+ position = new Vector2(80, -20);
+ }
+
+ VitaruPlayer player;
+ Parent.Add(player = new VitaruPlayer(Parent, currentCharacter, this)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Bot = true,
+ Auto = true,
+ Position = Position + position
+ });
+ cloneList.Add(player);
+ }
+ }
+
+ private void remiliaSpell()
+ {
+
+ }
+
+ private void yukariSpell()
+ {
+ riftActive = true;
+ riftStart.FadeInFromZero(beatLength / 2);
+ riftStart.Position = new Vector2(Position.X, Position.Y - 64);
+ riftEnd.FadeInFromZero(beatLength / 2);
+ riftEnd.Position = VitaruCursor.CenterCircle.ToSpaceOfOtherDrawable(Vector2.Zero, Parent);
+ }
+
+ private void kokoroSpell()
+ {
+ if (currentCharacter == Characters.KokoroHatano)
+ {
+ if (Time.Current <= lastQuarterBeat + hitwindow | Time.Current >= nextQuarterBeat - hitwindow)
+ Combo++;
+ else
+ Combo = Math.Max(Combo -= 30, 0);
+
+ if (Combo >= 10)
+ metranome.Alpha = Math.Min((Combo - 10) / 100, 0.5f);
+ else
+ metranome.Alpha = 0;
+
+ Energystored += 0.01f * Combo;
+ damageMultiplier = Combo * 0.01f + 1;
+ healingMultiplier = Combo * 0.01f + 1;
+ }
+ }
+
+ private void kaguyaSpell(float energyOverride = -1)
+ {
+ if (energyOverride == -1)
+ Energy -= energyRequired;
+ else
+ Energy -= energyOverride;
+
+ ghostActive = true;
+ this.FadeTo(0.5f, beatLength / 2);
+ Hitbox.HitDetection = false;
+
+ VitaruPlayer player;
+ Parent.Add(player = new VitaruPlayer(Parent, currentCharacter, this)
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Position = Position,
+ Auto = true,
+ Bot = true,
+ Clone = true
+ });
+ player.FadeIn(beatLength / 2);
+ cloneList.Add(player);
+ }
+
+ private void ibarakiSpell(float energyOverride = -1)
+ {
+ if (energyOverride == -1)
+ Energy -= energyRequired;
+ else
+ Energy -= energyOverride;
+
+ Position = VitaruCursor.CenterCircle.ToSpaceOfOtherDrawable(Vector2.Zero, Hitbox);
+ }
+
+ private void nueSpell(VitaruAction action)
+ {
+ Energy -= energyRequired;
+
+ VitaruPlayer closestPlayer = null;
+ float closestPlayerDistance = 80;
+
+ foreach (VitaruPlayer player in playerList)
+ {
+ Vector2 playerPos = VitaruCursor.CenterCircle.ToSpaceOfOtherDrawable(Vector2.Zero, player) + new Vector2(6);
+ float distance = (float)Math.Sqrt(Math.Pow(playerPos.X, 2) + Math.Pow(playerPos.Y, 2));
+
+ if (closestPlayerDistance >= distance)
+ {
+ closestPlayerDistance = distance;
+ closestPlayer = player;
+ }
+ }
+
+ if (closestPlayer != null)
+ {
+ switch (action)
+ {
+ case VitaruAction.Spell:
+ ufoHealth.AttachedPlayer.ufoList.Remove(ufoHealth);
+ ufoHealth.AttachedPlayer.ufoContainer.Remove(ufoHealth);
+ closestPlayer.ufoList.Add(ufoHealth);
+ closestPlayer.ufoContainer.Add(ufoHealth);
+ ufoHealth.AttachedPlayer = closestPlayer;
+ break;
+ case VitaruAction.Spell2:
+ ufoEnergy.AttachedPlayer.ufoList.Remove(ufoEnergy);
+ ufoEnergy.AttachedPlayer.ufoContainer.Remove(ufoEnergy);
+ closestPlayer.ufoList.Add(ufoEnergy);
+ closestPlayer.ufoContainer.Add(ufoEnergy);
+ ufoEnergy.AttachedPlayer = closestPlayer;
+ break;
+ case VitaruAction.Spell3:
+ ufoDamage.AttachedPlayer.ufoList.Remove(ufoDamage);
+ ufoDamage.AttachedPlayer.ufoContainer.Remove(ufoDamage);
+ closestPlayer.ufoList.Add(ufoDamage);
+ closestPlayer.ufoContainer.Add(ufoDamage);
+ ufoDamage.AttachedPlayer = closestPlayer;
+ break;
+ case VitaruAction.Spell4:
+ ufoMark.AttachedPlayer.ufoList.Remove(ufoMark);
+ ufoMark.AttachedPlayer.ufoContainer.Remove(ufoMark);
+ closestPlayer.ufoList.Add(ufoMark);
+ closestPlayer.ufoContainer.Add(ufoMark);
+ ufoMark.AttachedPlayer = closestPlayer;
+ break;
+ }
+ }
+ else
+ {
+ switch (action)
+ {
+ case VitaruAction.Spell:
+ ufoHealth.AttachedPlayer.ufoList.Remove(ufoHealth);
+ ufoHealth.AttachedPlayer.ufoContainer.Remove(ufoHealth);
+ ufoList.Add(ufoHealth);
+ ufoContainer.Add(ufoHealth);
+ ufoHealth.AttachedPlayer = this;
+ break;
+ case VitaruAction.Spell2:
+ ufoEnergy.AttachedPlayer.ufoList.Remove(ufoEnergy);
+ ufoEnergy.AttachedPlayer.ufoContainer.Remove(ufoEnergy);
+ ufoList.Add(ufoEnergy);
+ ufoContainer.Add(ufoEnergy);
+ ufoEnergy.AttachedPlayer = this;
+ break;
+ case VitaruAction.Spell3:
+ ufoDamage.AttachedPlayer.ufoList.Remove(ufoDamage);
+ ufoDamage.AttachedPlayer.ufoContainer.Remove(ufoDamage);
+ ufoList.Add(ufoDamage);
+ ufoContainer.Add(ufoDamage);
+ ufoDamage.AttachedPlayer = this;
+ break;
+ case VitaruAction.Spell4:
+ ufoMark.AttachedPlayer.ufoList.Remove(ufoMark);
+ ufoMark.AttachedPlayer.ufoContainer.Remove(ufoMark);
+ ufoList.Add(ufoMark);
+ ufoContainer.Add(ufoMark);
+ ufoMark.AttachedPlayer = this;
+ break;
+ }
+ }
+ }
+
+ private void spellUpdate()
+ {
+ if (currentCharacter != Characters.NueHoujuu)
+ {
+ ufoMark = null;
+ ufoHealth = null;
+ ufoEnergy = null;
+ ufoDamage = null;
+
+ foreach (UFO ufo in ufoList)
+ {
+ switch (ufo.UFOType)
+ {
+ case UFOType.Mark:
+ ufoMark = ufo;
+ break;
+ case UFOType.Health:
+ ufoHealth = ufo;
+ break;
+ case UFOType.Energy:
+ ufoEnergy = ufo;
+ break;
+ case UFOType.Damage:
+ ufoDamage = ufo;
+ break;
+ }
+ }
+ }
+
+ if (ufoHealth != null && ufoHealth.ParentNue.Energy >= (float)Clock.ElapsedFrameTime / 1000)
+ {
+ MaxHealth = originalMaxHealth + 10;
+ ufoHealth.ParentNue.Energy -= (float)Clock.ElapsedFrameTime / 1000;
+ Heal((float)Clock.ElapsedFrameTime / 1000);
+ }
+ else
+ {
+ MaxHealth = originalMaxHealth;
+ if (Health > MaxHealth)
+ Health = MaxHealth;
+ }
+
+ if (ufoEnergy != null && ufoEnergy.ParentNue.Energy >= (float)Clock.ElapsedFrameTime / 500 && maxEnergy - Energy >= (float)Clock.ElapsedFrameTime / 250)
+ {
+ Energy = Math.Min((float)Clock.ElapsedFrameTime / 250 + Energy, maxEnergy);
+ ufoEnergy.ParentNue.Energy -= (float)Clock.ElapsedFrameTime / 500;
+ }
+ else if (CanHeal)
+ Energy = Math.Min((float)Clock.ElapsedFrameTime / 500 * energyGainMultiplier + Energy, maxEnergy);
+
+ CharacterSign.Alpha = Energy / (maxEnergy * 2);
+
+ if (ghostActive)
+ Energy -= (float)Clock.ElapsedFrameTime / 1000 * energyRequiredPerSecond;
+
+ if (riftActive)
+ Energy -= (float)Clock.ElapsedFrameTime / 1000 * energyRequiredPerSecond;
+
+ if (Energy <= 0)
+ {
+ Energy = 0;
+ ghostActive = false;
+ timeFreezeActive = false;
+ riftActive = false;
+ }
+
+ CharacterSign.Alpha = Energy / (maxEnergy * 2);
+
+ foreach (Drawable child in Parent.Children)
+ if (child is Rift rift)
+ {
+ Vector2 riftPos = rift.ToSpaceOfOtherDrawable(Vector2.Zero, Hitbox);
+ float distance = (float)Math.Sqrt(Math.Pow(riftPos.X + 20, 2) + Math.Pow(riftPos.Y + 20, 2));
+
+ if (distance <= 32 && warpTime <= Time.Current && rift.Alpha > 0)
+ {
+ warpTime = Time.Current + beatLength;
+ Position = rift.LinkedRift.ToSpaceOfOtherDrawable(Vector2.Zero, Parent);
+ }
+ }
+
+ if (timeFreezeEndTime >= Time.Current)
+ {
+ if (!timeFreezeActive)
+ {
+ currentRate += (float)Clock.ElapsedFrameTime / 100;
+ if (currentRate > originalRate)
+ currentRate = originalRate;
+ applyToClock(workingBeatmap.Value.Track, currentRate);
+ if (timeFreezeEndTime - 500 <= Time.Current)
+ {
+ currentRate = originalRate;
+ applyToClock(workingBeatmap.Value.Track, currentRate);
+ }
+ }
+ else
+ {
+ float energyDrainMultiplier = 0;
+ if (currentRate < 1)
+ energyDrainMultiplier = 1 - currentRate;
+ else if (currentRate >= 1)
+ energyDrainMultiplier = currentRate - 1;
+
+ Energy -= (float)Clock.ElapsedFrameTime / 1000 * (1 / currentRate) * energyRequiredPerSecond * energyDrainMultiplier;
+ timeFreezeEndTime = Time.Current + 2000;
+ currentRate = originalRate * SetRate;
+ applyToClock(workingBeatmap.Value.Track, currentRate);
+ }
+ }
+
+ if (leader && Health > 0)
+ {
+ foreach(VitaruPlayer player in playerList)
+ {
+ Vector2 otherPlayerPos = player.ToSpaceOfOtherDrawable(Vector2.Zero, this) + new Vector2(6);
+ float distance = (float)Math.Sqrt(Math.Pow(otherPlayerPos.X, 2) + Math.Pow(otherPlayerPos.Y, 2));
+
+ if (player.Hitbox.Team == Hitbox.Team && distance <= 128)
+ {
+ player.Heal(2 * (float)Clock.ElapsedFrameTime);
+ }
+ }
+ }
+
+ if (tabooActive)
+ {
+ if (cloneList.Count == 0)
+ tabooActive = false;
+ }
+
+ if (Time.Current >= reFreezeTime)
+ {
+ reFreezeTime = double.MaxValue;
+
+ foreach (Crystal crystal in crystalList)
+ crystal.ReCollect(1000);
+
+ CharacterKiai.Delay(900)
+ .FadeIn(100);
+ CharacterSprite.Delay(900)
+ .FadeIn(100);
+ }
+
+ if (Time.Current >= reFrozenTime)
+ {
+ reFrozenTime = double.MaxValue;
+ Dead = false;
+ shattered = false;
+ Hitbox.HitDetection = true;
+ }
+
+ if (!riftActive && riftStart != null && riftStart.Alpha == 1)
+ {
+ riftEnd.FadeOut(beatLength / 4);
+ riftStart.FadeOut(beatLength / 4);
+ }
+
+ if (!ghostActive && Alpha == 0.5f)
+ {
+ Hitbox.HitDetection = true;
+ foreach (VitaruPlayer clone in cloneList)
+ {
+ clone.FadeOut(beatLength / 2)
+ .Delay(beatLength / 2)
+ .Expire();
+ cloneList.Remove(clone);
+ break;
+ }
+
+ this.FadeIn(beatLength / 2);
+ }
+
+ if (ufoList.Count > 0)
+ ufoContainer.RotateTo((float)(Clock.CurrentTime / -1000 * 90));
+
+ //just for debugging
+ Energystored = Energy;
+ }
+
+ public override float Damage(float damage)
+ {
+ if (currentCharacter == Characters.Cirno)
+ {
+ Health -= damage;
+
+ if (Health <= 0 && energyRequired <= Energy)
+ {
+ Energy -= energyRequired;
+ shattered = true;
+ reFreezeTime = Time.Current + beatLength;
+ reFrozenTime = Time.Current + beatLength * 2;
+ Hitbox.HitDetection = false;
+
+ foreach (Crystal crystal in crystalList)
+ crystal.Pop(1000);
+ CharacterKiai.FadeOut(100);
+ CharacterSprite.FadeOut(100);
+
+ return Health = MaxHealth;
+ }
+ return Health;
+ }
+ return base.Damage(damage);
+ }
+
+ private void applyToClock(IAdjustableClock clock, float speed)
+ {
+ var pitchAdjust = clock as IHasPitchAdjust;
+ if (pitchAdjust != null)
+ pitchAdjust.PitchAdjust = speed;
+ SpeedMultiplier = 1 / speed;
+ foreach (Drawable draw in Parent)
+ {
+ VitaruPlayer player = draw as VitaruPlayer;
+ if (player?.Team == Team && player != this)
+ player.SpeedMultiplier = SpeedMultiplier / 2 + 0.5f;
+ }
+ }
+ #endregion
+
+ #region Shooting Stuff
+ private void bulletAddRad(float speed, float angle, Color4 color)
+ {
+ DrawableBullet drawableBullet;
+
+ if (Invert)
+ angle += (float)Math.PI;
+
+ Parent.Add(drawableBullet = new DrawableBullet(Parent,
+ new Bullet
+ {
+ StartTime = Time.Current,
+ Cs = 1.2f,
+ DummyMode = true,
+ ComboColour = color,
+ BulletAngleRadian = angle,
+ BulletSpeed = speed,
+ BulletDiameter = 16,
+ BulletDamage = 20 * damageMultiplier,
+ Team = Team,
+ Ghost = currentCharacter == Characters.Kaguya | currentCharacter == Characters.AliceMuyart
+ }));
+ if (vampuric)
+ drawableBullet.OnHit = () => Heal(0.5f);
+ drawableBullet.MoveTo(Position);
+ }
+
+ private void patternWave()
+ {
+ const int numberbullets = 3;
+ float directionModifier = -0.1F;
+ Color4 color = CharacterColor;
+ for (int i = 1; i <= numberbullets; i++)
+ {
+ if (currentCharacter == Characters.NueHoujuu)
+ {
+ if (i == 1)
+ color = Color4.Red;
+ else if (i == 2)
+ color = Color4.Black;
+ else
+ color = Color4.Blue;
+ }
+ //-90 = up
+ bulletAddRad(1, MathHelper.DegreesToRadians(-90) + directionModifier, color);
+ directionModifier += 0.1f;
+ }
+ }
+
+ private void patternCircle()
+ {
+ int numberbullets = 8;
+ float directionModifier = (360f / numberbullets);
+ float direction = MathHelper.DegreesToRadians(-90);
+ directionModifier = MathHelper.DegreesToRadians(directionModifier);
+ for (int i = 1; i <= numberbullets; i++)
+ {
+ bulletAddRad(1, direction, CharacterColor);
+ direction += directionModifier;
+ }
+ }
+ #endregion
+
+ public override void Death()
+ {
+ if (Bot && Clone)
+ {
+ parentPlayer.cloneList.Remove(this);
+ Expire();
+ }
+ else if (cloneList.Count > 0)
+ {
+ foreach(VitaruPlayer player in cloneList)
+ {
+ player.Bot = Bot;
+ player.Auto = Auto;
+ player.Clone = Clone;
+ player.Invert = Invert;
+ cloneList.Remove(player);
+ player.Actions = Actions;
+ player.cloneList = cloneList;
+
+ foreach (VitaruPlayer clone in player.cloneList)
+ clone.parentPlayer = player;
+
+ if (!Bot)
+ VitaruPlayfield.VitaruPlayer = player;
+
+ Expire();
+ break;
+ }
+ }
+ }
+
+ #region Player Input Stuff
+ ///
+ /// Moves the player based on player input
+ ///
+ private void playerInput()
+ {
+ //Handles Player Speed
+ float yTranslationDistance = player_speed * (float)Clock.ElapsedFrameTime * SpeedMultiplier;
+ float xTranslationDistance = player_speed * (float)Clock.ElapsedFrameTime * SpeedMultiplier;
+ Vector2 playerPosition = Position;
+
+ if (Auto)
+ {
+ Actions[VitaruAction.Up] = false;
+ Actions[VitaruAction.Down] = false;
+ Actions[VitaruAction.Left] = false;
+ Actions[VitaruAction.Right] = false;
+ Actions[VitaruAction.Slow] = false;
+ Actions[VitaruAction.Fast] = false;
+ Actions[VitaruAction.Shoot] = false;
+ VisibleHitbox.Alpha = 0;
+
+ bool bulletClose = false;
+ DrawableBullet closestBullet = null;
+ float closestBulletEdgeDitance = float.MaxValue;
+ float closestBulletAngle = 0;
+
+ VitaruPlayer closestPlayerLatterally = null;
+ float closestPlayerLatteralDistance = float.MaxValue;
+
+
+ //bool bulletBehind = false;
+ float behindBulletEdgeDitance = float.MaxValue;
+ float behindBulletAngle = 0;
+
+ foreach (Drawable draw in Parent)
+ if (draw is DrawableBullet)
+ {
+ DrawableBullet bullet = draw as DrawableBullet;
+ if (bullet.Bullet.Team != Team)
+ {
+ Vector2 pos = bullet.ToSpaceOfOtherDrawable(Vector2.Zero, this) + new Vector2(6);
+ float distance = (float)Math.Sqrt(Math.Pow(pos.X, 2) + Math.Pow(pos.Y, 2));
+ float edgeDistance = distance - (bullet.Width / 2 + Hitbox.Width / 2);
+ float angleToBullet = MathHelper.RadiansToDegrees((float)Math.Atan2((bullet.Position.Y - Position.Y), (bullet.Position.X - Position.X))) + 90 + Rotation;
+
+ if (closestBulletAngle < 360 - field_of_view | closestBulletAngle < -field_of_view && closestBulletAngle > field_of_view | closestBulletAngle > 360 + field_of_view)
+ if (closestBullet.Position.X > Position.X && bullet.Position.X < Position.X || closestBullet.Position.X < Position.X && bullet.Position.X > Position.X)
+ {
+ //bulletBehind = true;
+ behindBulletEdgeDitance = edgeDistance;
+ behindBulletAngle = angleToBullet;
+ }
+
+ if (edgeDistance < closestBulletEdgeDitance)
+ {
+ closestBulletEdgeDitance = edgeDistance;
+ closestBullet = bullet;
+ closestBulletAngle = angleToBullet;
+ }
+ }
+ }
+ //Lets go after enemy players if possible
+ else if (draw is VitaruPlayer)
+ {
+ VitaruPlayer player = draw as VitaruPlayer;
+ if (player.Team != Team)
+ {
+ float latteralDistance = Position.X - player.Position.X;
+
+ if (latteralDistance < 0)
+ latteralDistance *= -1;
+
+ if (latteralDistance < closestPlayerLatteralDistance)
+ {
+ closestPlayerLatterally = player;
+ closestPlayerLatteralDistance = latteralDistance;
+ }
+ }
+ }
+
+ if (closestBulletEdgeDitance <= 50)
+ {
+ bulletClose = true;
+ if (closestBulletEdgeDitance <= 30)
+ {
+ if (!Invert)
+ Actions[VitaruAction.Down] = true;
+ else
+ Actions[VitaruAction.Up] = true;
+
+ Actions[VitaruAction.Slow] = true;
+ }
+
+ if (closestBulletAngle > 360 - field_of_view | closestBulletAngle > -field_of_view && closestBulletAngle < field_of_view | closestBulletAngle < 360 + field_of_view)
+ {
+ if (closestBullet.X < Position.X)
+ Actions[VitaruAction.Right] = true;
+ else
+ Actions[VitaruAction.Left] = true;
+ }
+ }
+ else if (!bulletClose)
+ {
+ if (Position.X > 512 - 100)
+ Actions[VitaruAction.Left] = true;
+ else if (Position.X < 100)
+ Actions[VitaruAction.Right] = true;
+ else if (closestPlayerLatterally != null)
+ {
+ if (Position.X > closestPlayerLatterally.Position.X)
+ Actions[VitaruAction.Left] = true;
+ else
+ Actions[VitaruAction.Right] = true;
+ }
+
+ Actions[VitaruAction.Slow] = true;
+
+ if (Position.Y < 400 && !Invert || Position.Y < 300 && Invert)
+ Actions[VitaruAction.Down] = true;
+ else if (Position.Y > 500 && !Invert || Position.Y > 400 && Invert)
+ Actions[VitaruAction.Up] = true;
+ }
+
+ Actions[VitaruAction.Shoot] = true;
+
+ if (Actions[VitaruAction.Slow])
+ {
+ xTranslationDistance /= 2;
+ yTranslationDistance /= 2;
+ VisibleHitbox.Alpha = 1;
+ }
+ if (Actions[VitaruAction.Fast])
+ {
+ xTranslationDistance *= 2;
+ yTranslationDistance *= 2;
+ }
+
+ if (Actions[VitaruAction.Up])
+ playerPosition.Y -= yTranslationDistance;
+ if (Actions[VitaruAction.Left])
+ playerPosition.X -= xTranslationDistance;
+ if (Actions[VitaruAction.Down])
+ playerPosition.Y += yTranslationDistance;
+ if (Actions[VitaruAction.Right])
+ playerPosition.X += xTranslationDistance;
+
+ playerPosition = Vector2.ComponentMin(playerPosition, playerBounds.Yw);
+ playerPosition = Vector2.ComponentMax(playerPosition, playerBounds.Xz);
+ }
+ else
+ {
+ if (Actions[VitaruAction.Slow])
+ {
+ xTranslationDistance /= 2;
+ yTranslationDistance /= 2;
+ }
+ if (Actions[VitaruAction.Fast])
+ {
+ xTranslationDistance *= 2;
+ yTranslationDistance *= 2;
+ }
+
+ if (Actions[VitaruAction.Up])
+ playerPosition.Y -= yTranslationDistance;
+ if (Actions[VitaruAction.Left])
+ playerPosition.X -= xTranslationDistance;
+ if (Actions[VitaruAction.Down])
+ playerPosition.Y += yTranslationDistance;
+ if (Actions[VitaruAction.Right])
+ playerPosition.X += xTranslationDistance;
+
+ playerPosition = Vector2.ComponentMin(playerPosition, playerBounds.Yw);
+ playerPosition = Vector2.ComponentMax(playerPosition, playerBounds.Xz);
+ }
+ Position = playerPosition;
+ }
+
+ public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true;
+
+ public bool OnPressed(VitaruAction action)
+ {
+ if (!Bot && !Puppet)
+ {
+ //Keyboard Stuff
+ if (currentCharacter == Characters.AliceMuyart)
+ {
+ if (action == VitaruAction.Increase)
+ SetRate = Math.Min(SetRate + 0.1f, 1.5f);
+ if (action == VitaruAction.Decrease)
+ SetRate = Math.Max(SetRate - 0.1f, 0.1f);
+ }
+ else if (currentCharacter == Characters.SakuyaIzayoi)
+ {
+ if (action == VitaruAction.Increase)
+ SetRate = Math.Min(SetRate + 0.2f, 0.8f);
+ if (action == VitaruAction.Decrease)
+ SetRate = Math.Max(SetRate - 0.2f, 0.2f);
+ }
+
+ if (action == VitaruAction.Up)
+ Actions[VitaruAction.Up] = true;
+ if (action == VitaruAction.Down)
+ Actions[VitaruAction.Down] = true;
+ if (action == VitaruAction.Left)
+ Actions[VitaruAction.Left] = true;
+ if (action == VitaruAction.Right)
+ Actions[VitaruAction.Right] = true;
+ if (action == VitaruAction.Fast && currentCharacter != Characters.IbarakiKasen)
+ Actions[VitaruAction.Fast] = true;
+ if (action == VitaruAction.Slow)
+ {
+ if (currentSkin != GraphicsPresets.StandardCompetitive && currentSkin != GraphicsPresets.HighPerformanceCompetitive)
+ VisibleHitbox.Alpha = 1;
+
+ Actions[VitaruAction.Slow] = true;
+ }
+ if (action == VitaruAction.LeftShoot | action == VitaruAction.RightShoot | action == VitaruAction.Shoot | action == VitaruAction.Spell && currentCharacter == Characters.KokoroHatano)
+ {
+ kokoroSpell();
+
+ if (Time.Current <= lastQuarterBeat + hitwindow | Time.Current >= nextHalfBeat - hitwindow)
+ patternWave();
+ }
+
+ //Mouse Stuff
+ if (action == VitaruAction.Shoot && currentCharacter != Characters.KokoroHatano)
+ Actions[VitaruAction.Shoot] = true;
+
+ spell(false, action);
+ sendPacket();
+
+ return true;
+ }
+ return false;
+ }
+
+ public bool OnReleased(VitaruAction action)
+ {
+ if (!Bot && !Puppet)
+ {
+ //Keyboard Stuff
+ if (action == VitaruAction.Up)
+ Actions[VitaruAction.Up] = false;
+ if (action == VitaruAction.Down)
+ Actions[VitaruAction.Down] = false;
+ if (action == VitaruAction.Left)
+ Actions[VitaruAction.Left] = false;
+ if (action == VitaruAction.Right)
+ Actions[VitaruAction.Right] = false;
+ if (action == VitaruAction.Fast)
+ Actions[VitaruAction.Fast] = false;
+ if (action == VitaruAction.Slow)
+ {
+ if (currentSkin != GraphicsPresets.StandardCompetitive && currentSkin != GraphicsPresets.HighPerformanceCompetitive)
+ VisibleHitbox.Alpha = 0;
+
+ Actions[VitaruAction.Slow] = false;
+ }
+
+ //Mouse Stuff
+ if (action == VitaruAction.Shoot)
+ Actions[VitaruAction.Shoot] = false;
+ spell(true, action);
+ sendPacket();
+
+ return true;
+ }
+ return false;
+ }
+ #endregion
+
+ #region Networking
+ private void sendPacket()
+ {
+ if (VitaruNetworkingClientHandler != null && !Puppet)
+ {
+ VitaruPlayerInformation playerInformation = new VitaruPlayerInformation
+ {
+ Character = currentCharacter,
+ PlayerX = Position.X,
+ PlayerY = Position.Y,
+ PlayerID = PlayerID,
+ Actions = Actions,
+ ClockSpeed = currentRate
+ };
+
+ ClientInfo clientInfo = new ClientInfo
+ {
+ IP = VitaruNetworkingClientHandler.ClientInfo.IP,
+ Port = VitaruNetworkingClientHandler.ClientInfo.Port
+ };
+
+ VitaruInMatchPacket packet = new VitaruInMatchPacket(clientInfo) { PlayerInformation = playerInformation };
+
+ VitaruNetworkingClientHandler.SendToHost(packet);
+ VitaruNetworkingClientHandler.SendToInGameClients(packet);
+ }
+ }
+
+ private void packetReceived(Packet p)
+ {
+ if (p is VitaruInMatchPacket packet)
+ {
+ if (packet.PlayerInformation.Character == Characters.SakuyaIzayoi | packet.PlayerInformation.Character == Characters.AliceMuyart)
+ applyToClock(workingBeatmap.Value.Track, packet.PlayerInformation.ClockSpeed);
+
+ if (packet.PlayerInformation.PlayerID == PlayerID && Puppet)
+ {
+ Actions = packet.PlayerInformation.Actions;
+ Position = new Vector2(packet.PlayerInformation.PlayerX, packet.PlayerInformation.PlayerY);
+ }
+
+ VitaruNetworkingClientHandler.ShareWithOtherPeers(packet);
+ }
+ }
+ #endregion
+
+ #region Touhosu Story
+ private double startSpeaking = double.MaxValue;
+ private double lengthOfSpeaking;
+
+ private readonly Bindable familiar = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.Familiar);
+ private int familiarity;
+
+ private readonly Bindable lastDance = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.LastDance);
+ private int dance;
+
+ private readonly Bindable insane = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.Insane);
+ private int insanity;
+
+ private readonly Bindable awoken = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.Awoken);
+ private int awakening;
+
+ private readonly Bindable sacred = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.Sacred);
+ private int tresspassing;
+
+ private readonly Bindable resurrected = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.Resurrected);
+ private int resurrection;
+
+ public void Speak(string text)
+ {
+ textContainer.FadeTo(0.5f, 200);
+ textContainer.Text = text;
+ startSpeaking = Time.Current;
+ lengthOfSpeaking = 0;
+
+ int y = 150;
+ foreach (char i in text)
+ {
+ lengthOfSpeaking += y;
+ y++;
+ }
+ }
+
+ private void speakingUpdate()
+ {
+ if (Time.Current > startSpeaking + lengthOfSpeaking)
+ textContainer.FadeOut(200);
+
+ if (workingBeatmap.Value.BeatmapInfo.OnlineBeatmapID == 1371893 && currentCharacter == Characters.ReimuHakurei && !familiar)
+ {
+ if (Time.Current >= 5200 && familiarity == 0)
+ {
+ Speak("This place. . .");
+ familiarity++;
+ }
+ if (Time.Current >= 59100 && familiarity == 1)
+ {
+ Speak("It seems familiar. . .");
+ familiarity++;
+ }
+ if (Time.Current >= 93920 && familiarity == 2)
+ {
+ Speak("Yes, this is where I got into my first fight!");
+ familiarity++;
+ }
+ if (Time.Current >= 149572 && familiarity == 3)
+ {
+ Speak("Fairies were mad for seemingly no reason,");
+ familiarity++;
+ }
+ if (Time.Current >= 177398 && familiarity == 4)
+ {
+ Speak("I had found out later Marisa had trespassed without even knowing,");
+ familiarity++;
+ }
+ if (Time.Current >= 205224 && familiarity == 5)
+ {
+ Speak("Thankfully she came to help, we fought hard,");
+ familiarity++;
+ }
+ if (Time.Current >= 233050 && familiarity == 6)
+ {
+ Speak("Then the Scarlet sisters came,");
+ familiarity++;
+ }
+ if (Time.Current >= 246963 && familiarity == 7)
+ {
+ Speak("I tried to resolve this, but too much blood had been shed,");
+ familiarity++;
+ }
+ if (Time.Current >= 274789 && familiarity == 8)
+ {
+ Speak("We fled, planning to meet at their mansion later that week,");
+ familiarity++;
+ }
+ if (Time.Current >= 302615 && familiarity == 9)
+ {
+ Speak("That was a mistake.");
+ familiarity++;
+ familiar.Value = true;
+ }
+ }
+
+ if (false)//workingBeatmap.Value.BeatmapInfo.OnlineBeatmapID == 1548917 && currentCharacter == Characters.KokoroHatano && !lastDance)
+ {
+ if (Time.Current >= 1430 && dance == 0)
+ {
+ Speak("This is it,");
+ dance++;
+ }
+ if (Time.Current >= 23760 && dance == 1)
+ {
+ Speak("My final act,");
+ dance++;
+ }
+ if (Time.Current >= 43300 && dance == 2)
+ {
+ Speak("My Last Dance.");
+ dance++;
+ lastDance.Value = true;
+ }
+ }
+
+ if (false)//workingBeatmap.Value.BeatmapInfo.OnlineBeatmapID == 114716 && currentCharacter == Characters.FlandreScarlet && insane)
+ {
+ if (Time.Current >= 760 && insanity == 0)
+ {
+ Speak("That piano. . .");
+ insanity++;
+ }
+ if (Time.Current >= 12340 && insanity == 1)
+ {
+ Speak("It is driving me insane!");
+ insanity++;
+ }
+ if (Time.Current >= 28600 && insanity == 2)
+ {
+ Speak("Missy please, I am trying to think.");
+ insanity++;
+ insane.Value = false;
+ }
+ }
+
+ if (false)//workingBeatmap.Value.BeatmapInfo.OnlineBeatmapID == 114716 && currentCharacter == Characters.FlandreScarlet && !insane)
+ {
+ if (Time.Current >= 760 && insanity == 0)
+ {
+ Speak("That piano really needs to stop. . .");
+ insanity++;
+ }
+ }
+ /*
+ if (workingBeatmap.Value.BeatmapInfo.OnlineBeatmapID == 114716 && currentCharacter == Characters.RemiliaScarlet && !insane)
+ {
+ if (Time.Current >= 760 && awakening == 0)
+ {
+ Speak("Flandre, what happened to you?");
+ awakening++;
+ }
+ if (Time.Current >= 12340 && awakening == 1)
+ {
+ Speak("Flan, are you there?");
+ awakening++;
+ }
+ if (Time.Current >= 28600 && awakening == 2)
+ {
+ Speak("Its me, your sister Remilia,");
+ awakening++;
+ }
+ if (Time.Current >= 0 && awakening == 3)
+ {
+ Speak("Flan? I know you can hear me.");
+ awakening++;
+ }
+ if (Time.Current >= 0 && awakening == 4)
+ {
+ Speak("Please? I need to talk.");
+ awakening++;
+ }
+ if (Time.Current >= 0 && awakening == 5)
+ {
+ Speak("I know you're upset,");
+ awakening++;
+ }
+ if (Time.Current >= 0 && awakening == 6)
+ {
+ Speak("But I need my sister back.");
+ awakening++;
+ }
+ if (Time.Current >= 0 && awakening == 7)
+ {
+ Speak("What would Hong say if she knew you were this lazy?");
+ awakening++;
+ }
+ if (Time.Current >= 0 && awakening == 8)
+ {
+ //Flandre.Speak("\"Get off your ass\"?");
+ awakening++;
+ }
+ }
+ */
+
+ if (false)//workingBeatmap.Value.BeatmapInfo.OnlineBeatmapID == 148000 && currentCharacter == Characters.Kaguya)
+ {
+ if (Time.Current >= 1280 && tresspassing == 0)
+ {
+ Speak("What a lovely night it is for a walk.");
+ tresspassing++;
+ }
+ if (Time.Current >= 20860 && tresspassing == 1)
+ {
+ Speak("Oh?");
+ tresspassing++;
+ }
+ if (Time.Current >= 22120 && tresspassing == 2)
+ {
+ Speak("Someone has been here already. . .");
+ tresspassing++;
+ }
+ if (Time.Current >= 37280 && tresspassing == 3)
+ {
+ Speak("Thats them over there.");
+ tresspassing++;
+ }
+ if (Time.Current >= 41060 && tresspassing == 4)
+ {
+ Speak("Whaaa-");
+ tresspassing++;
+ }
+ if (Time.Current >= 82740 && tresspassing == 5)
+ {
+ Speak("Why are we fighting? What did I do to you?");
+ tresspassing++;
+ }
+ }
+ }
+ #endregion
+ }
+
+ public enum Characters
+ {
+ //Alex,
+ [Description("Reimu Hakurei")]
+ ReimuHakurei = 1,
+ [Description("Marisa Kirisame")]
+ MarisaKirisame,
+ [Description("Sakuya Izayoi")]
+ SakuyaIzayoi,
+ [Description("Hong Meiling")]
+ HongMeiling,
+ [Description("Flandre Scarlet")]
+ FlandreScarlet,
+ [Description("Remilia Scarlet")]
+ RemiliaScarlet,
+ [Description("Cirno")]
+ Cirno,
+ [Description("Tenshi Hinanai")]
+ TenshiHinanai,
+ [Description("Yuyuko Saigyouji")]
+ YuyukoSaigyouji,
+ [Description("Yukari Yakumo")]
+ YukariYakumo,
+ [Description("Ran Yakumo")]
+ RanYakumo,
+ [Description("Chen")]
+ Chen,
+ [Description("Alice Margatroid")]
+ AliceMargatroid,
+ [Description("Komachi Onozuka")]
+ KomachiOnozuka,
+ [Description("Byakuren Hijiri")]
+ ByakurenHijiri,
+ [Description("Rumia")]
+ Rumia,
+ [Description("Sikieiki Yamaxanadu")]
+ SikieikiYamaxanadu,
+ [Description("Suwako Moriya")]
+ SuwakoMoriya,
+ [Description("Youmu Konpaku")]
+ YoumuKonpaku,
+ [Description("Kokoro Hatano")]
+ KokoroHatano,
+ [Description("Kaguya")]
+ Kaguya,
+ [Description("Ibaraki Kasen")]
+ IbarakiKasen,
+ [Description("Nue Houjuu")]
+ NueHoujuu,
+ //[Description("Meme")]
+ //Taikonator,
+ [Description("Alice Muyart")]
+ AliceMuyart,
+ [Description("Arysa Muyart")]
+ ArysaMuyart
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableBullet.cs b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableBullet.cs
new file mode 100644
index 0000000000..0bcd38a78a
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableBullet.cs
@@ -0,0 +1,276 @@
+using osu.Framework.Graphics;
+using OpenTK;
+using System;
+using osu.Game.Rulesets.Vitaru.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Vitaru.Judgements;
+using osu.Game.Rulesets.Vitaru.Settings;
+using osu.Game.Rulesets.Vitaru.Scoring;
+using osu.Game.Rulesets.Vitaru.UI;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Scoring;
+using Symcol.Core.GameObjects;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Drawables
+{
+ public class DrawableBullet : DrawableVitaruHitObject
+ {
+ public static int BulletCount;
+
+ private readonly ScoringMetric currentScoringMetric = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.ScoringMetric);
+ private readonly VitaruGamemode currentGameMode = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GameMode);
+
+ //Used like a multiple (useful for spells in multiplayer)
+ public static float BulletSpeedModifier = 1;
+
+ //Playfield size + Margin of 10 on each side
+ public Vector4 BulletBounds = new Vector4(-10, -10, 520, 830);
+
+ //Result of bulletSpeed + bulletAngle math, should never be modified outside of this class
+ public Vector2 BulletVelocity;
+
+ //Set to "true" when a judgement should be returned
+ private bool returnJudgement;
+
+ public bool ReturnGreat = false;
+
+ //Can be set for the Graze ScoringMetric
+ public int ScoreZone;
+
+ //Should be set to true when a character is hit
+ public bool Hit;
+
+ //Incase we want to be deleted in the near future
+ public double BulletDeleteTime = -1;
+
+ private readonly DrawablePattern drawablePattern;
+ public readonly Bullet Bullet;
+
+ public Action OnHit;
+
+ public SymcolHitbox Hitbox;
+
+ private BulletPiece bulletPiece;
+
+ private bool started;
+ private bool loaded;
+
+ public DrawableBullet(Container parent, Bullet bullet, DrawablePattern drawablePattern) : base(bullet, parent)
+ {
+ AlwaysPresent = true;
+ Alpha = 0;
+
+ Anchor = Anchor.TopLeft;
+ Origin = Anchor.Centre;
+
+ BulletCount++;
+
+ Bullet = bullet;
+ this.drawablePattern = drawablePattern;
+
+ if (currentGameMode == VitaruGamemode.Dodge)
+ BulletBounds = new Vector4(-10, -10, 522, 394);
+ }
+
+ public DrawableBullet(Container parent, Bullet bullet) : base(bullet, parent)
+ {
+ AlwaysPresent = true;
+ Alpha = 0;
+
+ Anchor = Anchor.TopLeft;
+ Origin = Anchor.Centre;
+
+ BulletCount++;
+
+ Bullet = bullet;
+
+ if (currentGameMode == VitaruGamemode.Dodge)
+ BulletBounds = new Vector4(-10, -10, 522, 394);
+ }
+
+ ///
+ /// Called 1 second before the bullet's starttime
+ ///
+ private void load()
+ {
+ if (!loaded)
+ {
+ loaded = true;
+
+ Size = new Vector2(Bullet.BulletDiameter);
+ Scale = new Vector2(0.1f);
+
+ Children = new Drawable[]
+ {
+ bulletPiece = new BulletPiece(this),
+ Hitbox = new SymcolHitbox(new Vector2(Bullet.BulletDiameter), Shape.Circle)
+ {
+ Team = Bullet.Team,
+ HitDetection = false
+ }
+ };
+ }
+ }
+
+ ///
+ /// Called to unload the bullet for storage
+ ///
+ private void unload()
+ {
+ if (loaded)
+ {
+ loaded = false;
+ started = false;
+ returnJudgement = false;
+ BulletDeleteTime = -1;
+ Alpha = 0;
+
+ Remove(bulletPiece);
+ bulletPiece.Dispose();
+ Remove(Hitbox);
+ Hitbox.Dispose();
+ ParentContainer.Remove(this);
+ Dispose();
+ }
+ }
+
+ ///
+ /// Called once when the bullet starts
+ ///
+ private void start()
+ {
+ if (!started)
+ {
+ Position = Bullet.Position;
+ Hitbox.HitDetection = true;
+ started = true;
+ this.FadeInFromZero(100);
+ this.ScaleTo(Vector2.One, 100);
+ BulletVelocity = getBulletVelocity();
+ }
+ }
+
+ protected override void CheckForJudgements(bool userTriggered, double timeOffset)
+ {
+ base.CheckForJudgements(userTriggered, timeOffset);
+
+ if (returnJudgement)
+ {
+ if (currentScoringMetric == ScoringMetric.ScoreZones)
+ {
+ switch (VitaruPlayfield.VitaruPlayer.ScoreZone)
+ {
+ case 0:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Miss });
+ break;
+ case 100:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Ok });
+ break;
+ case 200:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Good });
+ break;
+ case 300:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ }
+ }
+ else if (currentScoringMetric == ScoringMetric.InverseCatch)
+ {
+ switch (VitaruPlayfield.VitaruPlayer.ScoreZone)
+ {
+ case 0:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Miss });
+ break;
+ case 100:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ case 200:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ case 300:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ }
+ }
+ else if (currentScoringMetric == ScoringMetric.Graze)
+ {
+ switch (ScoreZone)
+ {
+ case 0:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Miss });
+ break;
+ case 50:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Meh });
+ break;
+ case 100:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Ok });
+ break;
+ case 200:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Good });
+ break;
+ case 300:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ }
+ }
+ }
+
+ else if (Hit)
+ {
+ if (!Bullet.DummyMode)
+ AddJudgement(new VitaruJudgement { Result = HitResult.Miss });
+ unload();
+ }
+
+ else if (ReturnGreat)
+ {
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ unload();
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ if(isDisposing)
+ BulletCount--;
+ }
+
+ private Vector2 getBulletVelocity()
+ {
+ Vector2 velocity = new Vector2(Bullet.BulletSpeed * (float)Math.Cos(Bullet.BulletAngleRadian), Bullet.BulletSpeed * (float)Math.Sin(Bullet.BulletAngleRadian));
+ return velocity;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (OnHit != null && Hit)
+ {
+ OnHit();
+ OnHit = null;
+ }
+
+ if (Position.Y >= BulletBounds.Y | Position.X >= BulletBounds.X | Position.Y <= BulletBounds.W | Position.X <= BulletBounds.Z && Time.Current >= Bullet.StartTime | Bullet.DummyMode || !Bullet.ObeyBoundries && Time.Current >= Bullet.StartTime | Bullet.DummyMode)
+ load();
+
+ if (BulletDeleteTime <= Time.Current && BulletDeleteTime != -1 || Time.Current < Bullet.StartTime && !Bullet.DummyMode)
+ unload();
+
+ if (Time.Current >= Bullet.StartTime)
+ {
+ start();
+
+ float frameTime = (float)Clock.ElapsedFrameTime;
+ this.MoveToOffset(new Vector2(BulletVelocity.X * BulletSpeedModifier * frameTime, BulletVelocity.Y * BulletSpeedModifier * frameTime));
+
+ if (Bullet.ObeyBoundries && Position.Y < BulletBounds.Y | Position.X < BulletBounds.X | Position.Y > BulletBounds.W | Position.X > BulletBounds.Z && !returnJudgement)
+ {
+ returnJudgement = true;
+ BulletDeleteTime = Time.Current + TIME_FADEOUT / 12;
+ this.FadeOutFromOne(TIME_FADEOUT / 12);
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableLaser.cs b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableLaser.cs
new file mode 100644
index 0000000000..e7fb653785
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableLaser.cs
@@ -0,0 +1,244 @@
+using OpenTK;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Vitaru.Judgements;
+using osu.Game.Rulesets.Vitaru.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Vitaru.Scoring;
+using osu.Game.Rulesets.Vitaru.Settings;
+using osu.Game.Rulesets.Vitaru.UI;
+using Symcol.Core.GameObjects;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Drawables
+{
+ public class DrawableLaser : DrawableVitaruHitObject
+ {
+ private readonly ScoringMetric currentScoringMetric = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.ScoringMetric);
+ private VitaruGamemode currentGameMode = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GameMode);
+
+ //Set to "true" when a judgement should be returned
+ private bool returnJudgement;
+
+ private bool returnedJudgement;
+
+ public bool ReturnGreat = false;
+
+ //Can be set for the Graze ScoringMetric
+ public int ScoreZone;
+
+ //Should be set to true when a character is hit
+ public bool Hit;
+
+ //Incase we want to be deleted in the near future
+ public double LaserDeleteTime = -1;
+
+ public SymcolHitbox Hitbox;
+ private LaserPiece laserPiece;
+
+ private readonly DrawablePattern drawablePattern;
+ public readonly Laser Laser;
+
+ private const float fade_in_time = 200;
+ private const float fade_out_time = 200;
+
+ private bool started;
+ private bool loaded;
+
+ public DrawableLaser(Container parent, Laser laser, DrawablePattern drawablePattern) : base(laser, parent)
+ {
+ AlwaysPresent = true;
+ Alpha = 0;
+
+ Anchor = Anchor.TopLeft;
+ Origin = Anchor.BottomCentre;
+
+ Laser = laser;
+ this.drawablePattern = drawablePattern;
+
+ Size = new Vector2(Laser.LaserSize.X / 2, Laser.LaserSize.Y / 8);
+ Rotation = MathHelper.RadiansToDegrees(Laser.LaserAngleRadian);
+ }
+
+ public DrawableLaser(Container parent, Laser laser) : base(laser, parent)
+ {
+ AlwaysPresent = true;
+ Alpha = 0;
+
+ Anchor = Anchor.TopLeft;
+ Origin = Anchor.BottomCentre;
+
+ Laser = laser;
+
+ Size = new Vector2(Laser.LaserSize.X / 2, Laser.LaserSize.Y / 8);
+ Rotation = MathHelper.RadiansToDegrees(Laser.LaserAngleRadian);
+ }
+
+ ///
+ /// Called 1 second before the bullet's starttime
+ ///
+ private void load()
+ {
+ if (!loaded)
+ {
+ loaded = true;
+
+ Children = new Drawable[]
+ {
+ laserPiece = new LaserPiece(this),
+ Hitbox = new SymcolHitbox(new Vector2(Laser.LaserSize.X / 2, Laser.LaserSize.Y / 8), Shape.Rectangle)
+ {
+ Team = Laser.Team,
+ HitDetection = false
+ }
+ };
+ }
+ }
+
+ ///
+ /// Called to unload the bullet for storage
+ ///
+ private void unload()
+ {
+ if (loaded)
+ {
+ loaded = false;
+ started = false;
+ returnJudgement = false;
+ LaserDeleteTime = -1;
+ Alpha = 0;
+
+ Remove(laserPiece);
+ laserPiece.Dispose();
+ Remove(Hitbox);
+ Hitbox.Dispose();
+ ParentContainer.Remove(this);
+ Dispose();
+ }
+ }
+
+ ///
+ /// Called once when the bullet starts
+ ///
+ private void start()
+ {
+ if (!started)
+ {
+ Hitbox.HitDetection = true;
+ started = true;
+ this.FadeInFromZero(fade_in_time);
+ this.ResizeTo(Laser.LaserSize, fade_in_time);
+ laserPiece.ResizeTo(Laser.LaserSize, fade_in_time);
+ Hitbox.ResizeTo(Laser.LaserSize, fade_in_time);
+ }
+ }
+
+ public void End()
+ {
+ if (started)
+ {
+ started = false;
+ this.FadeOutFromOne(fade_out_time);
+ this.ResizeTo(new Vector2(Laser.LaserSize.X / 2, Laser.LaserSize.Y), fade_out_time);
+ laserPiece.ResizeTo(new Vector2(Laser.LaserSize.X / 2, Laser.LaserSize.Y), fade_out_time);
+ Hitbox.ResizeTo(new Vector2(Laser.LaserSize.X / 2, Laser.LaserSize.Y), fade_out_time);
+ LaserDeleteTime = Time.Current + fade_out_time;
+ }
+ }
+
+ protected override void CheckForJudgements(bool userTriggered, double timeOffset)
+ {
+ base.CheckForJudgements(userTriggered, timeOffset);
+
+ if (returnJudgement)
+ {
+ if (currentScoringMetric == ScoringMetric.ScoreZones)
+ {
+ switch (VitaruPlayfield.VitaruPlayer.ScoreZone)
+ {
+ case 0:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Miss });
+ break;
+ case 100:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Ok });
+ break;
+ case 200:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Good });
+ break;
+ case 300:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ }
+ }
+ else if (currentScoringMetric == ScoringMetric.InverseCatch)
+ {
+ switch (VitaruPlayfield.VitaruPlayer.ScoreZone)
+ {
+ case 0:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Miss });
+ break;
+ case 100:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ case 200:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ case 300:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ }
+ }
+ else if (currentScoringMetric == ScoringMetric.Graze)
+ {
+ switch (ScoreZone)
+ {
+ case 0:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Miss });
+ break;
+ case 50:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Meh });
+ break;
+ case 100:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Ok });
+ break;
+ case 200:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Good });
+ break;
+ case 300:
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ break;
+ }
+ }
+ }
+
+ else if (Hit && !returnedJudgement)
+ {
+ if (!Laser.DummyMode)
+ AddJudgement(new VitaruJudgement { Result = HitResult.Miss });
+ returnedJudgement = true;
+ }
+
+ else if (ReturnGreat)
+ {
+ AddJudgement(new VitaruJudgement { Result = HitResult.Great });
+ unload();
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (Time.Current >= Laser.StartTime | Laser.DummyMode)
+ load();
+
+ if (LaserDeleteTime <= Time.Current && LaserDeleteTime != -1 || Time.Current < Laser.StartTime && !Laser.DummyMode)
+ unload();
+
+ if (Time.Current >= Laser.StartTime && Time.Current < Laser.EndTime)
+ start();
+
+ if (Time.Current >= Laser.EndTime)
+ End();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawablePatterns.cs b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawablePatterns.cs
new file mode 100644
index 0000000000..978d6617cc
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawablePatterns.cs
@@ -0,0 +1,359 @@
+using OpenTK;
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Vitaru.UI;
+using osu.Game.Rulesets.Vitaru.Objects.Characters;
+using System;
+using osu.Game.Rulesets.Vitaru.Settings;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Extensions.Color4Extensions;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Drawables
+{
+ public class DrawablePattern : DrawableVitaruHitObject
+ {
+ private readonly VitaruGamemode currentGameMode = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GameMode);
+
+ public static int PatternCount;
+ private readonly Pattern pattern;
+ private Vector2 patternStartPosition;
+ private Container energyCircle;
+
+ private bool loaded;
+ private bool started;
+ private bool done;
+
+ private int currentRepeat;
+
+ private Enemy enemy;
+
+ private bool prepedToPop;
+ private bool popped;
+
+ public DrawablePattern(Container parent, Pattern pattern) : base(pattern, parent)
+ {
+ AlwaysPresent = true;
+
+ this.pattern = pattern;
+
+ if (!pattern.IsSlider && !pattern.IsSpinner)
+ this.pattern.EndTime = this.pattern.StartTime + TIME_FADEOUT;
+ else if (pattern.IsSlider)
+ this.pattern.EndTime += TIME_FADEOUT;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ PatternCount++;
+
+ if (pattern.IsSlider)
+ pattern.EndTime = pattern.StartTime + pattern.RepeatCount * pattern.Curve.Distance / pattern.Velocity;
+
+ LifetimeStart = pattern.StartTime - (TIME_PREEMPT + 1000f);
+ }
+
+ //Should be called when a DrawablePattern is getting ready to become visable as to save on resources before hand
+ private void load()
+ {
+ if (!loaded)
+ {
+ if (currentGameMode != VitaruGamemode.Dodge)
+ {
+ //load the enemy
+ ParentContainer.Add(enemy = new Enemy(ParentContainer, pattern, this)
+ {
+ Alpha = 0,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Depth = 5,
+ MaxHealth = pattern.EnemyHealth,
+ Team = 1
+ });
+
+ Child = energyCircle = new Container
+ {
+ Alpha = 0,
+ Masking = true,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(30),
+ CornerRadius = 30f / 2,
+ BorderThickness = 10,
+ BorderColour = pattern.ComboColour,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = pattern.ComboColour.Opacity(0.5f),
+ Radius = Width / 2,
+ }
+ };
+ enemy.FadeInFromZero(TIME_FADEIN);
+ enemy.Position = getPatternStartPosition();
+ enemy.MoveTo(pattern.Position, TIME_PREEMPT);
+ }
+ else
+ {
+ Child = energyCircle = new CircularContainer
+ {
+ Masking = true,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Size = new Vector2(20),
+ BorderThickness = 6,
+ BorderColour = pattern.ComboColour,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ },
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Colour = pattern.ComboColour.Opacity(0.5f),
+ Radius = Width / 2,
+ }
+ };
+ }
+
+ Position = getPatternStartPosition();
+ this.MoveTo(pattern.Position, TIME_PREEMPT);
+
+ if (NestedHitObjects != null)
+ foreach (var o in NestedHitObjects)
+ {
+ var b = (DrawableBullet)o;
+ ParentContainer.Remove(b);
+ b.Dispose();
+ }
+
+ //Load the bullets
+ foreach (var o in pattern.NestedHitObjects)
+ {
+ var b = (Bullet)o;
+ b.ComboColour = pattern.ComboColour;
+ DrawableBullet drawableBullet = new DrawableBullet(ParentContainer, b, this);
+ ParentContainer.Add(drawableBullet);
+ AddNested(drawableBullet);
+ }
+
+ loaded = true;
+ }
+ }
+
+ private void unload()
+ {
+ if (loaded)
+ {
+ if (currentGameMode != VitaruGamemode.Dodge)
+ {
+ ParentContainer.Remove(enemy);
+ enemy.Dispose();
+ }
+
+ loaded = false;
+ started = false;
+ done = false;
+ }
+ }
+
+ private Vector2 getPatternStartPosition()
+ {
+ if (pattern.Position.X <= 384f / 2 && pattern.Position.Y <= 512f / 2)
+ patternStartPosition = pattern.Position - new Vector2(384f / 2, 512f / 2);
+ else if (pattern.Position.X > 384f / 2 && pattern.Position.Y <= 512f / 2)
+ patternStartPosition = new Vector2(pattern.Position.X + 384f / 2, pattern.Position.Y - 512f / 2);
+ else if (pattern.Position.X > 384f / 2 && pattern.Position.Y > 512f / 2)
+ patternStartPosition = pattern.Position + new Vector2(384f / 2, 512f / 2);
+ else
+ patternStartPosition = new Vector2(pattern.Position.X - 384f / 2, pattern.Position.Y + 512f / 2);
+
+ return patternStartPosition;
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ //Used just to keep this Update(); function clean looking
+ generalUpdateLogic();
+
+ if (!pattern.IsSlider && !pattern.IsSpinner && loaded)
+ hitcircleUpdate();
+
+ if (pattern.IsSlider && loaded)
+ sliderUpdate();
+
+ if (pattern.IsSpinner && loaded)
+ spinnerUpdate();
+ }
+
+ private void generalUpdateLogic()
+ {
+ if (HitObject.StartTime - TIME_PREEMPT <= Time.Current && Time.Current < pattern.EndTime + TIME_FADEOUT)
+ load();
+
+ else
+ unload();
+
+ if (currentGameMode != VitaruGamemode.Dodge && prepedToPop && HitObject.StartTime <= Time.Current)
+ pop();
+ }
+
+ ///
+ /// Will leave and hide
+ ///
+ private void end()
+ {
+ if (energyCircle.Alpha <= 0)
+ {
+ if (currentGameMode != VitaruGamemode.Dodge)
+ enemy.MoveTo(patternStartPosition, TIME_FADEOUT, Easing.InQuint);
+ this.MoveTo(patternStartPosition, TIME_FADEOUT, Easing.InQuint);
+ enemy.ScaleTo(new Vector2(0.5f), TIME_FADEOUT, Easing.InQuint);
+ enemy.FadeOut(TIME_FADEOUT, Easing.InQuint);
+ }
+ else
+ {
+ energyCircle.FadeOut(TIME_FADEOUT / 4);
+ energyCircle.ScaleTo(new Vector2(0.1f), TIME_FADEOUT / 4);
+ }
+ }
+
+ public void PrepPop()
+ {
+ if (!prepedToPop && !done)
+ {
+ double time = pattern.StartTime - Time.Current;
+
+ if (time < 0)
+ time = 0;
+
+ energyCircle.FadeInFromZero(time);
+ energyCircle.ScaleTo(Vector2.One, time);
+ prepedToPop = true;
+ }
+ }
+
+ private void pop()
+ {
+ if (!popped)
+ {
+ enemy.FadeOut(100);
+ enemy.ScaleTo(new Vector2(1.2f), 100);
+ popped = true;
+ }
+ }
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ if (isDisposing)
+ PatternCount--;
+ }
+
+ private void throwBullets()
+ {
+ PlaySamples();
+ foreach (var o in NestedHitObjects)
+ {
+ var b = (DrawableBullet)o;
+ if (b.Bullet.StartTime <= Time.Current)
+ {
+ b.Position = Position;
+ try
+ {
+ if (b.Bullet.ShootPlayer)
+ b.Bullet.BulletAngleRadian += pattern.PlayerRelativePositionAngle(VitaruPlayfield.VitaruPlayer.Position, b.Position) - (float)Math.PI / 2;
+ }
+ catch { b.Bullet.BulletAngleRadian = 0; }
+ }
+ }
+ }
+
+ ///
+ /// All the hitcircle stuff
+ ///
+ #region Hitcircle Stuff
+ private void hitcircleUpdate()
+ {
+ if (HitObject.StartTime <= Time.Current && !started)
+ {
+ started = true;
+ done = true;
+
+ throwBullets();
+ end();
+ }
+ }
+ #endregion
+
+ ///
+ /// All The Slider Stuff
+ ///
+ #region Slider Stuff
+ private void sliderUpdate()
+ {
+ double progress = MathHelper.Clamp((Time.Current - pattern.StartTime) / pattern.Duration, 0, 1);
+ int repeat = pattern.RepeatAt(progress);
+ progress = pattern.ProgressAt(progress);
+
+ if (HitObject.StartTime <= Time.Current && !started)
+ {
+ throwBullets();
+ started = true;
+ }
+
+ if (!done && started)
+ {
+ Position = pattern.Curve.PositionAt(progress);
+ if (currentGameMode != VitaruGamemode.Dodge)
+ enemy.Position = pattern.Curve.PositionAt(progress);
+ }
+
+ if (repeat > currentRepeat)
+ {
+ if (repeat < pattern.RepeatCount)
+ throwBullets();
+ currentRepeat = repeat;
+ }
+
+ if (pattern.EndTime <= Time.Current && started && !done)
+ {
+ end();
+ throwBullets();
+ done = true;
+ }
+ }
+ #endregion
+
+ ///
+ /// All the spinner stuff
+ ///
+ #region Spinner Stuff
+ private void spinnerUpdate()
+ {
+ if (pattern.StartTime <= Time.Current && !started)
+ {
+ throwBullets();
+ started = true;
+ }
+ if (pattern.EndTime <= Time.Current)
+ {
+ done = true;
+ end();
+ }
+ }
+ #endregion
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableSeekingBullet.cs b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableSeekingBullet.cs
new file mode 100644
index 0000000000..25379832cf
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableSeekingBullet.cs
@@ -0,0 +1,146 @@
+using OpenTK;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Rulesets.Vitaru.Objects.Characters;
+using osu.Game.Rulesets.Vitaru.Objects.Drawables.Pieces;
+using osu.Game.Rulesets.Vitaru.Settings;
+using Symcol.Core.GameObjects;
+using System;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Drawables
+{
+ public class DrawableSeekingBullet : DrawableVitaruHitObject
+ {
+ private readonly VitaruGamemode currentGameMode = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GameMode);
+
+ public VitaruCharacter NearestEnemy;
+
+ private double startTime;
+
+ public readonly SymcolHitbox Hitbox;
+
+ //Result of bulletSpeed + bulletAngle math, should never be modified outside of this class
+ private Vector2 bulletVelocity;
+
+ //Incase we want to be deleted in the near future
+ public double BulletDeleteTime = -1;
+
+ //Should be set to true when a character is hit
+ public bool Hit;
+
+ public readonly SeekingBullet SeekingBullet;
+
+ //Playfield size + Margin of 10 on each side
+ public Vector4 BulletBounds = new Vector4(-10, -10, 520, 830);
+
+ public DrawableSeekingBullet(Container parent, SeekingBullet seekingBullet) : base(seekingBullet, parent)
+ {
+ AlwaysPresent = true;
+ Alpha = 0;
+ Scale = new Vector2(0.1f);
+ Size = new Vector2(20);
+
+ Anchor = Anchor.TopLeft;
+ Origin = Anchor.Centre;
+
+ this.FadeInFromZero(100);
+ this.ScaleTo(Vector2.One, 100);
+
+ SeekingBullet = seekingBullet;
+
+ if (currentGameMode == VitaruGamemode.Dodge)
+ BulletBounds = new Vector4(-10, -10, 522, 394);
+
+ Children = new Drawable[]
+ {
+ new SeekingBulletPiece(this),
+ Hitbox = new SymcolHitbox(Size, Shape.Rectangle)
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ startTime = Time.Current;
+ }
+
+ private void nearestEnemy()
+ {
+ foreach (Drawable draw in ParentContainer.Children)
+ {
+ VitaruCharacter enemy = draw as VitaruCharacter;
+ if (enemy?.Hitbox != null && enemy.Hitbox.Team != SeekingBullet.Team)
+ {
+ if (enemy.Alpha > 0)
+ {
+ float minDist = float.MaxValue;
+ Vector2 pos = enemy.ToSpaceOfOtherDrawable(Vector2.Zero, this) + new Vector2(6);
+ float distance = (float)Math.Sqrt(Math.Pow(pos.X, 2) + Math.Pow(pos.Y, 2));
+ if (distance < minDist)
+ {
+ NearestEnemy = enemy;
+ minDist = distance;
+ }
+ }
+ }
+ }
+ }
+
+ public float enemyRelativePositionAngle()
+ {
+ //Returns a Radian
+ float enemyAngle = (float)Math.Atan2((NearestEnemy.Position.Y - Position.Y), (NearestEnemy.Position.X - Position.X));
+ return enemyAngle;
+ }
+
+ private Vector2 getBulletVelocity(float angle)
+ {
+ Vector2 velocity = new Vector2(SeekingBullet.BulletSpeed * (float)Math.Cos(angle), SeekingBullet.BulletSpeed * (float)Math.Sin(angle));
+ return velocity;
+ }
+
+ private void unload()
+ {
+ Alpha = 0;
+ Expire();
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (Hit)
+ unload();
+
+ Rotation = Rotation + 0.25f;
+
+ if (BulletDeleteTime <= Time.Current && BulletDeleteTime != -1)
+ unload();
+
+ if (SeekingBullet.ObeyBoundries && Position.Y < BulletBounds.Y | Position.X < BulletBounds.X | Position.Y > BulletBounds.W | Position.X > BulletBounds.Z && BulletDeleteTime == -1)
+ {
+ BulletDeleteTime = Time.Current + TIME_FADEOUT / 12;
+ this.FadeOutFromOne(TIME_FADEOUT / 12);
+ }
+
+ //IdleTimer
+ float frameTime = (float)Clock.ElapsedFrameTime;
+ bulletVelocity = getBulletVelocity(MathHelper.DegreesToRadians(SeekingBullet.StartAngle - 90));
+
+ if (startTime + 300 <= Time.Current)
+ {
+ nearestEnemy();
+ if (NearestEnemy != null && !NearestEnemy.Dead)
+ {
+ bulletVelocity = getBulletVelocity(enemyRelativePositionAngle());
+ this.MoveToOffset(new Vector2(bulletVelocity.X * DrawableBullet.BulletSpeedModifier * frameTime, bulletVelocity.Y * DrawableBullet.BulletSpeedModifier * frameTime));
+
+ }
+ else
+ this.MoveToOffset(new Vector2(bulletVelocity.X * DrawableBullet.BulletSpeedModifier * frameTime, bulletVelocity.Y * DrawableBullet.BulletSpeedModifier * frameTime));
+ }
+ else
+ this.MoveToOffset(new Vector2(bulletVelocity.X * DrawableBullet.BulletSpeedModifier * frameTime, bulletVelocity.Y * DrawableBullet.BulletSpeedModifier * frameTime));
+
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableVitaruHitObject.cs b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableVitaruHitObject.cs
new file mode 100644
index 0000000000..3bfd738e73
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableVitaruHitObject.cs
@@ -0,0 +1,38 @@
+using osu.Game.Rulesets.Objects.Drawables;
+using System.ComponentModel;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Drawables
+{
+ public class DrawableVitaruHitObject : DrawableHitObject
+ {
+ public static float TIME_PREEMPT = 600;
+ public static float TIME_FADEIN = 300;
+ public static float TIME_FADEOUT = 1200;
+
+ public readonly Framework.Graphics.Containers.Container ParentContainer;
+
+ public DrawableVitaruHitObject(VitaruHitObject hitObject, Framework.Graphics.Containers.Container parent) : base(hitObject)
+ {
+ ParentContainer = parent;
+
+ if (hitObject.Ar != -1)
+ {
+ TIME_PREEMPT = hitObject.Ar;
+ TIME_FADEOUT = hitObject.Ar * 2;
+ TIME_FADEIN = hitObject.Ar / 2;
+ }
+ }
+
+ protected sealed override void UpdateState(ArmedState state) { }
+ }
+
+ public enum ComboResult
+ {
+ [Description(@"")]
+ None,
+ [Description(@"Good")]
+ Good,
+ [Description(@"Amazing")]
+ Perfect
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableVitaruJudgement.cs b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableVitaruJudgement.cs
new file mode 100644
index 0000000000..678e20e6d5
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Drawables/DrawableVitaruJudgement.cs
@@ -0,0 +1,23 @@
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Vitaru.Judgements;
+using OpenTK;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Rulesets.Scoring;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Drawables
+{
+ public class DrawableVitaruJudgement : DrawableJudgement
+ {
+ public DrawableVitaruJudgement(VitaruJudgement judgement) : base(judgement)
+ {
+ }
+
+ protected override void LoadComplete()
+ {
+ if (Judgement.Result != HitResult.Miss)
+ JudgementText.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint);
+
+ base.LoadComplete();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Drawables/Pieces/BulletPiece.cs b/osu.Game.Rulesets.Vitaru/Objects/Drawables/Pieces/BulletPiece.cs
new file mode 100644
index 0000000000..afa34b60e6
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Drawables/Pieces/BulletPiece.cs
@@ -0,0 +1,113 @@
+using osu.Framework.Graphics;
+using OpenTK;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Containers;
+using osu.Framework.Audio.Track;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Graphics.Sprites;
+using osu.Framework.MathUtils;
+using osu.Game.Rulesets.Vitaru.Settings;
+using osu.Framework.Extensions.Color4Extensions;
+using OpenTK.Graphics;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Drawables.Pieces
+{
+ public class BulletPiece : BeatSyncedContainer
+ {
+ private readonly Characters.Characters currentCharacter = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.Characters);
+ private readonly GraphicsPresets currentSkin = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GraphicsPresets);
+
+ private Sprite bulletKiai;
+ private CircularContainer circle;
+ private Box box;
+
+ private readonly float randomRotationValue = 1;
+ private readonly bool randomRotateDirection;
+
+ private readonly DrawableBullet drawableBullet;
+
+ public BulletPiece(DrawableBullet drawableBullet)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ this.drawableBullet = drawableBullet;
+
+ randomRotationValue = (float)RNG.Next(10, 15) / 10;
+ randomRotateDirection = RNG.NextBool();
+ }
+
+ protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
+ {
+ base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
+
+ if (currentSkin != GraphicsPresets.HighPerformanceCompetitive && currentSkin != GraphicsPresets.HighPerformance)
+ {
+ if (effectPoint.KiaiMode && bulletKiai.Alpha == 0)
+ bulletKiai.FadeInFromZero(timingPoint.BeatLength / 4);
+ if (!effectPoint.KiaiMode && bulletKiai.Alpha == 1)
+ bulletKiai.FadeOutFromOne(timingPoint.BeatLength);
+ }
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (currentSkin != GraphicsPresets.HighPerformanceCompetitive && currentSkin != GraphicsPresets.HighPerformance && bulletKiai.Alpha > 0)
+ {
+ if (randomRotateDirection)
+ bulletKiai.RotateTo((float)(Clock.CurrentTime / 1000 * 90) * randomRotationValue);
+ else
+ bulletKiai.RotateTo((float)(Clock.CurrentTime / 1000 * 90) * -1 * randomRotationValue);
+ }
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ Size = new Vector2(drawableBullet.Bullet.BulletDiameter + 12);
+
+ if (currentSkin != GraphicsPresets.HighPerformanceCompetitive && currentSkin != GraphicsPresets.HighPerformance)
+ Child = bulletKiai = new Sprite
+ {
+ //Just to look nice for the time being, will fix the sprite later
+ Scale = new Vector2(2),
+ Alpha = 0,
+ Origin = Anchor.Centre,
+ Anchor = Anchor.Centre,
+ Colour = drawableBullet.Bullet.ComboColour,
+ Texture = VitaruRuleset.VitaruTextures.Get("bulletKiai"),
+ };
+
+ Add(circle = new CircularContainer
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Alpha = 1,
+ RelativeSizeAxes = Axes.Both,
+ BorderColour = drawableBullet.Bullet.ComboColour,
+ BorderThickness = 6,
+ Masking = true,
+
+ Child = box = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ }
+ });
+
+ if (currentSkin != GraphicsPresets.HighPerformanceCompetitive && currentSkin != GraphicsPresets.HighPerformance)
+ circle.EdgeEffect = new EdgeEffectParameters
+ {
+ Radius = drawableBullet.Bullet.BulletDiameter,
+ Type = EdgeEffectType.Shadow,
+ Colour = drawableBullet.Bullet.ComboColour.Opacity(0.2f)
+ };
+
+ if (drawableBullet.Bullet.Ghost && currentCharacter == Characters.Characters.Kaguya | currentCharacter == Characters.Characters.AliceMuyart)
+ box.Colour = Color4.Cyan;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Drawables/Pieces/LaserPiece.cs b/osu.Game.Rulesets.Vitaru/Objects/Drawables/Pieces/LaserPiece.cs
new file mode 100644
index 0000000000..ecb84f3162
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Drawables/Pieces/LaserPiece.cs
@@ -0,0 +1,30 @@
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.Containers;
+using osu.Game.Rulesets.Vitaru.Settings;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Drawables.Pieces
+{
+ public class LaserPiece : BeatSyncedContainer
+ {
+ private readonly GraphicsPresets currentSkin = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.GraphicsPresets);
+
+ public LaserPiece(DrawableLaser drawableLaser)
+ {
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+
+ Masking = true;
+
+ CornerRadius = 16;
+
+ BorderThickness = 8;
+ BorderColour = drawableLaser.Laser.ComboColour;
+
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Drawables/Pieces/SeekingBulletPiece.cs b/osu.Game.Rulesets.Vitaru/Objects/Drawables/Pieces/SeekingBulletPiece.cs
new file mode 100644
index 0000000000..83b1bd4ca3
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Drawables/Pieces/SeekingBulletPiece.cs
@@ -0,0 +1,41 @@
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Graphics.Containers;
+
+namespace osu.Game.Rulesets.Vitaru.Objects.Drawables.Pieces
+{
+ public class SeekingBulletPiece : BeatSyncedContainer
+ {
+ public SeekingBulletPiece(DrawableSeekingBullet seekingBullet)
+ {
+ Masking = true;
+ RelativeSizeAxes = Axes.Both;
+ Origin = Anchor.Centre;
+ Anchor = Anchor.Centre;
+ BorderThickness = 4;
+ AlwaysPresent = true;
+ BorderColour = seekingBullet.SeekingBullet.ComboColour;
+ CornerRadius = 4;
+
+ Child = new Box
+ {
+ RelativeSizeAxes = Axes.Both
+ };
+ EdgeEffect = new EdgeEffectParameters
+ {
+ Type = EdgeEffectType.Shadow,
+ Radius = 8,
+ Colour = seekingBullet.SeekingBullet.ComboColour.Opacity(0.25f),
+ };
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ this.RotateTo((float)(Clock.CurrentTime / 1000 * 90) * 2);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/HitObjectType.cs b/osu.Game.Rulesets.Vitaru/Objects/HitObjectType.cs
new file mode 100644
index 0000000000..a20e9c9b7c
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/HitObjectType.cs
@@ -0,0 +1,9 @@
+namespace osu.Game.Rulesets.Vitaru.Objects
+{
+ public enum HitObjectType
+ {
+ Pattern,
+ Bullet,
+ Laser
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Laser.cs b/osu.Game.Rulesets.Vitaru/Objects/Laser.cs
new file mode 100644
index 0000000000..22a50b8415
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Laser.cs
@@ -0,0 +1,20 @@
+using OpenTK;
+
+namespace osu.Game.Rulesets.Vitaru.Objects
+{
+ public class Laser : VitaruHitObject
+ {
+ public override HitObjectType Type => HitObjectType.Laser;
+
+ ///
+ /// Basically just bypasses all hitobject functionality (useful for player bullets)
+ ///
+ public bool DummyMode { get; set; }
+
+ public double EndTime { get; set; }
+ public float LaserDamage { get; set; } = 10;
+ public Vector2 LaserSize { get; set; } = new Vector2(2, 8);
+ public float LaserAngleRadian { get; set; }
+ public int Team { get; set; } = -1;
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/Pattern.cs b/osu.Game.Rulesets.Vitaru/Objects/Pattern.cs
new file mode 100644
index 0000000000..52e8ffbb17
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/Pattern.cs
@@ -0,0 +1,399 @@
+using OpenTK;
+using osu.Game.Audio;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Beatmaps;
+using System;
+
+namespace osu.Game.Rulesets.Vitaru.Objects
+{
+ public class Pattern : VitaruHitObject
+ {
+ public override HitObjectType Type => HitObjectType.Pattern;
+
+ ///
+ /// All Pattern specific stuff
+ ///
+ #region Pattern
+ public int PatternID { get; set; }
+ public float PatternSpeed { get; set; }
+ public float PatternDifficulty { get; set; } = 1;
+ private float patternAngleRadian { get; set; } = -10;
+ public float PatternAngleDegree { get; set; }
+ public float PatternBulletDiameter { get; set; } = 4;
+ public float PatternDamage { get; set; } = 10;
+ private bool dynamicPatternVelocity { get; } = false;
+ public int PatternTeam { get; set; }
+ private int totalBullets;
+ private bool shootPlayer;
+ #endregion
+
+ ///
+ /// All Slider specific stuff
+ ///
+ #region Slider
+ public bool IsSlider { get; set; } = false;
+ public List> RepeatSamples { get; set; } = new List>();
+ private const float base_scoring_distance = 100;
+ public double Duration => EndTime - StartTime;
+ public readonly SliderCurve Curve = new SliderCurve();
+ public int RepeatCount { get; set; } = 1;
+ public double Velocity;
+
+ public override Vector2 EndPosition => PositionAt(1);
+ public Vector2 PositionAt(double progress) => Curve.PositionAt(ProgressAt(progress));
+
+ public double ProgressAt(double progress)
+ {
+ double p = progress * RepeatCount % 1;
+ if (RepeatAt(progress) % 2 == 1)
+ p = 1 - p;
+ return p;
+ }
+
+ public int RepeatAt(double progress) => (int)(progress * RepeatCount);
+
+ public List ControlPoints
+ {
+ get { return Curve.ControlPoints; }
+ set { Curve.ControlPoints = value; }
+ }
+
+ public CurveType CurveType
+ {
+ get { return Curve.CurveType; }
+ set { Curve.CurveType = value; }
+ }
+
+ public double Distance
+ {
+ get { return Curve.Distance; }
+ set { Curve.Distance = value; }
+ }
+
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+
+ TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
+ DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
+
+ double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier;
+
+ Velocity = scoringDistance / timingPoint.BeatLength;
+ }
+ #endregion
+
+ ///
+ /// All Spinner specific stuff
+ ///
+ #region Spinner
+ public bool IsSpinner { get; set; }
+ public double EndTime { get; set; }
+ #endregion
+
+ #region Bullet Loading
+ public int GetTotalBullets()
+ {
+ switch (PatternID)
+ {
+ case 1:
+ totalBullets += (int)PatternDifficulty * 2 + 1;
+ break;
+ case 2:
+ totalBullets += (int)PatternDifficulty + 1;
+ break;
+ case 3:
+ totalBullets += (int)(PatternDifficulty + 2) / 2;
+ break;
+ case 4:
+ totalBullets += (int)(PatternDifficulty * 2) + 3;
+ break;
+ case 5:
+ totalBullets += (int)(30 * (PatternDifficulty / 3) * (Duration / 1000));
+ break;
+ }
+
+ return totalBullets;
+ }
+
+ public float EnemyHealth { get; set; } = 40;
+
+ protected override void CreateNestedHitObjects()
+ {
+ base.CreateNestedHitObjects();
+
+ createBullets();
+ }
+
+ private void createBullets()
+ {
+
+ var length = Curve.Distance;
+ var repeatPointDistance = Math.Min(Distance, length);
+ var repeatDuration = length / Velocity;
+ int repeatCount = RepeatCount;
+
+ if (IsSlider)
+ {
+ repeatCount += 1;
+ bool sliderStart = false;
+ for (var repeat = 0; repeat < repeatCount; repeat++)
+ {
+ sliderStart = !sliderStart;
+ for (var d = repeatPointDistance; d <= length; d += repeatPointDistance)
+ {
+ var repeatStartTime = StartTime + repeat * repeatDuration;
+ var distanceProgress = d / length;
+
+ IEnumerable bullets = createPattern();
+
+ foreach (Bullet b in bullets)
+ {
+ if (IsSlider)
+ {
+ b.StartTime = repeatStartTime;
+
+ b.Position = Curve.PositionAt(!sliderStart ? distanceProgress : 0);
+ }
+
+ b.NewCombo = NewCombo;
+ b.Ar = Ar;
+ b.Cs = Cs;
+ b.StackHeight = StackHeight;
+
+ b.ShootPlayer = shootPlayer;
+
+ AddNested(b);
+ }
+ }
+ }
+ }
+ else
+ {
+ IEnumerable bullets = createPattern();
+
+ foreach (Bullet b in bullets)
+ {
+ b.NewCombo = NewCombo;
+ b.Ar = Ar;
+ b.Cs = Cs;
+ b.StackHeight = StackHeight;
+
+ b.ShootPlayer = shootPlayer;
+
+ AddNested(b);
+ }
+ }
+ }
+
+ private IEnumerable createPattern()
+ {
+ if (patternAngleRadian == -10)
+ patternAngleRadian = MathHelper.DegreesToRadians(PatternAngleDegree - 90);
+
+ float bulletDiameter = PatternBulletDiameter;
+ bulletDiameter += Cs;
+
+ GetTotalBullets();
+
+ switch (PatternID)
+ {
+ default:
+ shootPlayer = false;
+ return patternWave(bulletDiameter);
+ case 1:
+ shootPlayer = false;
+ return patternWave(bulletDiameter);
+ case 2:
+ shootPlayer = true;
+ return PatternLine(bulletDiameter);
+ case 3:
+ shootPlayer = true;
+ return PatternTriangleWave(bulletDiameter);
+ case 4:
+ shootPlayer = false;
+ return PatternCoolWave(bulletDiameter);
+ case 5:
+ shootPlayer = true;
+ //should be PatternSpin() once its fixed
+ return patternWave(bulletDiameter);
+ }
+ }
+
+ ///
+ /// These will be the base patterns
+ ///
+ private List patternWave(float diameter)
+ {
+ List bullets = new List();
+ int numberOfBullets = (int)PatternDifficulty * 2 + 1;
+ float directionModifier = -0.1f * ((float)(numberOfBullets - 1) / 2);
+ for (int i = 1; i <= numberOfBullets; i++)
+ {
+ float angle = directionModifier + patternAngleRadian;
+ bullets.Add(new Bullet
+ {
+ StartTime = StartTime,
+ Position = Position,
+ ComboColour = ComboColour,
+ BulletSpeed = PatternSpeed,
+ BulletAngleRadian = angle,
+ BulletDiameter = diameter,
+ BulletDamage = PatternDamage,
+ DynamicBulletVelocity = dynamicPatternVelocity,
+ Team = 1,
+ Ghost = i == ((numberOfBullets - 1) / 2) + 1
+ });
+ directionModifier += 0.1f;
+ }
+ return bullets;
+ }
+ public List PatternLine(float diameter)
+ {
+ List bullets = new List();
+ int numberbullets = (int)PatternDifficulty + 1;
+ float speed = PatternSpeed;
+ for (int i = 1; i <= numberbullets; i++)
+ {
+ bullets.Add(new Bullet
+ {
+ StartTime = StartTime,
+ Position = Position,
+ ComboColour = ComboColour,
+ BulletSpeed = speed,
+ BulletAngleRadian = patternAngleRadian,
+ BulletDiameter = diameter,
+ BulletDamage = PatternDamage,
+ DynamicBulletVelocity = dynamicPatternVelocity,
+ Team = 1,
+ });
+ speed += 0.14f;
+ }
+ return bullets;
+ }
+ public List PatternCoolWave(float diameter)
+ {
+ List bullets = new List();
+ int numberbullets = (int)(PatternDifficulty * 2) + 3;
+ float speedModifier = 0.02f * (PatternDifficulty);
+ float directionModifier = -0.15f * (PatternDifficulty);
+ for (int i = 1; i <= numberbullets; i++)
+ {
+ PatternSpeed = PatternSpeed + Math.Abs(speedModifier);
+ float angle = directionModifier + patternAngleRadian;
+ bullets.Add(new Bullet
+ {
+ StartTime = StartTime,
+ Position = Position,
+ ComboColour = ComboColour,
+ BulletSpeed = PatternSpeed,
+ BulletAngleRadian = angle,
+ BulletDiameter = diameter,
+ BulletDamage = PatternDamage,
+ DynamicBulletVelocity = dynamicPatternVelocity,
+ Team = 1,
+ });
+ speedModifier -= 0.01f;
+ directionModifier += 0.075f;
+ }
+ return bullets;
+ }
+ public List PatternTriangleWave(float diameter)
+ {
+ List bullets = new List();
+ int numberwaves = (int)(PatternDifficulty + 2) / 2;
+ float originalDirection = 0f;
+ double duration = Duration / numberwaves;
+ for (int i = 1; i <= numberwaves; i++)
+ {
+ var numberbullets = i;
+ var speedModifier = 0.30f - (i - 1) * 0.03f;
+ for (int j = 1; j <= numberbullets; j++)
+ {
+ float directionModifier = ((j - 1) * 0.1f);
+ var speed = PatternSpeed + speedModifier;
+ float angle = patternAngleRadian + (originalDirection - directionModifier);
+ bullets.Add(new Bullet
+ {
+ StartTime = StartTime,
+ Position = Position,
+ ComboColour = ComboColour,
+ BulletSpeed = speed,
+ BulletAngleRadian = angle,
+ BulletDiameter = diameter,
+ BulletDamage = PatternDamage,
+ DynamicBulletVelocity = dynamicPatternVelocity,
+ Team = 1,
+ });
+ }
+ originalDirection = 0.05f * i;
+ }
+ return bullets;
+ }
+
+ public List PatternCurve(float diameter)
+ {
+ List bullets = new List();
+ int numberbullets = (int)(PatternDifficulty + 10) / 2;
+ float originalDirection = 0.01f * ((float)numberbullets / 2);
+ float speedModifier = 0f;
+ float directionModifier = 0f;
+ for (int i = 1; i <= numberbullets; i++)
+ {
+ var speed = PatternSpeed + speedModifier;
+ patternAngleRadian = patternAngleRadian - originalDirection + directionModifier;
+ directionModifier += 0.015f;
+ speedModifier -= (i * 0.002f);
+ bullets.Add(new Bullet
+ {
+ StartTime = StartTime,
+ Position = Position,
+ ComboColour = ComboColour,
+ BulletSpeed = speed,
+ BulletAngleRadian = patternAngleRadian,
+ BulletDiameter = diameter,
+ BulletDamage = PatternDamage,
+ DynamicBulletVelocity = dynamicPatternVelocity,
+ Team = 1,
+ });
+ }
+ return bullets;
+ }
+ public List PatternCircle(float diameter)
+ {
+ List bullets = new List();
+ int numberbullets = (int)(PatternDifficulty + 1) * 8;
+ float directionModifier = (360f / numberbullets);
+ directionModifier = MathHelper.DegreesToRadians(directionModifier);
+ for (int i = 1; i <= numberbullets; i++)
+ {
+ patternAngleRadian = patternAngleRadian + (directionModifier * (i - 1));
+ bullets.Add(new Bullet
+ {
+ StartTime = StartTime,
+ Position = Position,
+ ComboColour = ComboColour,
+ BulletSpeed = PatternSpeed,
+ BulletAngleRadian = patternAngleRadian,
+ BulletDiameter = diameter,
+ BulletDamage = PatternDamage,
+ DynamicBulletVelocity = dynamicPatternVelocity,
+ Team = 1,
+ });
+ }
+ return bullets;
+ }
+
+ //Finds what direction the player is
+ public float PlayerRelativePositionAngle(Vector2 playerPos, Vector2 enemyPos)
+ {
+ //Returns a Radian
+ var playerAngle = (float)Math.Atan2((playerPos.Y - enemyPos.Y), (playerPos.X - enemyPos.X));
+ return playerAngle;
+ }
+ #endregion
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/SeekingBullet.cs b/osu.Game.Rulesets.Vitaru/Objects/SeekingBullet.cs
new file mode 100644
index 0000000000..b773b855aa
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/SeekingBullet.cs
@@ -0,0 +1,7 @@
+namespace osu.Game.Rulesets.Vitaru.Objects
+{
+ public class SeekingBullet : Bullet
+ {
+ public float StartAngle { get; set; }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/VitaruConverter.cs b/osu.Game.Rulesets.Vitaru/Objects/VitaruConverter.cs
new file mode 100644
index 0000000000..f7d8dc7c10
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/VitaruConverter.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Modes.Objects;
+using osu.Game.Modes.Osu.Objects;
+using osu.Game.Modes.Vitaru.Objects.Characters;
+
+namespace osu.Game.Modes.Vitaru.Objects
+{
+ internal class VitaruConverter : HitObjectConverter
+ {
+ public override List Convert(Beatmap beatmap)
+ {
+ List output = new List();
+
+ foreach (HitObject i in beatmap.HitObjects)
+ {
+ VitaruHitObject h = i as VitaruHitObject;
+
+ if (h == null)
+ {
+ OsuHitObject o = i as OsuHitObject;
+
+ if (o == null) throw new HitObjectConvertException(@"Vitaru", i);
+
+ h = new Enemy();
+ }
+ output.Add(h);
+ }
+ return output;
+ }
+ }
+}
\ No newline at end of file
diff --git a/osu.Game.Rulesets.Vitaru/Objects/VitaruHitObject.cs b/osu.Game.Rulesets.Vitaru/Objects/VitaruHitObject.cs
new file mode 100644
index 0000000000..f27ec8544e
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/VitaruHitObject.cs
@@ -0,0 +1,46 @@
+using osu.Game.Rulesets.Objects;
+using OpenTK;
+using OpenTK.Graphics;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Rulesets.Vitaru.Objects
+{
+ public abstract class VitaruHitObject : HitObject
+ {
+ public float BPM;
+
+ public float Ar { get; set; } = -1;
+
+ public float Cs { get; set; } = -1;
+
+ public Vector2 Position { get; set; }
+
+ public Vector2 StackedPosition => Position + StackOffset;
+
+ public virtual Vector2 EndPosition => Position;
+
+ public Vector2 StackedEndPosition => EndPosition + StackOffset;
+
+ public virtual int StackHeight { get; set; }
+
+ public Vector2 StackOffset => new Vector2(0,0);
+
+ public float Scale { get; set; } = 1;
+
+ public abstract HitObjectType Type { get; }
+
+ public Color4 ComboColour { get; set; }
+ public virtual bool NewCombo { get; set; }
+ public int ComboIndex { get; set; }
+
+ protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ {
+ base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
+
+ EffectControlPoint effectPoint = controlPointInfo.EffectPointAt(StartTime);
+
+ Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Objects/VitaruHitObjectDifficulty.cs b/osu.Game.Rulesets.Vitaru/Objects/VitaruHitObjectDifficulty.cs
new file mode 100644
index 0000000000..ea09a9cb44
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Objects/VitaruHitObjectDifficulty.cs
@@ -0,0 +1,124 @@
+using OpenTK;
+using osu.Game.Rulesets.Vitaru.Beatmaps;
+using System;
+using System.Diagnostics;
+
+namespace osu.Game.Rulesets.Vitaru.Objects
+{
+ //Basically copied from standard atm, definitely needs work
+ internal class VitaruHitObjectDifficulty
+ {
+ ///
+ /// Factor by how much speed / aim strain decays per second.
+ ///
+ ///
+ /// These values are results of tweaking a lot and taking into account general feedback.
+ /// Opinionated observation: Speed is easier to maintain than accurate jumps.
+ ///
+ internal static readonly double[] DECAY_BASE = { 0.3, 0.15 };
+
+ ///
+ /// Pseudo threshold values to distinguish between "singles" and "streams"
+ ///
+ ///
+ /// Of course the border can not be defined clearly, therefore the algorithm has a smooth transition between those values.
+ /// They also are based on tweaking and general feedback.
+ ///
+ private const double stream_spacing_threshold = 110,
+ single_spacing_threshold = 125;
+
+ ///
+ /// Scaling values for weightings to keep aim and speed difficulty in balance.
+ ///
+ ///
+ /// Found from testing a very large map pool (containing all ranked maps) and keeping the average values the same.
+ ///
+ private static readonly double[] spacing_weight_scaling = { 1400, 26.25 };
+
+ ///
+ /// Almost the normed diameter of a hitbox (104 osu pixel). That is -after- position transforming.
+ ///
+ private const double almost_diameter = 90;
+
+ internal VitaruHitObject BaseHitObject;
+ internal double[] Strains = { 1, 1 };
+
+ internal int MaxCombo = 1;
+
+ private float scalingFactor;
+
+ private Vector2 startPosition = new Vector2(0);
+ private Vector2 endPosition;
+
+ internal VitaruHitObjectDifficulty(VitaruHitObject baseHitObject)
+ {
+ BaseHitObject = baseHitObject;
+ float hitboxRadius = baseHitObject.Scale * 64;
+
+ // We will scale everything by this factor, so we can assume a uniform HitboxSize among beatmaps.
+ scalingFactor = 52.0f / hitboxRadius;
+ if (hitboxRadius < 4)
+ {
+ float smallHitboxBonus = Math.Min(30.0f - hitboxRadius, 5.0f) / 50.0f;
+ scalingFactor *= 1.0f + smallHitboxBonus;
+ }
+
+ else
+ endPosition = startPosition;
+ }
+
+ internal void CalculateStrains(VitaruHitObjectDifficulty previousHitObject, double timeRate)
+ {
+ calculateSpecificStrain(previousHitObject, VitaruDifficultyCalculator.DifficultyType.Speed, timeRate);
+ calculateSpecificStrain(previousHitObject, VitaruDifficultyCalculator.DifficultyType.Aim, timeRate);
+ }
+
+ // Caution: The subjective values are strong with this one
+ private static double spacingWeight(double distance, VitaruDifficultyCalculator.DifficultyType type)
+ {
+ switch (type)
+ {
+ case VitaruDifficultyCalculator.DifficultyType.Speed:
+ if (distance > single_spacing_threshold)
+ return 2.5;
+ else if (distance > stream_spacing_threshold)
+ return 1.6 + 0.9 * (distance - stream_spacing_threshold) / (single_spacing_threshold - stream_spacing_threshold);
+ else if (distance > almost_diameter)
+ return 1.2 + 0.4 * (distance - almost_diameter) / (stream_spacing_threshold - almost_diameter);
+ else if (distance > almost_diameter / 2)
+ return 0.95 + 0.25 * (distance - almost_diameter / 2) / (almost_diameter / 2);
+ else
+ return 0.95;
+
+ case VitaruDifficultyCalculator.DifficultyType.Aim:
+ return Math.Pow(distance, 0.99);
+ }
+
+ Debug.Assert(false, "Invalid vitaru difficulty hit object type.");
+ return 0;
+ }
+
+ private void calculateSpecificStrain(VitaruHitObjectDifficulty previousHitObject, VitaruDifficultyCalculator.DifficultyType type, double timeRate)
+ {
+ double addition = 0;
+ double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate;
+ double decay = Math.Pow(DECAY_BASE[(int)type], timeElapsed / 1000);
+
+ if (BaseHitObject.Type == HitObjectType.Pattern)
+ {
+ addition = spacingWeight(DistanceTo(previousHitObject), type) * spacing_weight_scaling[(int)type];
+ }
+
+ // You will never find maps that require this amongst ranked maps.
+ addition /= Math.Max(timeElapsed, 50);
+
+ Strains[(int)type] = previousHitObject.Strains[(int)type] * decay + addition;
+ }
+
+ internal double DistanceTo(VitaruHitObjectDifficulty other)
+ {
+ // Scale the distance by hitbox size.
+ return (startPosition - other.endPosition).Length * scalingFactor;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/OpenTK.dll.config b/osu.Game.Rulesets.Vitaru/OpenTK.dll.config
new file mode 100644
index 0000000000..5620e3d9e2
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/OpenTK.dll.config
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/osu.Game.Rulesets.Vitaru/Properties/AssemblyInfo.cs b/osu.Game.Rulesets.Vitaru/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..1f5febc1b5
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Properties/AssemblyInfo.cs
@@ -0,0 +1,35 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("osu.Game.Modes.Vitaru")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("osu.Game.Modes.Vitaru")]
+[assembly: AssemblyCopyright("Copyright © 2016 - 2017")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("58f6c80c-1253-4a0e-a465-b8c85ebeadf3")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/osu.Game.Rulesets.Vitaru/Replays/VitaruAutoGenerator.cs b/osu.Game.Rulesets.Vitaru/Replays/VitaruAutoGenerator.cs
new file mode 100644
index 0000000000..6b9e1219b9
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Replays/VitaruAutoGenerator.cs
@@ -0,0 +1,47 @@
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Replays;
+using osu.Game.Rulesets.Vitaru.Objects;
+using osu.Game.Users;
+using System.Collections.Generic;
+using osu.Game.Rulesets.Objects.Types;
+
+namespace osu.Game.Rulesets.Vitaru.Replays
+{
+ /*
+ public class VitaruAutoGenerator : AutoGenerator
+ {
+ public VitaruAutoGenerator(Beatmap beatmap) : base(beatmap)
+ {
+ Replay = new Replay
+ {
+ User = new User
+ {
+ Username = @"Autoplay",
+ }
+ };
+ }
+
+ protected Replay Replay;
+ protected List Frames => Replay.Frames;
+
+ public override Replay Generate()
+ {
+ Frames.Add(new ReplayFrame(-100000, null, null, ReplayButtonState.None));
+ Frames.Add(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, null, null, ReplayButtonState.None));
+
+ for (int i = 0; i < Beatmap.HitObjects.Count; i++)
+ {
+ VitaruHitObject h = Beatmap.HitObjects[i];
+
+ IHasEndTime endTimeData = h as IHasEndTime;
+
+ double endTime = endTimeData?.EndTime ?? h.StartTime;
+
+ Frames.Add(new ReplayFrame(endTime, null, null, ReplayButtonState.None));
+ }
+
+ return Replay;
+ }
+ }
+ */
+}
diff --git a/osu.Game.Rulesets.Vitaru/Replays/VitaruReplayInputHandler.cs b/osu.Game.Rulesets.Vitaru/Replays/VitaruReplayInputHandler.cs
new file mode 100644
index 0000000000..0a6742c1f0
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Replays/VitaruReplayInputHandler.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+using osu.Framework.Input;
+using osu.Game.Rulesets.Replays;
+
+namespace osu.Game.Rulesets.Vitaru.Replays
+{
+ /*
+ public class VitaruReplayInputHandler : FramedReplayInputHandler
+ {
+ public VitaruReplayInputHandler(Replay replay)
+ : base(replay)
+ {
+ }
+
+ public override List GetPendingStates()
+ {
+ var actions = new List();
+
+ return new List { new ReplayState { PressedActions = actions } };
+ }
+ }
+ */
+}
diff --git a/osu.Game.Rulesets.Vitaru/Scoring/VitaruPerformanceCalculator.cs b/osu.Game.Rulesets.Vitaru/Scoring/VitaruPerformanceCalculator.cs
new file mode 100644
index 0000000000..14ca6e1e06
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Scoring/VitaruPerformanceCalculator.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.Vitaru.Objects;
+using System;
+using osu.Game.Rulesets.Vitaru.Beatmaps;
+using osu.Game.Rulesets.Mods;
+using System.Linq;
+using osu.Game.Rulesets.Vitaru.Mods;
+using osu.Game.Rulesets.Vitaru.Settings;
+
+namespace osu.Game.Rulesets.Vitaru.Scoring
+{
+ public class VitaruPerformanceCalculator : PerformanceCalculator
+ {
+ private readonly ScoringMetric currentScoringMetric = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.ScoringMetric);
+
+ private const float pp_multiplier = 1f;
+
+ public static float CurrentPPValue = 0;
+ public static float MaxPPValue = 0;
+
+ public VitaruPerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) : base(ruleset, beatmap, score)
+ {
+ CurrentPPValue = 0;
+ MaxPPValue = 0;
+ }
+
+ public override double Calculate(Dictionary categoryRatings = null)
+ {
+ Mod[] mods = Score.Mods;
+ double accuracy = Score.Accuracy;
+ int scoreMaxCombo = Score.MaxCombo;
+ float difficulty = 1;
+
+ double ar = Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate;
+ double cs = Beatmap.BeatmapInfo.BaseDifficulty.CircleSize;
+
+ int count300 = Convert.ToInt32(Score.Statistics[HitResult.Great]);
+ int count200 = Convert.ToInt32(Score.Statistics[HitResult.Good]);
+ int count100 = Convert.ToInt32(Score.Statistics[HitResult.Meh]);
+ //int count10 = Convert.ToInt32(Score.Statistics[HitResult.Tick]);
+ int countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]);
+
+ if (currentScoringMetric == ScoringMetric.ScoreZones)
+ {
+ // Don't count scores made with supposedly unranked mods
+ if (mods.Any(m => !m.Ranked))
+ difficulty = 0;
+
+ if (mods.Any(m => m is VitaruModNoFail))
+ difficulty *= 0.90f;
+ }
+
+ return difficulty * Score.TotalScore * pp_multiplier;
+ }
+
+ protected override BeatmapConverter CreateBeatmapConverter() => new VitaruBeatmapConverter();
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Scoring/VitaruScoreProcessor.cs b/osu.Game.Rulesets.Vitaru/Scoring/VitaruScoreProcessor.cs
new file mode 100644
index 0000000000..146a828450
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Scoring/VitaruScoreProcessor.cs
@@ -0,0 +1,94 @@
+using System.Collections.Generic;
+using osu.Framework.Extensions;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Vitaru.Judgements;
+using osu.Game.Rulesets.Vitaru.Objects;
+using osu.Game.Rulesets.Vitaru.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.Judgements;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets.Vitaru.UI;
+using osu.Game.Rulesets.Vitaru.Settings;
+using System.Linq;
+using osu.Game.Rulesets.Vitaru.Objects.Characters;
+using osu.Framework.Graphics;
+
+namespace osu.Game.Rulesets.Vitaru.Scoring
+{
+ internal class VitaruScoreProcessor : ScoreProcessor
+ {
+ private readonly ScoringMetric currentScoringMetric = VitaruSettings.VitaruConfigManager.GetBindable(VitaruSetting.ScoringMetric);
+
+ public new static int Combo;
+
+ public VitaruScoreProcessor(RulesetContainer rulesetContainer)
+ : base(rulesetContainer)
+ {
+ }
+
+ protected override void Reset(bool storeResults)
+ {
+ base.Reset(storeResults);
+
+ Health.Value = 1;
+ Combo = 0;
+
+ TotalScore.Value = 0;
+
+ scoreResultCounts.Clear();
+ comboResultCounts.Clear();
+ }
+
+ private readonly Dictionary scoreResultCounts = new Dictionary();
+ private readonly Dictionary comboResultCounts = new Dictionary();
+
+ protected override void SimulateAutoplay(Beatmap beatmap)
+ {
+ foreach (var obj in beatmap.HitObjects)
+ {
+ var pattern = obj as Pattern;
+ foreach (var unused in pattern.NestedHitObjects.OfType())
+ AddJudgement(new VitaruJudgement { Result = HitResult.Perfect });
+ foreach (var unused in pattern.NestedHitObjects.OfType())
+ AddJudgement(new VitaruJudgement { Result = HitResult.Perfect });
+ }
+ }
+
+ public override void PopulateScore(Score score)
+ {
+ base.PopulateScore(score);
+
+ score.Statistics[HitResult.Great] = scoreResultCounts.GetOrDefault(HitResult.Great);
+ score.Statistics[HitResult.Good] = scoreResultCounts.GetOrDefault(HitResult.Good);
+ score.Statistics[HitResult.Ok] = scoreResultCounts.GetOrDefault(HitResult.Ok);
+ score.Statistics[HitResult.Meh] = scoreResultCounts.GetOrDefault(HitResult.Meh);
+ score.Statistics[HitResult.Miss] = scoreResultCounts.GetOrDefault(HitResult.Miss);
+ }
+
+ protected override void OnNewJudgement(Judgement judgement)
+ {
+ base.OnNewJudgement(judgement);
+
+ var vitaruJudgement = (VitaruJudgement)judgement;
+
+ if (judgement.Result != HitResult.None)
+ {
+ scoreResultCounts[judgement.Result] = scoreResultCounts.GetOrDefault(judgement.Result) + 1;
+ comboResultCounts[vitaruJudgement.Combo] = comboResultCounts.GetOrDefault(vitaruJudgement.Combo) + 1;
+ Combo = comboResultCounts[vitaruJudgement.Combo];
+ }
+
+ if (VitaruPlayfield.VitaruPlayer != null)
+ Health.Value = VitaruPlayfield.VitaruPlayer.Health / VitaruPlayfield.VitaruPlayer.MaxHealth;
+
+ }
+ }
+
+ public enum ScoringMetric
+ {
+ Graze,
+ ScoreZones,
+ InverseCatch
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Settings/VitaruConfigManager.cs b/osu.Game.Rulesets.Vitaru/Settings/VitaruConfigManager.cs
new file mode 100644
index 0000000000..ac9cd6e0fe
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Settings/VitaruConfigManager.cs
@@ -0,0 +1,126 @@
+using eden.Game.GamePieces;
+using osu.Framework.Configuration;
+using osu.Framework.Platform;
+using osu.Game.Rulesets.Vitaru.Edit;
+using osu.Game.Rulesets.Vitaru.Objects.Characters;
+using osu.Game.Rulesets.Vitaru.Scoring;
+
+namespace osu.Game.Rulesets.Vitaru.Settings
+{
+ public class VitaruConfigManager : IniConfigManager
+ {
+ protected override string Filename => @"vitaru.ini";
+
+ public VitaruConfigManager(Storage storage) : base(storage) { }
+
+ protected override void InitialiseDefaults()
+ {
+ Set(VitaruSetting.ScoringMetric, ScoringMetric.Graze);
+ Set(VitaruSetting.DebugOverlay, false);
+ Set(VitaruSetting.DebugUIConfiguration, DebugUiConfiguration.PerformanceMetrics);
+ Set(VitaruSetting.GraphicsPresets, GraphicsPresets.Standard);
+ Set(VitaruSetting.GameMode, VitaruGamemode.Vitaru);
+ Set(VitaruSetting.Characters, Characters.ReimuHakurei);
+ Set(VitaruSetting.EditorConfiguration, EditorConfiguration.Simple);
+ Set(VitaruSetting.ComboFire, true);
+ Set(VitaruSetting.ShittyMultiplayer, false);
+ Set(VitaruSetting.FriendlyPlayerCount, 0, 0, 7);
+ Set(VitaruSetting.FriendlyPlayerOverride, false);
+ Set(VitaruSetting.EnemyPlayerCount, 0, 0, 8);
+ Set(VitaruSetting.EnemyPlayerOverride, false);
+
+ Set(VitaruSetting.PlayerOne, Characters.MarisaKirisame);
+ Set(VitaruSetting.PlayerTwo, Characters.SakuyaIzayoi);
+ Set(VitaruSetting.PlayerThree, Characters.FlandreScarlet);
+ Set(VitaruSetting.PlayerFour, Characters.RemiliaScarlet);
+ Set(VitaruSetting.PlayerFive, Characters.Cirno);
+ Set(VitaruSetting.PlayerSix, Characters.TenshiHinanai);
+ Set(VitaruSetting.PlayerSeven, Characters.YukariYakumo);
+
+ Set(VitaruSetting.EnemyOne, Characters.MarisaKirisame);
+ Set(VitaruSetting.EnemyTwo, Characters.SakuyaIzayoi);
+ Set(VitaruSetting.EnemyThree, Characters.FlandreScarlet);
+ Set(VitaruSetting.EnemyFour, Characters.RemiliaScarlet);
+ Set(VitaruSetting.EnemyFive, Characters.Cirno);
+ Set(VitaruSetting.EnemySix, Characters.TenshiHinanai);
+ Set(VitaruSetting.EnemySeven, Characters.YukariYakumo);
+ Set(VitaruSetting.EnemyEight, Characters.Chen);
+
+ Set(VitaruSetting.VectorVideos, true);
+ Set(VitaruSetting.Skin, "default");
+
+ //Touhosu
+ Set(VitaruSetting.Familiar, false);
+ Set(VitaruSetting.LastDance, false);
+ Set(VitaruSetting.Insane, true);
+ Set(VitaruSetting.Awoken, false);
+ Set(VitaruSetting.Sacred, false);
+ Set(VitaruSetting.Resurrected, false);
+ Set(VitaruSetting.Revenge, false);
+
+ //Online Multiplayer
+ Set(VitaruSetting.HostIP, "Host IP Address");
+ Set(VitaruSetting.LocalIP, "Your Local IP Address");
+ }
+
+ }
+
+ public enum VitaruSetting
+ {
+ ScoringMetric,
+ DebugOverlay,
+ DebugUIConfiguration,
+ GraphicsPresets,
+ GameMode,
+ Characters,
+ EditorConfiguration,
+ ComboFire,
+ ShittyMultiplayer,
+ FriendlyPlayerCount,
+ FriendlyPlayerOverride,
+ EnemyPlayerCount,
+ EnemyPlayerOverride,
+
+ //Becuase fuck arrays
+ PlayerOne,
+ PlayerTwo,
+ PlayerThree,
+ PlayerFour,
+ PlayerFive,
+ PlayerSix,
+ PlayerSeven,
+
+ //See above comment
+ EnemyOne,
+ EnemyTwo,
+ EnemyThree,
+ EnemyFour,
+ EnemyFive,
+ EnemySix,
+ EnemySeven,
+ EnemyEight,
+
+ VectorVideos,
+ Skin,
+
+ //Touhosu
+ Familiar,
+ LastDance,
+ Insane,
+ Awoken,
+ Sacred,
+ Resurrected,
+ Revenge,
+
+ HostIP,
+ LocalIP,
+ }
+
+ public enum GraphicsPresets
+ {
+ HighPerformance,
+ Standard,
+ StandardCompetitive,
+ HighPerformanceCompetitive
+ }
+}
diff --git a/osu.Game.Rulesets.Vitaru/Settings/VitaruSettings.cs b/osu.Game.Rulesets.Vitaru/Settings/VitaruSettings.cs
new file mode 100644
index 0000000000..daeb8a42af
--- /dev/null
+++ b/osu.Game.Rulesets.Vitaru/Settings/VitaruSettings.cs
@@ -0,0 +1,353 @@
+using eden.Game.GamePieces;
+using OpenTK;
+using osu.Framework.Allocation;
+using osu.Framework.Configuration;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Platform;
+using osu.Game.Rulesets.Vitaru.Multi;
+using osu.Game.Overlays.Settings;
+using osu.Game.Rulesets.Vitaru.Objects.Characters;
+using System.Collections.Generic;
+using System.Linq;
+using Symcol.Rulesets.Core;
+using Symcol.Rulesets.Core.Wiki;
+using osu.Game.Rulesets.Vitaru.Wiki;
+using osu.Game.Rulesets.Vitaru.Scoring;
+using osu.Game.Rulesets.Vitaru.Edit;
+using osu.Game.Screens.Symcol.Screens;
+using Symcol.Rulesets.Core.Multiplayer.Screens;
+
+namespace osu.Game.Rulesets.Vitaru.Settings
+{
+ public class VitaruSettings : SymcolSettingsSubsection
+ {
+ protected override string Header => "vitaru!";
+
+ public override WikiOverlay Wiki => vitaruWiki;
+
+ public override RulesetLobbyItem RulesetLobbyItem => vitaruLobby;
+
+ private readonly VitaruWikiOverlay vitaruWiki = new VitaruWikiOverlay();
+
+ private readonly VitaruLobbyItem vitaruLobby = new VitaruLobbyItem();
+
+ public static VitaruConfigManager VitaruConfigManager;
+
+ private static VitaruAPIContainer api;
+
+ private Bindable selectedCharacter;
+
+ private FillFlowContainer multiplayerSettings;
+ private Bindable multiplayer;
+ private Bindable friendlyPlayerCount;
+ private Bindable friendlyPlayerOverride;
+ private FillFlowContainer friendlyPlayerSettings;
+ private Bindable enemyPlayerCount;
+ private Bindable enemyPlayerOverride;
+ private FillFlowContainer enemyPlayerSettings;
+
+ private FillFlowContainer debugUiSettings;
+ private Bindable showDebugUi;
+
+ private SettingsDropdown skin;
+ private Bindable currentSkin;
+
+ private const int transition_duration = 400;
+
+ [BackgroundDependencyLoader]
+ private void load(GameHost host, Storage storage)
+ {
+ if (api == null)
+ Add(api = new VitaruAPIContainer());
+
+ VitaruConfigManager = new VitaruConfigManager(host.Storage);
+
+ Storage skinsStorage = storage.GetStorageForDirectory("Skins");
+
+ showDebugUi = VitaruConfigManager.GetBindable(VitaruSetting.DebugOverlay);
+ selectedCharacter = VitaruConfigManager.GetBindable(VitaruSetting.Characters);
+ multiplayer = VitaruConfigManager.GetBindable(VitaruSetting.ShittyMultiplayer);
+ friendlyPlayerCount = VitaruConfigManager.GetBindable(VitaruSetting.FriendlyPlayerCount);
+ friendlyPlayerOverride = VitaruConfigManager.GetBindable(VitaruSetting.FriendlyPlayerOverride);
+ enemyPlayerCount = VitaruConfigManager.GetBindable(VitaruSetting.EnemyPlayerCount);
+ enemyPlayerOverride = VitaruConfigManager.GetBindable(VitaruSetting.EnemyPlayerOverride);
+
+ Children = new Drawable[]
+ {
+ new SettingsEnumDropdown
+ {
+ LabelText = "Vitaru's current gamemode",
+ Bindable = VitaruConfigManager.GetBindable(VitaruSetting.GameMode)
+ },
+ new SettingsEnumDropdown
+ {
+ LabelText = "Selected Character",
+ Bindable = selectedCharacter
+ },
+ new SettingsEnumDropdown
+ {
+ LabelText = "Graphics Presets",
+ Bindable = VitaruConfigManager.GetBindable(VitaruSetting.GraphicsPresets)
+ },
+ new SettingsEnumDropdown
+ {
+ LabelText = "Current Editor Configuration",
+ Bindable = VitaruConfigManager.GetBindable(VitaruSetting.EditorConfiguration)
+ },
+ new SettingsEnumDropdown
+ {
+ LabelText = "Current Scoring Metric used (Difficulty, Score and PP)",
+ Bindable = VitaruConfigManager.GetBindable(VitaruSetting.ScoringMetric)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = "Enable ComboFire",
+ Bindable = VitaruConfigManager.GetBindable(VitaruSetting.ComboFire)
+ },
+ new SettingsCheckbox
+ {
+ LabelText = "Offline Multiplayer",
+ Bindable = multiplayer
+ },
+ multiplayerSettings = new FillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ AutoSizeDuration = transition_duration,
+ AutoSizeEasing = Easing.OutQuint,
+ Masking = true,
+
+ Children = new Drawable[]
+ {
+ new SettingsSlider
+ {
+ LabelText = "How many Friends?",
+ Bindable = friendlyPlayerCount,
+ },
+ new SettingsCheckbox
+ {
+ LabelText = "Override Friendly Characters",
+ Bindable = friendlyPlayerOverride
+ },
+ friendlyPlayerSettings = new FillFlowContainer
+ {
+ Direction = FillDirection.Vertical,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ AutoSizeDuration = transition_duration,
+ AutoSizeEasing = Easing.OutQuint,
+ Masking = true,
+
+ Children = new Drawable[]
+ {
+ new SettingsEnumDropdown
+ {
+ LabelText = "PlayerOne override",
+ Bindable = VitaruConfigManager.GetBindable