├── .gitignore ├── .gitattributes ├── README.md ├── osu.Game.Rulesets.Solosu ├── Objects │ ├── IHasLane.cs │ ├── HardBeat.cs │ ├── LanedSolosuHitObject.cs │ ├── Bonus.cs │ ├── Drawables │ │ ├── DrawableLanedSolosuHitObject.cs │ │ ├── DrawableBonus.cs │ │ ├── DrawableSolosuHitObject.cs │ │ ├── DrawableHardBeat.cs │ │ ├── DrawablePacketJudgement.cs │ │ ├── DrawableStream.cs │ │ ├── DrawableMultiStream.cs │ │ └── DrawablePacket.cs │ ├── SolosuLifetimeEntry.cs │ ├── SolosuHitObject.cs │ ├── Packet.cs │ ├── Stream.cs │ └── MultiLaneStream.cs ├── Resources │ └── Textures │ │ └── SolosuIcon.png ├── Replays │ ├── IFlowObject.cs │ ├── SolosuFramedReplayInputHandler.cs │ ├── SolosuReplayRecorder.cs │ ├── ActualReplay.cs │ ├── SolosuReplayFrame.cs │ ├── SolosuAutoGenerator.cs │ └── DifficultyFlowLane.cs ├── Mods │ ├── SolosuModNoFail.cs │ ├── SolosuModRelax.cs │ ├── SolosuModAutoplay.cs │ └── SolosuModAutopilot.cs ├── Patterns │ ├── PatternSet.cs │ └── Pattern.cs ├── Scoring │ └── SolosuHitWindows.cs ├── osu.Game.Rulesets.Solosu.csproj ├── UI │ ├── SolosuColours.cs │ ├── BeatDetector.cs │ ├── DrawableSolosuRuleset.cs │ ├── Lane.cs │ ├── Wireframe.cs │ ├── SolosuPlayfield.cs │ └── PlayerByte.cs ├── Beatmaps │ ├── SolosuBeatmapProcessor.cs │ ├── SolosuBeatmap.cs │ └── SolosuBeatmapConverter.cs ├── SolosuInputManager.cs ├── Localisation │ ├── ModStrings.l12n.generated.cs │ ├── ActionStrings.l12n.generated.cs │ ├── JudgementStrings.l12n.generated.cs │ ├── StatsStrings.l12n.generated.cs │ ├── Action.Strings.resx │ ├── Action.Strings.pl.resx │ ├── Action.Strings.ru.resx │ ├── Mod.Strings.resx │ ├── Mod.Strings.pl.resx │ ├── Mod.Strings.ru.resx │ ├── Judgement.Strings.resx │ ├── Judgement.Strings.pl.resx │ ├── Judgement.Strings.ru.resx │ ├── Stats.Strings.pl.resx │ ├── Stats.Strings.resx │ └── Stats.Strings.ru.resx ├── SolosuDifficultyCalculator.cs ├── Collections │ └── TimeSeekableList.cs ├── SolosuTextures.cs ├── Extensions.cs └── SolosuRuleset.cs ├── osu.Game.Rulesets.Solosu.Tests ├── VisualTestRunner.cs ├── TestSceneSolosuPlayer.cs ├── TestSceneOsuGame.cs └── osu.Game.Rulesets.Solosu.Tests.csproj ├── LICENSE └── osu.Game.Rulesets.Solosu.sln /.gitignore: -------------------------------------------------------------------------------- 1 | **/.vs/ 2 | **/.vscode/ 3 | **/bin/ 4 | **/obj/ 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solosu 2 | A custom osu ruleset based on solous. This repository adopted a no longer maintained ( or ever working ) repo idea. 3 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/IHasLane.cs: -------------------------------------------------------------------------------- 1 | namespace osu.Game.Rulesets.Solosu.Objects { 2 | public interface IHasLane { 3 | SolosuLane Lane { get; } 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Resources/Textures/SolosuIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flutterish/Solosu/HEAD/osu.Game.Rulesets.Solosu/Resources/Textures/SolosuIcon.png -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Replays/IFlowObject.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace osu.Game.Rulesets.Solosu.Replays { 4 | public interface IFlowObject { 5 | IEnumerable CreateFlowObjects (); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/HardBeat.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Judgements; 2 | 3 | namespace osu.Game.Rulesets.Solosu.Objects { 4 | public class HardBeat : SolosuHitObject { 5 | public override Judgement CreateJudgement () { 6 | return new IgnoreJudgement(); 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/LanedSolosuHitObject.cs: -------------------------------------------------------------------------------- 1 | namespace osu.Game.Rulesets.Solosu.Objects { 2 | public class LanedSolosuHitObject : SolosuHitObject, IHasLane { 3 | public SolosuLane Lane { get; set; } 4 | } 5 | 6 | public enum SolosuLane { 7 | Left, 8 | Center, 9 | Right 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Mods/SolosuModNoFail.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Localisation; 2 | using osu.Game.Rulesets.Mods; 3 | 4 | namespace osu.Game.Rulesets.Solosu.Mods { 5 | public class SolosuModNoFail : ModNoFail { 6 | public override LocalisableString Description => Localisation.ModStrings.NoFailDescription; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Patterns/PatternSet.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace osu.Game.Rulesets.Solosu.Patterns { 4 | public class PatternSet { 5 | public List Patterns = new List { 6 | "ABBA", 7 | "A(AA)!", 8 | "(ABAC)+", // stairs 9 | "(AB)+", // 2 lane castle 10 | "(ABCB)+" // 3 lane castle 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Bonus.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Judgements; 2 | using osu.Game.Rulesets.Scoring; 3 | 4 | namespace osu.Game.Rulesets.Solosu.Objects { 5 | public class Bonus : LanedSolosuHitObject { 6 | public override Judgement CreateJudgement () 7 | => new BonusJudgement(); 8 | } 9 | 10 | public class BonusJudgement : Judgement { 11 | public override HitResult MaxResult => HitResult.SmallTickHit; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu.Tests/VisualTestRunner.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework; 2 | using osu.Framework.Platform; 3 | using osu.Game.Tests; 4 | using System; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Tests { 7 | public static class VisualTestRunner { 8 | [STAThread] 9 | public static int Main ( string[] args ) { 10 | using DesktopGameHost host = Host.GetSuitableDesktopHost( @"osu", new() { BindIPC = true } ); 11 | host.Run( new OsuTestBrowser() ); 12 | return 0; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Mods/SolosuModRelax.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Localisation; 2 | using osu.Game.Rulesets.Mods; 3 | using System; 4 | 5 | namespace osu.Game.Rulesets.Solosu.Mods { 6 | public class SolosuModRelax : ModRelax { 7 | public override LocalisableString Description => "No need to click, just follow the path"; 8 | public override Type[] IncompatibleMods => new[] { typeof( SolosuModAutopilot ), typeof( ModAutoplay ) }; 9 | public override bool HasImplementation => true; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Drawables/DrawableLanedSolosuHitObject.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Solosu.UI; 2 | 3 | namespace osu.Game.Rulesets.Solosu.Objects.Drawables { 4 | public class DrawableLanedSolosuHitObject : DrawableSolosuHitObject where T : LanedSolosuHitObject { 5 | public Lane Lane => Lanes[ HitObject.Lane ]; 6 | 7 | protected override void Update () { 8 | base.Update(); 9 | Y = -(float)Lane.HeightAtTime( Clock.CurrentTime, HitObject.StartTime ); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Drawables/DrawableBonus.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Scoring; 2 | 3 | namespace osu.Game.Rulesets.Solosu.Objects.Drawables { 4 | public class DrawableBonus : DrawableLanedSolosuHitObject { 5 | protected override void CheckForResult ( bool userTriggered, double timeOffset ) { 6 | if ( ParentHitObject?.Result.Type == HitResult.Miss ) 7 | ApplyResult( j => j.Type = HitResult.SmallTickMiss ); 8 | else if ( timeOffset >= 0 ) 9 | ApplyResult( j => j.Type = HitResult.SmallTickHit ); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/SolosuLifetimeEntry.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Game.Rulesets.Objects; 3 | 4 | namespace osu.Game.Rulesets.Solosu.Objects { 5 | public class SolosuLifetimeEntry : HitObjectLifetimeEntry { 6 | BindableDouble scrollDuration; 7 | public SolosuLifetimeEntry ( HitObject hitObject, BindableDouble scrollDuration ) : base( hitObject ) { 8 | this.scrollDuration = scrollDuration; 9 | } 10 | protected override double InitialLifetimeOffset => scrollDuration?.Value ?? 3000; // NOTE this doesnt take into account scroll speeds 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu.Tests/TestSceneSolosuPlayer.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using osu.Game.Tests.Visual; 3 | 4 | namespace osu.Game.Rulesets.Solosu.Tests { 5 | [TestFixture] 6 | public class TestSceneSolosuPlayerAuto : PlayerTestScene { 7 | protected override Ruleset CreatePlayerRuleset () => new SolosuRuleset(); 8 | protected override bool Autoplay => true; 9 | } 10 | 11 | [TestFixture] 12 | public class TestSceneSolosuPlayer : PlayerTestScene { 13 | protected override Ruleset CreatePlayerRuleset () => new SolosuRuleset(); 14 | protected override bool Autoplay => false; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Scoring/SolosuHitWindows.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Scoring; 2 | 3 | namespace osu.Game.Rulesets.Solosu.Scoring { 4 | public class SolosuHitWindows : HitWindows { 5 | public override bool IsHitResultAllowed ( HitResult result ) 6 | => result is HitResult.Perfect or HitResult.Great or HitResult.Meh or HitResult.Miss; 7 | protected override DifficultyRange[] GetRanges () => new DifficultyRange[] { 8 | new DifficultyRange( HitResult.Perfect, 40, 35, 30 ), 9 | new DifficultyRange( HitResult.Great, 80, 70, 60 ), 10 | new DifficultyRange( HitResult.Meh, 200, 180, 150 ), 11 | new DifficultyRange( HitResult.Miss, 400, 450, 500 ) 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/SolosuHitObject.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Objects; 2 | using osu.Game.Rulesets.Scoring; 3 | using osu.Game.Rulesets.Solosu.Objects.Drawables; 4 | using osu.Game.Rulesets.Solosu.Scoring; 5 | using System; 6 | 7 | namespace osu.Game.Rulesets.Solosu.Objects { 8 | public abstract class SolosuHitObject : HitObject { 9 | public virtual DrawableSolosuHitObject AsDrawable () => null; 10 | public LeftRight PieceDirection; 11 | 12 | public virtual void ApplyVisualRandom ( Random random ) { 13 | PieceDirection = random.Chance( 0.5 ) ? LeftRight.Left : LeftRight.Right; 14 | } 15 | protected override HitWindows CreateHitWindows () => new SolosuHitWindows(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu.Tests/TestSceneOsuGame.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Shapes; 4 | using osu.Framework.Platform; 5 | using osu.Game.Tests.Visual; 6 | using osuTK.Graphics; 7 | 8 | namespace osu.Game.Rulesets.Solosu.Tests { 9 | public class TestSceneOsuGame : OsuTestScene { 10 | [BackgroundDependencyLoader] 11 | private void load ( GameHost host, OsuGameBase gameBase ) { 12 | OsuGame game = new OsuGame(); 13 | 14 | Children = new Drawable[] 15 | { 16 | new Box 17 | { 18 | RelativeSizeAxes = Axes.Both, 19 | Colour = Color4.Black, 20 | } 21 | }; 22 | 23 | AddGame( game ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Mods/SolosuModAutoplay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Localisation; 2 | using osu.Game.Beatmaps; 3 | using osu.Game.Online.API.Requests.Responses; 4 | using osu.Game.Rulesets.Mods; 5 | using osu.Game.Rulesets.Solosu.Replays; 6 | using osu.Game.Scoring; 7 | using osu.Game.Users; 8 | using System.Collections.Generic; 9 | 10 | namespace osu.Game.Rulesets.Solosu.Mods { 11 | public class SolosuModAutoplay : ModAutoplay { 12 | public override ModReplayData CreateReplayData ( IBeatmap beatmap, IReadOnlyList mods ) => new( new SolosuAutoGenerator( beatmap ).Generate(), new() { 13 | Username = "Autosu" 14 | } ); 15 | 16 | public override LocalisableString Description => Localisation.ModStrings.AutoplayDescription; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Replays/SolosuFramedReplayInputHandler.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Input.StateChanges; 2 | using osu.Game.Replays; 3 | using osu.Game.Rulesets.Replays; 4 | using System.Collections.Generic; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Replays { 7 | public class SolosuFramedReplayInputHandler : FramedReplayInputHandler { 8 | public SolosuFramedReplayInputHandler ( Replay replay ) : base( replay ) { } 9 | 10 | protected override bool IsImportant ( SolosuReplayFrame frame ) => true; 11 | protected override void CollectReplayInputs ( List inputs ) { 12 | inputs.Add( new ReplayState { 13 | PressedActions = CurrentFrame?.Actions ?? new List() 14 | } ); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/osu.Game.Rulesets.Solosu.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | osu.Game.Rulesets.Sample 5 | Library 6 | AnyCPU 7 | osu.Game.Rulesets.Solosu 8 | preview 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Packet.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Solosu.Replays; 2 | using osuTK; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Objects { 7 | public class Packet : LanedSolosuHitObject, IFlowObject { 8 | public Vector2 Offset; 9 | public double MissRotation; 10 | 11 | public override void ApplyVisualRandom ( Random random ) { 12 | base.ApplyVisualRandom( random ); 13 | MissRotation = random.Chance( 0.5 ) ? random.Range( 50, 80 ) : random.Range( -50, -80 ); 14 | } 15 | 16 | public IEnumerable CreateFlowObjects () { 17 | yield return new FlowObject( StartTime, StartTime, FlowObjectType.Intercept, Lane ) { Source = this }; 18 | } 19 | } 20 | 21 | public enum LeftRight { 22 | Left, 23 | Right 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Mods/SolosuModAutopilot.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Sprites; 2 | using osu.Framework.Localisation; 3 | using osu.Game.Graphics; 4 | using osu.Game.Rulesets.Mods; 5 | using System; 6 | 7 | namespace osu.Game.Rulesets.Solosu.Mods { 8 | public class SolosuModAutopilot : Mod { 9 | public override IconUsage? Icon => OsuIcon.ModAutopilot; 10 | public override ModType Type => ModType.Automation; 11 | public override LocalisableString Description => "No need to move, just tap to the rhythm"; 12 | public override string Name => "Autopilot"; 13 | public override string Acronym => "AP"; 14 | public override double ScoreMultiplier => 1; 15 | public override Type[] IncompatibleMods => new[] { typeof( ModRelax ), typeof( ModAutoplay ) }; 16 | public override bool HasImplementation => true; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/UI/SolosuColours.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Game.Rulesets.Scoring; 3 | 4 | namespace osu.Game.Rulesets.Solosu.UI { 5 | public class SolosuColours : Drawable { 6 | public readonly Colour4 Perfect = Colour4.FromHex( "#ff7be0" ); 7 | public readonly Colour4 Great = Colour4.FromHex( "#f95bae" ); 8 | public readonly Colour4 Meh = Colour4.FromHex( "#f33b7c" ); 9 | public readonly Colour4 Miss = Colour4.FromHex( "#ed1a48" ); 10 | public readonly Colour4 Regular = Colour4.Cyan; 11 | 12 | public Colour4 ColourFor ( HitResult result ) { 13 | if ( result == HitResult.IgnoreHit ) return Perfect; 14 | if ( result == HitResult.Perfect ) return Perfect; 15 | if ( result == HitResult.Great ) return Great; 16 | if ( result == HitResult.Meh ) return Meh; 17 | else return Miss; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Beatmaps/SolosuBeatmapProcessor.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Beatmaps; 2 | using osu.Game.Rulesets.Solosu.Objects; 3 | using System; 4 | using System.Linq; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Beatmaps { 7 | public class SolosuBeatmapProcessor : BeatmapProcessor { 8 | public SolosuBeatmapProcessor ( IBeatmap beatmap ) : base( beatmap ) { } 9 | 10 | public override void PostProcess () { 11 | base.PostProcess(); 12 | var random = Beatmap.Random(); 13 | 14 | foreach ( var i in Beatmap.HitObjects.OfType() ) { 15 | applyRandom( i, random ); 16 | } 17 | } 18 | 19 | void applyRandom ( SolosuHitObject h, Random random ) { 20 | h.ApplyVisualRandom( random ); 21 | foreach ( var k in h.NestedHitObjects.OfType() ) { 22 | applyRandom( k, random ); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/SolosuInputManager.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Input.Bindings; 2 | using osu.Framework.Localisation; 3 | using osu.Game.Rulesets.UI; 4 | 5 | namespace osu.Game.Rulesets.Solosu { 6 | public class SolosuInputManager : RulesetInputManager { 7 | public SolosuInputManager ( RulesetInfo ruleset ) : base( ruleset, 0, SimultaneousBindingMode.Unique ) { } 8 | } 9 | 10 | public enum SolosuAction { 11 | [LocalisableDescription( typeof( Localisation.ActionStrings ), nameof( Localisation.ActionStrings.Left ) )] 12 | Left, 13 | 14 | // this is for buffered input 15 | [LocalisableDescription( typeof( Localisation.ActionStrings ), nameof( Localisation.ActionStrings.Center ) )] 16 | Center, 17 | 18 | [LocalisableDescription( typeof( Localisation.ActionStrings ), nameof( Localisation.ActionStrings.Right ) )] 19 | Right 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Replays/SolosuReplayRecorder.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Replays; 2 | using osu.Game.Rulesets.UI; 3 | using osu.Game.Scoring; 4 | using osuTK; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace osu.Game.Rulesets.Solosu.Replays { 9 | public class SolosuReplayRecorder : ReplayRecorder { 10 | public SolosuReplayRecorder ( Score target ) : base( target ) { } 11 | 12 | protected override ReplayFrame HandleFrame ( Vector2 mousePosition, List actions, ReplayFrame previousFrame ) { 13 | SolosuReplayFrame previous = previousFrame as SolosuReplayFrame; 14 | if ( previous is null ) { 15 | return new SolosuReplayFrame( actions ) { Time = Time.Current }; 16 | } 17 | else if ( ( actions.Count() != previous.Actions.Count ) || actions.Except( previous.Actions ).Any() ) { 18 | return new SolosuReplayFrame( actions ) { Time = Time.Current }; 19 | } 20 | return null; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/ModStrings.l12n.generated.cs: -------------------------------------------------------------------------------- 1 | // This file is auto-generated 2 | // Do not edit it manually as it will be overwritten 3 | 4 | using osu.Framework.Localisation; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Localisation { 7 | public static class ModStrings { 8 | private const string PREFIX = "osu.Game.Rulesets.Solosu.Localisation.Mod.Strings"; 9 | private static string getKey( string key ) => $"{PREFIX}:{key}"; 10 | 11 | /// 12 | /// Let the cute bot do... wait, where did he go? 13 | /// 14 | public static readonly LocalisableString AutoplayDescription = new TranslatableString( 15 | getKey( "autoplay-description" ), 16 | "Let the cute bot do... wait, where did he go?" 17 | ); 18 | 19 | /// 20 | /// TCP at it's finest 21 | /// 22 | public static readonly LocalisableString NoFailDescription = new TranslatableString( 23 | getKey( "no-fail-description" ), 24 | "TCP at it's finest" 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/ActionStrings.l12n.generated.cs: -------------------------------------------------------------------------------- 1 | // This file is auto-generated 2 | // Do not edit it manually as it will be overwritten 3 | 4 | using osu.Framework.Localisation; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Localisation { 7 | public static class ActionStrings { 8 | private const string PREFIX = "osu.Game.Rulesets.Solosu.Localisation.Action.Strings"; 9 | private static string getKey( string key ) => $"{PREFIX}:{key}"; 10 | 11 | /// 12 | /// Center 13 | /// 14 | public static readonly LocalisableString Center = new TranslatableString( 15 | getKey( "center" ), 16 | "Center" 17 | ); 18 | 19 | /// 20 | /// Left 21 | /// 22 | public static readonly LocalisableString Left = new TranslatableString( 23 | getKey( "left" ), 24 | "Left" 25 | ); 26 | 27 | /// 28 | /// Right 29 | /// 30 | public static readonly LocalisableString Right = new TranslatableString( 31 | getKey( "right" ), 32 | "Right" 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Flutterish 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/SolosuDifficultyCalculator.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Beatmaps; 2 | using osu.Game.Rulesets.Difficulty; 3 | using osu.Game.Rulesets.Difficulty.Preprocessing; 4 | using osu.Game.Rulesets.Difficulty.Skills; 5 | using osu.Game.Rulesets.Mods; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace osu.Game.Rulesets.Solosu { 10 | public class SolosuDifficultyCalculator : DifficultyCalculator { 11 | // TODO SolosuDifficultyCalculator 12 | public SolosuDifficultyCalculator ( IRulesetInfo ruleset, IWorkingBeatmap beatmap ) 13 | : base( ruleset, beatmap ) { 14 | } 15 | 16 | protected override DifficultyAttributes CreateDifficultyAttributes ( IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate ) { 17 | return new DifficultyAttributes( mods, beatmap.BeatmapInfo.Difficulty.OverallDifficulty ); 18 | } 19 | 20 | protected override IEnumerable CreateDifficultyHitObjects ( IBeatmap beatmap, double clockRate ) => Enumerable.Empty(); 21 | 22 | protected override Skill[] CreateSkills ( IBeatmap beatmap, Mod[] mods, double clockRate ) 23 | => new Skill[ 0 ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Replays/ActualReplay.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Replays; 2 | using System; 3 | 4 | namespace osu.Game.Rulesets.Solosu.Replays { 5 | public class ActualReplay { 6 | public Replay replay; 7 | 8 | private int frameIndex = 0; 9 | int nextFrameIndex => Math.Clamp( frameIndex + 1, 0, replay.Frames.Count - 1 ); 10 | int previousFrameIndex => Math.Clamp( frameIndex - 1, 0, replay.Frames.Count - 1 ); 11 | SolosuReplayFrame currentFrame => replay.Frames[ frameIndex ] as SolosuReplayFrame; 12 | SolosuReplayFrame nextFrame => replay.Frames[ nextFrameIndex ] as SolosuReplayFrame; 13 | SolosuReplayFrame previousFrame => replay.Frames[ previousFrameIndex ] as SolosuReplayFrame; 14 | 15 | SolosuReplayFrame lastFrame; 16 | 17 | /// 18 | /// Next frame towards that time, or null if no frame change occured. 19 | /// 20 | public SolosuReplayFrame FrameAt ( double time ) { 21 | if ( time > nextFrame.Time ) { 22 | frameIndex = nextFrameIndex; 23 | } 24 | else if ( time < currentFrame.Time ) { 25 | frameIndex = previousFrameIndex; 26 | } 27 | 28 | if ( currentFrame == lastFrame ) return null; 29 | return lastFrame = currentFrame; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/JudgementStrings.l12n.generated.cs: -------------------------------------------------------------------------------- 1 | // This file is auto-generated 2 | // Do not edit it manually as it will be overwritten 3 | 4 | using osu.Framework.Localisation; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Localisation { 7 | public static class JudgementStrings { 8 | private const string PREFIX = "osu.Game.Rulesets.Solosu.Localisation.Judgement.Strings"; 9 | private static string getKey( string key ) => $"{PREFIX}:{key}"; 10 | 11 | /// 12 | /// Nice 13 | /// 14 | public static readonly LocalisableString Great = new TranslatableString( 15 | getKey( "great" ), 16 | "Nice" 17 | ); 18 | 19 | /// 20 | /// Corrupt 21 | /// 22 | public static readonly LocalisableString Miss = new TranslatableString( 23 | getKey( "miss" ), 24 | "Corrupt" 25 | ); 26 | 27 | /// 28 | /// Ok 29 | /// 30 | public static readonly LocalisableString Ok = new TranslatableString( 31 | getKey( "ok" ), 32 | "Ok" 33 | ); 34 | 35 | /// 36 | /// Perfect 37 | /// 38 | public static readonly LocalisableString Perfect = new TranslatableString( 39 | getKey( "perfect" ), 40 | "Perfect" 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/StatsStrings.l12n.generated.cs: -------------------------------------------------------------------------------- 1 | // This file is auto-generated 2 | // Do not edit it manually as it will be overwritten 3 | 4 | using osu.Framework.Localisation; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Localisation { 7 | public static class StatsStrings { 8 | private const string PREFIX = "osu.Game.Rulesets.Solosu.Localisation.Stats.Strings"; 9 | private static string getKey( string key ) => $"{PREFIX}:{key}"; 10 | 11 | /// 12 | /// Hard Beats 13 | /// 14 | public static readonly LocalisableString Beats = new TranslatableString( 15 | getKey( "beats" ), 16 | "Hard Beats" 17 | ); 18 | 19 | /// 20 | /// Multi-Streams 21 | /// 22 | public static readonly LocalisableString MultiStreams = new TranslatableString( 23 | getKey( "multi-streams" ), 24 | "Multi-Streams" 25 | ); 26 | 27 | /// 28 | /// Packets 29 | /// 30 | public static readonly LocalisableString Packets = new TranslatableString( 31 | getKey( "packets" ), 32 | "Packets" 33 | ); 34 | 35 | /// 36 | /// Streams 37 | /// 38 | public static readonly LocalisableString Streams = new TranslatableString( 39 | getKey( "streams" ), 40 | "Streams" 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu.Tests/osu.Game.Rulesets.Solosu.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | osu.Game.Rulesets.Solosu.Tests.VisualTestRunner 4 | 5 | 6 | 7 | 8 | 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | WinExe 23 | net6.0 24 | osu.Game.Rulesets.Solosu.Tests 25 | preview 26 | 27 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/UI/BeatDetector.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Audio.Track; 2 | using osu.Framework.Utils; 3 | using osu.Game.Beatmaps.ControlPoints; 4 | using osu.Game.Graphics.Containers; 5 | using osuTK; 6 | using System.Collections.Generic; 7 | 8 | namespace osu.Game.Rulesets.Solosu.UI { 9 | public class BeatDetector : BeatSyncedContainer { 10 | public delegate void BeatEvent ( int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes ); 11 | public event BeatEvent OnBeat; 12 | protected override void OnNewBeat ( int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes ) { 13 | vectors.Clear(); 14 | floats.Clear(); 15 | 16 | base.OnNewBeat( beatIndex, timingPoint, effectPoint, amplitudes ); 17 | OnBeat?.Invoke( beatIndex, timingPoint, effectPoint, amplitudes ); 18 | } 19 | 20 | private List vectors = new(); 21 | public Vector2 RandomVector ( int index ) { 22 | while ( index >= vectors.Count ) { 23 | vectors.Add( new Vector2( RNG.NextSingle( -1, 1 ), RNG.NextSingle( -1, 1 ) ).Normalized() ); 24 | } 25 | 26 | return vectors[ index ]; 27 | } 28 | private List floats = new(); 29 | public float RandomFloat ( int index ) { 30 | while ( index >= floats.Count ) { 31 | floats.Add( RNG.NextSingle() ); 32 | } 33 | 34 | return floats[ index ]; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/UI/DrawableSolosuRuleset.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Input; 3 | using osu.Game.Beatmaps; 4 | using osu.Game.Input.Handlers; 5 | using osu.Game.Replays; 6 | using osu.Game.Rulesets.Mods; 7 | using osu.Game.Rulesets.Objects.Drawables; 8 | using osu.Game.Rulesets.Solosu.Objects; 9 | using osu.Game.Rulesets.Solosu.Replays; 10 | using osu.Game.Rulesets.UI; 11 | using osu.Game.Scoring; 12 | using System.Collections.Generic; 13 | 14 | namespace osu.Game.Rulesets.Solosu.UI { 15 | [Cached] 16 | public class DrawableSolosuRuleset : DrawableRuleset { 17 | public DrawableSolosuRuleset ( SolosuRuleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null ) : base( ruleset, beatmap, mods ) { } 18 | 19 | protected override Playfield CreatePlayfield () { 20 | var playfield = new SolosuPlayfield(); 21 | playfield.replay.replay = new SolosuAutoGenerator( Beatmap ).Generate(); 22 | return playfield; 23 | } 24 | protected override ReplayInputHandler CreateReplayInputHandler ( Replay replay ) => new SolosuFramedReplayInputHandler( replay ); 25 | public override DrawableHitObject CreateDrawableRepresentation ( SolosuHitObject h ) => h.AsDrawable(); 26 | protected override PassThroughInputManager CreateInputManager () => new SolosuInputManager( Ruleset?.RulesetInfo ); 27 | protected override ReplayRecorder CreateReplayRecorder ( Score score ) 28 | => new SolosuReplayRecorder( score ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Collections/TimeSeekableList.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Collections { 7 | public class TimeSeekableList : IEnumerable<(double time, T value)> { 8 | private struct TimeEntry { 9 | public double Time; 10 | public T Value; 11 | } 12 | 13 | private List entries = new(); 14 | 15 | public void Add ( double time, T value ) { 16 | entries.Add( new TimeEntry { Time = time, Value = value } ); 17 | entries.Sort( ( a, b ) => Math.Sign( a.Time - b.Time ) ); 18 | } 19 | 20 | public void ClearAfter ( double time ) { 21 | entries.RemoveAll( x => x.Time > time ); 22 | } 23 | 24 | public T At ( double time ) 25 | => entries.Last( x => x.Time <= time ).Value; 26 | 27 | public bool AnyAfter ( double time ) 28 | => entries.Any() && entries.Last().Time > time; 29 | 30 | public IEnumerator<(double time, T value)> GetEnumerator () 31 | => entries.Select( x => (x.Time, x.Value) ).GetEnumerator(); 32 | IEnumerator IEnumerable.GetEnumerator () 33 | => GetEnumerator(); 34 | 35 | public IEnumerable<(double startTime, double endTime, T value)> Ranges ( double endTime = double.PositiveInfinity ) { 36 | var prev = entries.First(); 37 | foreach ( var i in entries.Skip( 1 ) ) { 38 | yield return (prev.Time, i.Time, prev.Value); 39 | prev = i; 40 | } 41 | yield return (prev.Time, endTime, prev.Value); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Replays/SolosuReplayFrame.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Beatmaps; 2 | using osu.Game.Replays.Legacy; 3 | using osu.Game.Rulesets.Replays; 4 | using osu.Game.Rulesets.Replays.Types; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace osu.Game.Rulesets.Solosu.Replays { 9 | public class SolosuReplayFrame : ReplayFrame, IConvertibleReplayFrame { 10 | public List Actions = new List(); 11 | 12 | public SolosuReplayFrame ( params SolosuAction[] buttons ) : this( buttons.AsEnumerable() ) { } 13 | public SolosuReplayFrame ( IEnumerable buttons ) { 14 | Actions.AddRange( buttons.Distinct() ); 15 | } 16 | 17 | public void FromLegacy ( LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null ) { 18 | Time = currentFrame.Time; 19 | if ( currentFrame.MouseRight1 ) Actions.Add( SolosuAction.Left ); 20 | if ( currentFrame.MouseRight2 ) Actions.Add( SolosuAction.Right ); 21 | if ( ( currentFrame.ButtonState & ReplayButtonState.Smoke ) != 0 ) Actions.Add( SolosuAction.Center ); 22 | } 23 | public LegacyReplayFrame ToLegacy ( IBeatmap beatmap ) { 24 | ReplayButtonState s = ReplayButtonState.None; 25 | if ( Actions.Contains( SolosuAction.Left ) ) s |= ReplayButtonState.Right1; 26 | if ( Actions.Contains( SolosuAction.Right ) ) s |= ReplayButtonState.Right2; 27 | if ( Actions.Contains( SolosuAction.Center ) ) s |= ReplayButtonState.Smoke; 28 | return new LegacyReplayFrame( Time, null, null, s ); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Drawables/DrawableSolosuHitObject.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Game.Rulesets.Objects.Drawables; 5 | using osu.Game.Rulesets.Scoring; 6 | using osu.Game.Rulesets.Solosu.UI; 7 | using System.Collections.Generic; 8 | 9 | namespace osu.Game.Rulesets.Solosu.Objects.Drawables { 10 | public abstract class DrawableSolosuHitObject : DrawableHitObject { 11 | public DrawableSolosuHitObject () : this( null ) { } 12 | public DrawableSolosuHitObject ( SolosuHitObject hitObject ) : base( hitObject ) { 13 | Anchor = Anchor.BottomCentre; 14 | AlwaysPresent = true; 15 | } 16 | private SolosuInputManager inputManager; 17 | protected SolosuInputManager InputManager => inputManager ??= ( GetContainingInputManager() as SolosuInputManager ); 18 | protected override double InitialLifetimeOffset => 3000; 19 | 20 | [Resolved] 21 | public SolosuColours Colours { get; private set; } 22 | [Resolved] 23 | public PlayerByte Player { get; private set; } 24 | [Resolved] 25 | public Dictionary Lanes { get; private set; } 26 | [Resolved( name: nameof( SolosuPlayfield.RelaxBindable ) )] 27 | public BindableBool RelaxBindable { get; private set; } 28 | public HitWindows HitWindows => HitObject.HitWindows; 29 | } 30 | 31 | public abstract class DrawableSolosuHitObject : DrawableSolosuHitObject where T : SolosuHitObject { 32 | new public T HitObject => base.HitObject as T; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Stream.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Beatmaps; 2 | using osu.Game.Beatmaps.ControlPoints; 3 | using osu.Game.Rulesets.Objects.Types; 4 | using osu.Game.Rulesets.Solosu.Replays; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading; 8 | 9 | namespace osu.Game.Rulesets.Solosu.Objects { 10 | public class Stream : LanedSolosuHitObject, IHasDuration, IFlowObject { 11 | public double EndTime { get; set; } 12 | 13 | public double Duration { 14 | get => EndTime - StartTime; 15 | set => EndTime = StartTime + value; 16 | } 17 | 18 | public IEnumerable CreateFlowObjects () { 19 | yield return new FlowObject( StartTime, EndTime, FlowObjectType.Dangerous, Lane ) { Source = this }; 20 | } 21 | 22 | public double BonusInterval { get; private set; } 23 | protected override void ApplyDefaultsToSelf ( ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty ) { 24 | base.ApplyDefaultsToSelf( controlPointInfo, difficulty ); 25 | BonusInterval = controlPointInfo.TimingPointAt( StartTime ).BeatLength; 26 | } 27 | 28 | protected override void CreateNestedHitObjects ( CancellationToken cancellationToken ) { 29 | cancellationToken.ThrowIfCancellationRequested(); 30 | 31 | int bonusCount = (int)Math.Floor( Duration / BonusInterval ); 32 | double paddingTime = ( Duration - ( bonusCount - 1 ) * BonusInterval ) / 2; 33 | 34 | for ( int i = 0; i < bonusCount; i++ ) { 35 | AddNested( new Bonus { StartTime = StartTime + paddingTime + i * BonusInterval, Lane = Lane } ); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Beatmaps/SolosuBeatmap.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Beatmaps; 2 | using osu.Game.Rulesets.Solosu.Objects; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Beatmaps { 7 | public class SolosuBeatmap : Beatmap { 8 | public override IEnumerable GetStatistics () { 9 | var packets = HitObjects.OfType(); 10 | if ( packets.Any() ) yield return new BeatmapStatistic { 11 | CreateIcon = () => new BeatmapStatisticIcon( BeatmapStatisticsIconType.Circles ), 12 | Name = Localisation.StatsStrings.Packets, 13 | Content = packets.Count().ToString() 14 | }; 15 | 16 | var streams = HitObjects.OfType(); 17 | if ( streams.Any() ) yield return new BeatmapStatistic { 18 | CreateIcon = () => new BeatmapStatisticIcon( BeatmapStatisticsIconType.Sliders ), 19 | Name = Localisation.StatsStrings.Streams, 20 | Content = streams.Count().ToString() 21 | }; 22 | 23 | var multistreams = HitObjects.OfType(); 24 | if ( multistreams.Any() ) yield return new BeatmapStatistic { 25 | CreateIcon = () => new BeatmapStatisticIcon( BeatmapStatisticsIconType.Spinners ), 26 | Name = Localisation.StatsStrings.MultiStreams, 27 | Content = multistreams.Count().ToString() 28 | }; 29 | 30 | var hardBeats = HitObjects.OfType(); 31 | if ( hardBeats.Any() ) yield return new BeatmapStatistic { 32 | CreateIcon = () => new BeatmapStatisticIcon( BeatmapStatisticsIconType.Circles ), 33 | Name = Localisation.StatsStrings.Beats, 34 | Content = hardBeats.Count().ToString() 35 | }; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Patterns/Pattern.cs: -------------------------------------------------------------------------------- 1 | namespace osu.Game.Rulesets.Solosu.Patterns { 2 | public class Pattern { 3 | public readonly string Source; 4 | /// 5 | /// Patterns are noted as follows: 6 | /// `A`, `B`, `C` are any distinct directions. 7 | /// `L`, `R`, `C` are left, right, center respectively. 8 | /// `_` is a gap. 9 | /// `(ABC)` is a group. It does nothing by itself, but allows more complex behaviours. 10 | /// `[ABC]` is an intercept group. It places an intecept at that position. This is the default group. 11 | /// `{ABC}` is a danger group. It places a danger at that position. 12 | /// `<ABC>` is a move group. It forces the player to move there. 13 | /// `(ABC)?` makes a group optinal. 14 | /// `(ABC)xN` makes a group repeat N times. 15 | /// `(ABC)xN-K` makes a group repeat between N and K times. 16 | /// `(ABC)+` makes a group repeat one or more times. 17 | /// `(ABC)?+` makes a group repeat zero or more times. 18 | /// `(ABC)xN+` makes a group repeat at least N times. 19 | /// `(ABC)!` makes a group faster. Stackable. 20 | /// `(ABC)#` makes a group slower. Stackable. 21 | /// It is not possible to place a `[]`, `{}` or `<>` group around any notes with already defined behaviours. 22 | /// `!` and `#` are exclusive to each other and affect speed as follows: 23 | /// `sqrt(2)^(!)` is the minimum note speed, `sqrt(2)^(!+1)` is the maximum 24 | /// `sqrt(2)^(-#)` is the maximum note speed, `sqrt(2)^(1-#)` is the minimum 25 | /// 26 | public Pattern ( string source ) { 27 | Source = source; 28 | } 29 | 30 | public override string ToString () 31 | => Source; 32 | 33 | public static implicit operator Pattern ( string source ) 34 | => new Pattern( source ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Drawables/DrawableHardBeat.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Shapes; 3 | using osu.Game.Rulesets.Objects; 4 | using osu.Game.Rulesets.Objects.Drawables; 5 | using osu.Game.Rulesets.Scoring; 6 | using System; 7 | 8 | namespace osu.Game.Rulesets.Solosu.Objects.Drawables { 9 | public class DrawableHardBeat : DrawableSolosuHitObject { 10 | Circle child; 11 | public DrawableHardBeat () { 12 | AddInternal( child = new Circle { 13 | Anchor = Anchor.BottomCentre, 14 | Origin = Anchor.BottomCentre, 15 | Width = 500, 16 | Height = 10 17 | } ); 18 | Origin = Anchor.BottomCentre; 19 | AutoSizeAxes = Axes.Y; 20 | } 21 | 22 | protected override void Update () { 23 | base.Update(); 24 | 25 | Y = -(float)( Lanes[ SolosuLane.Center ].HeightAtTime( Math.Min( Clock.CurrentTime, HitObject.GetEndTime() ), HitObject.StartTime, 1.4 ) ); 26 | } 27 | 28 | protected override void OnApply () { 29 | base.OnApply(); 30 | Colour = Colours.Regular; 31 | } 32 | 33 | protected override void CheckForResult ( bool userTriggered, double timeOffset ) { 34 | if ( timeOffset > 0 ) { 35 | ApplyResult( j => j.Type = HitResult.IgnoreHit ); 36 | } 37 | } 38 | 39 | protected override void UpdateInitialTransforms () { 40 | this.FadeIn( 500 ); 41 | } 42 | protected override void UpdateHitStateTransforms ( ArmedState state ) { 43 | if ( state == ArmedState.Hit ) { 44 | this.FadeOut( 150 ).FadeColour( Colours.ColourFor( Result.Type ), 100 ); 45 | child.ResizeHeightTo( 100, 150, Easing.Out ); 46 | } 47 | else if ( state == ArmedState.Miss ) { 48 | this.FadeOut( 500 ).FadeColour( Colours.Miss, 200 ); 49 | } 50 | 51 | LifetimeEnd = HitStateUpdateTime + 500; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/SolosuTextures.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Rendering; 2 | using osu.Framework.Graphics.Textures; 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.PixelFormats; 5 | using System; 6 | 7 | namespace osu.Game.Rulesets.Solosu { 8 | public static class SolosuTextures { 9 | public static Texture Generate ( IRenderer renderer, int width, int height, Func generator ) { 10 | Image image = new Image( width, height ); 11 | image.ProcessPixelRows( rows => { 12 | for ( int y = 0; y < height; y++ ) { 13 | var span = rows.GetRowSpan( y ); 14 | for ( int x = 0; x < width; x++ ) { 15 | span[ x ] = generator( x, y ); 16 | } 17 | } 18 | } ); 19 | 20 | Texture texture = renderer.CreateTexture( width, height, true ); 21 | texture.SetData( new TextureUpload( image ) ); 22 | return texture; 23 | } 24 | public static Texture GeneratePercentile ( IRenderer renderer, int width, int height, Func generator ) 25 | => Generate( renderer, width, height, ( x, y ) => generator( (double)x / width, (double)y / height ) ); 26 | public static Texture GenerateMirroredPercentile ( IRenderer renderer, int width, int height, Func generator ) 27 | => Generate( renderer, width, height, ( x, y ) => generator( 1 - Math.Abs( 1 - x * 2d / width ), 1 - Math.Abs( 1 - y * 2d / height ) ) ); 28 | public static Texture WidthFade ( IRenderer renderer, int width, int height ) 29 | => Generate( renderer, width, height, ( x, y ) => new Rgba32( 255, 255, 255, (byte)( 255 - MathF.Abs( 1 - 2f * x / width ) * 255 ) ) ); 30 | 31 | public static Texture FadeLeft ( IRenderer renderer, int width, int height ) 32 | => Generate( renderer, width, height, ( x, y ) => new Rgba32( 255, 255, 255, (byte)( 255 * ( width - x ) / width ) ) ); 33 | 34 | static Texture cachedFadeLeft; 35 | public static Texture CachedFadeLeft ( IRenderer renderer ) => cachedFadeLeft ??= FadeLeft( renderer, 100, 100 ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Action.Strings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Center 31 | Left 32 | Right 33 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Action.Strings.pl.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Środek 31 | Lewo 32 | Prawo 33 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Action.Strings.ru.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089В центр 31 | Влево 32 | Вправо 33 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Mod.Strings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Let the cute bot do... wait, where did he go? 31 | TCP at it's finest 32 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Mod.Strings.pl.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Pozwól słodziaczkobotowi... czekaj, gdzie on jest? 31 | Nic nie przebije TCP 32 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Mod.Strings.ru.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Славный бот сдел… постой, куда он делся? 31 | Протокол TCP на максималках 32 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Judgement.Strings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Nice 31 | Corrupt 32 | Ok 33 | Perfect 34 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Judgement.Strings.pl.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Nieźle 31 | Błąd 32 | Ok 33 | Idealnie 34 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Judgement.Strings.ru.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Хорошо 31 | Повреждение 32 | Ок 33 | Идеально 34 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Stats.Strings.pl.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Hard Beaty 31 | Multi-Streamy 32 | Pakiety 33 | Streamy 34 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Stats.Strings.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Hard Beats 31 | Multi-Streams 32 | Packets 33 | Streams 34 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Localisation/Stats.Strings.ru.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | text/microsoft-resx1.3System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089Громкие биты 31 | Мультистримы 32 | Пакеты 33 | Стримы 34 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/UI/Lane.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Game.Rulesets.Solosu.Objects; 5 | using osu.Game.Rulesets.Solosu.Objects.Drawables; 6 | using osu.Game.Rulesets.UI; 7 | using static osu.Game.Rulesets.Solosu.UI.SolosuPlayfield; 8 | 9 | namespace osu.Game.Rulesets.Solosu.UI { 10 | [Cached] 11 | public class Lane : Playfield { 12 | public Lane () { 13 | AddRangeInternal( new Drawable[] { 14 | HitObjectContainer, 15 | } ); 16 | 17 | HitObjectContainer.Origin = Anchor.BottomCentre; 18 | HitObjectContainer.Anchor = Anchor.BottomCentre; 19 | 20 | RegisterPool( 20 ); 21 | RegisterPool( 5 ); 22 | RegisterPool( 30 ); 23 | } 24 | 25 | 26 | [Resolved( name: nameof( SolosuPlayfield.ScrollDuration ) )] 27 | public BindableDouble ScrollDuration { get; private set; } 28 | [Resolved( name: nameof( SolosuPlayfield.HitHeight ) )] 29 | public BindableDouble HitHeight { get; private set; } 30 | [Resolved( name: nameof( SolosuPlayfield.KiaiBindable ) )] 31 | public BindableBool KiaiBindable { get; private set; } 32 | 33 | public readonly BindableDouble LazerSpeed = new( 1 ); 34 | 35 | protected override void Update () { 36 | base.Update(); 37 | if ( Time.Elapsed < 0 ) { 38 | FinishTransforms(); 39 | ClearTransformsAfter( Time.Current ); 40 | } 41 | } 42 | 43 | [BackgroundDependencyLoader] 44 | private void load () { 45 | KiaiBindable.BindValueChanged( v => { 46 | if ( v.NewValue ) { 47 | this.TransformBindableTo( LazerSpeed, 2, 400 ); 48 | } 49 | else { 50 | this.TransformBindableTo( LazerSpeed, 1, 400 ); 51 | } 52 | } ); 53 | } 54 | 55 | public double TimeAtHeight ( double height, double hitTime, double speed = 1 ) 56 | => hitTime - ( height - HitHeight.Value ) * ScrollDuration.Value / SCROLL_HEIGHT / speed; 57 | 58 | public double HeightAtTime ( double time, double hitTime, double speed = 1 ) { 59 | var position = ( hitTime - time ) / ScrollDuration.Value; 60 | var height = speed * SCROLL_HEIGHT * position; 61 | return height + HitHeight.Value; 62 | } 63 | 64 | public double CubeHeight => DrawHeight - 150 / 2 - 70; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/MultiLaneStream.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Beatmaps; 2 | using osu.Game.Beatmaps.ControlPoints; 3 | using osu.Game.Rulesets.Objects.Types; 4 | using osu.Game.Rulesets.Solosu.Collections; 5 | using osu.Game.Rulesets.Solosu.Replays; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading; 10 | 11 | namespace osu.Game.Rulesets.Solosu.Objects { 12 | public class MultiLaneStream : SolosuHitObject, IHasDuration, IFlowObject { // NOTE perhaps this should be in between 2 lanes and block movement between them so its unique from regular streams 13 | public double EndTime { get; set; } 14 | 15 | public double Duration { 16 | get => EndTime - StartTime; 17 | set => EndTime = StartTime + value; 18 | } 19 | 20 | public TimeSeekableList Lanes = new(); 21 | 22 | public MultiLaneStream Randomize ( Random random, double frequency ) { 23 | double time = StartTime; 24 | double timePerSwap = 1 / frequency; 25 | 26 | void Next () { 27 | Lanes.Add( time, random.FromEnum() ); 28 | time += timePerSwap; 29 | } 30 | 31 | do { 32 | Next(); 33 | } while ( time < EndTime - timePerSwap ); 34 | 35 | return this; 36 | } 37 | 38 | public IEnumerable CreateFlowObjects () { 39 | return Lanes.Ranges( EndTime ).Select( x => new FlowObject( x.startTime, x.endTime, FlowObjectType.Dangerous, x.value ) { Source = new Stream { StartTime = x.startTime, EndTime = x.endTime, Lane = x.value } } ); 40 | } 41 | 42 | public double BonusInterval { get; private set; } 43 | protected override void ApplyDefaultsToSelf ( ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty ) { 44 | base.ApplyDefaultsToSelf( controlPointInfo, difficulty ); 45 | BonusInterval = controlPointInfo.TimingPointAt( StartTime ).BeatLength; 46 | } 47 | 48 | protected override void CreateNestedHitObjects ( CancellationToken cancellationToken ) { 49 | cancellationToken.ThrowIfCancellationRequested(); 50 | 51 | int bonusCount = (int)Math.Floor( Duration / BonusInterval ); 52 | double paddingTime = ( Duration - ( bonusCount - 1 ) * BonusInterval ) / 2; 53 | 54 | for ( int i = 0; i < bonusCount; i++ ) { 55 | var time = StartTime + paddingTime + i * BonusInterval; 56 | AddNested( new Bonus { StartTime = time, Lane = Lanes.At( time ) } ); 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Drawables/DrawablePacketJudgement.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Audio.Track; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Pooling; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.Utils; 7 | using osu.Game.Beatmaps.ControlPoints; 8 | using osu.Game.Rulesets.Judgements; 9 | using osu.Game.Rulesets.Solosu.UI; 10 | using osuTK; 11 | 12 | namespace osu.Game.Rulesets.Solosu.Objects.Drawables { 13 | public class DrawablePacketJudgement : PoolableDrawable { 14 | public override bool RemoveCompletedTransforms => false; 15 | [Resolved] 16 | SolosuColours colours { get; set; } 17 | 18 | DrawableSolosuHitObject dho; 19 | JudgementResult judgement; 20 | 21 | [Resolved] 22 | BeatDetector beat { get; set; } 23 | 24 | public DrawablePacketJudgement () { 25 | AddInternal( new Circle { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2( 8 ) } ); 26 | } 27 | 28 | [BackgroundDependencyLoader] 29 | private void load () { 30 | beat.OnBeat += OnBeat; 31 | } 32 | 33 | bool isSent; 34 | int beatsToGo; 35 | private void OnBeat ( int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes ) { 36 | if ( !IsInUse || isSent ) return; 37 | beatsToGo--; 38 | if ( beatsToGo > 0 ) return; 39 | isSent = true; 40 | this.MoveTo( new Vector2( 0, 150 / 2 + 70 ), 200, Easing.In ).Delay( 100 ).FadeOut( 100 ).Then().Expire(); 41 | } 42 | 43 | public void Apply ( DrawableSolosuHitObject dho, JudgementResult judgement ) { 44 | this.dho = dho; 45 | this.judgement = judgement; 46 | } 47 | 48 | protected override void PrepareForUse () { 49 | ClearTransforms( true ); 50 | Anchor = Anchor.TopCentre; 51 | Origin = Anchor.Centre; 52 | isSent = false; 53 | beatsToGo = 2; 54 | 55 | applyHitAnimations(); 56 | } 57 | 58 | Vector2 offset; 59 | protected override void Update () { 60 | if ( !IsInUse || isSent ) return; 61 | if ( LatestTransformEndTime <= Clock.CurrentTime ) { 62 | var shake = 25; 63 | this.MoveTo( offset + new Vector2( RNG.NextSingle( -shake, shake ), RNG.NextSingle( -shake, shake ) ), 200, Easing.InOutCubic ); 64 | } 65 | } 66 | 67 | private void applyHitAnimations () { 68 | Colour = colours.ColourFor( judgement.Type ); 69 | Position = dho.ToSpaceOfOtherDrawable( Vector2.Zero, Parent ) - new Vector2( Parent.DrawWidth / 2, 0 ); 70 | Alpha = 0; 71 | 72 | offset = new Vector2( ( dho.HitObject.PieceDirection == LeftRight.Right ? 350 : -350 ), Position.Y - 140 ); 73 | 74 | this.FadeTo( 0.6f, 60 ).MoveTo( offset, 200, Easing.Out ); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Extensions.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Beatmaps; 2 | using osu.Game.Rulesets.Solosu.Objects; 3 | using osuTK; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace osu.Game.Rulesets.Solosu { 9 | public static class Extensions { 10 | public static bool Chance ( this Random random, double chance ) => random.NextDouble() < chance; 11 | public static T From ( this Random random, IEnumerable collection ) { 12 | var array = collection.ToArray(); 13 | return array[ random.Next( 0, array.Length ) ]; 14 | } 15 | public static T[] From ( this Random random, IEnumerable collection, int count ) { 16 | var array = collection.ToList(); 17 | T[] result = new T[ count ]; 18 | for ( ; count > 0; count-- ) { 19 | int index = random.Next( array.Count ); 20 | result[ count - 1 ] = array[ index ]; 21 | array.RemoveAt( index ); 22 | } 23 | return result; 24 | } 25 | public static T FromEnum ( this Random random ) where T : Enum 26 | => random.From( Enum.Values ); 27 | public static T[] FromEnum ( this Random random, int count ) where T : Enum 28 | => random.From( Enum.Values, count ); 29 | 30 | public static double Range ( this Random random, double min, double max ) 31 | => min + ( max - min ) * random.NextDouble(); 32 | 33 | public static double NormalizedOffset ( this SolosuLane lane ) { 34 | if ( lane == SolosuLane.Center ) return 0; 35 | if ( lane == SolosuLane.Left ) return -1; 36 | if ( lane == SolosuLane.Right ) return 1; 37 | return 0; 38 | } 39 | 40 | public static SolosuLane GetLane ( this SolosuAction action ) { 41 | if ( action == SolosuAction.Left ) return SolosuLane.Left; 42 | else if ( action == SolosuAction.Right ) return SolosuLane.Right; 43 | else if ( action == SolosuAction.Center ) return SolosuLane.Center; 44 | else throw new InvalidOperationException( $"{action} is not {SolosuAction.Left}, {SolosuAction.Center} nor {SolosuAction.Right}" ); 45 | } 46 | 47 | public static SolosuAction GetAction ( this SolosuLane lane ) { 48 | if ( lane == SolosuLane.Left ) return SolosuAction.Left; 49 | else if ( lane == SolosuLane.Right ) return SolosuAction.Right; 50 | else if ( lane == SolosuLane.Center ) return SolosuAction.Center; 51 | else throw new InvalidOperationException( $"{lane} is not {SolosuLane.Left}, {SolosuLane.Center} nor {SolosuLane.Right}" ); 52 | } 53 | 54 | public static bool IsMovement ( this SolosuAction action ) 55 | => action is SolosuAction.Left or SolosuAction.Right or SolosuAction.Center; 56 | public static bool IsAction ( this SolosuAction action ) 57 | => false; 58 | 59 | public static bool IsEmpty ( this IEnumerable self ) 60 | => !self.Any(); 61 | 62 | public static double LerpTo ( this double from, double to, double progress ) 63 | => from + ( to - from ) * progress; 64 | public static Vector2 LerpTo ( this Vector2 from, Vector2 to, double progress ) 65 | => from + ( to - from ) * (float)progress; 66 | 67 | public static Random Random ( this IBeatmap beatmap ) 68 | => new Random( (int)( beatmap.BeatmapInfo.Length + beatmap.BeatmapInfo.ID.GetHashCode() + beatmap.BeatmapInfo.AudioLeadIn * beatmap.BeatmapInfo.BeatDivisor / beatmap.BeatmapInfo.BPM ) ); 69 | 70 | public delegate void Mutator ( ref T value ); 71 | public static IEnumerable Mutate ( this IEnumerable self, Mutator mutator ) { 72 | return self.Select( x => { mutator( ref x ); return x; } ); 73 | } 74 | } 75 | 76 | public static class Enum where T : Enum { 77 | public static IEnumerable Values => Enum.GetValues( typeof( T ) ).OfType(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Beatmaps/SolosuBeatmapConverter.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Extensions.IEnumerableExtensions; 2 | using osu.Game.Beatmaps; 3 | using osu.Game.Rulesets.Objects; 4 | using osu.Game.Rulesets.Objects.Types; 5 | using osu.Game.Rulesets.Solosu.Objects; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading; 10 | 11 | namespace osu.Game.Rulesets.Solosu.Beatmaps { 12 | public class SolosuBeatmapConverter : BeatmapConverter { 13 | Random laneRandom; 14 | Random bonusRandom; 15 | public SolosuBeatmapConverter ( IBeatmap beatmap, Ruleset ruleset ) : base( beatmap, ruleset ) { 16 | laneRandom = beatmap.Random(); 17 | bonusRandom = beatmap.Random(); 18 | } 19 | 20 | public override bool CanConvert () => true; 21 | bool isKiai = false; 22 | SolosuLane[] possibleLanes = new[] { SolosuLane.Center }; 23 | 24 | protected override IEnumerable ConvertHitObject ( HitObject original, IBeatmap beatmap, CancellationToken cancellationToken ) { // currently lazer lets you only convert std beatmaps 25 | cancellationToken.ThrowIfCancellationRequested(); // TODO aspire maps overlay packets on top of streams. do something to make that playable 26 | 27 | // this way of converting maximizes movement to the beat for lazers and generates random patterns for packets 28 | if ( original is IHasDuration bonus and not ( IHasPath or IHasPathWithRepeats ) ) { 29 | MultiLaneStream next; 30 | yield return next = new MultiLaneStream { 31 | Samples = original.Samples, 32 | StartTime = original.StartTime, 33 | EndTime = bonus.EndTime 34 | }.Randomize( bonusRandom, Beatmap.ControlPointInfo.TimingPointAt( original.StartTime ).BPM / 60 / 1000 ); 35 | possibleLanes = Enum.Values.Except( next.Lanes.Last().value.Yield() ).ToArray(); 36 | } 37 | else if ( original is IHasDuration dur ) { 38 | if ( possibleLanes.Length > 1 || laneRandom.Chance( 0.4 ) ) { 39 | bool setSamples = true; 40 | IEnumerable danger; 41 | if ( possibleLanes.Length == 1 ) { 42 | danger = new[] { possibleLanes[ 0 ], laneRandom.From( Enum.Values.Except( possibleLanes ) ) }; 43 | } 44 | else if ( possibleLanes.Length == 2 ) { 45 | danger = possibleLanes; 46 | } 47 | else { 48 | throw new InvalidOperationException( "How?" ); 49 | } 50 | 51 | foreach ( var i in danger ) { 52 | var str = new Stream { 53 | StartTime = original.StartTime, 54 | EndTime = dur.EndTime, 55 | Lane = i 56 | }; 57 | if ( setSamples ) { 58 | setSamples = false; 59 | str.Samples = original.Samples; 60 | } 61 | yield return str; 62 | } 63 | possibleLanes = Enum.Values.Except( danger ).ToArray(); 64 | } 65 | else { // there is only 1 free lane 66 | yield return new Stream { 67 | Samples = original.Samples, 68 | StartTime = original.StartTime, 69 | EndTime = original.StartTime + dur.Duration * 0.9, 70 | Lane = possibleLanes[ 0 ] 71 | }; 72 | 73 | possibleLanes = Enum.Values.Except( possibleLanes[ 0 ].Yield() ).ToArray(); 74 | } 75 | } 76 | else { 77 | Packet next; 78 | yield return next = new Packet { 79 | Samples = original.Samples, 80 | StartTime = original.StartTime, 81 | Lane = laneRandom.FromEnum() 82 | }; 83 | 84 | possibleLanes = new[] { next.Lane }; 85 | } 86 | 87 | if ( isKiai != original.Kiai ) { 88 | isKiai = !isKiai; 89 | yield return new HardBeat { 90 | // TODO samples 91 | StartTime = original.StartTime 92 | }; 93 | } 94 | } 95 | 96 | protected override Beatmap CreateBeatmap () 97 | => new SolosuBeatmap(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Drawables/DrawableStream.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Audio.Track; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Pooling; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Game.Beatmaps.ControlPoints; 7 | using osu.Game.Rulesets.Objects.Drawables; 8 | using osu.Game.Rulesets.Scoring; 9 | using osu.Game.Rulesets.Solosu.UI; 10 | using osuTK; 11 | using System; 12 | using System.Collections.Generic; 13 | 14 | namespace osu.Game.Rulesets.Solosu.Objects.Drawables { 15 | public class DrawableStream : DrawableLanedSolosuHitObject { 16 | StreamVisual main; 17 | StreamVisual r; 18 | StreamVisual g; 19 | StreamVisual b; 20 | 21 | public DrawableStream () { 22 | AddInternal( r = new StreamVisual { Colour = Colour4.Red, Alpha = 0.4f } ); 23 | AddInternal( g = new StreamVisual { Colour = Colour4.Green, Alpha = 0.4f } ); 24 | AddInternal( b = new StreamVisual { Colour = Colour4.Blue, Alpha = 0.4f } ); 25 | AddInternal( main = new StreamVisual() ); 26 | Origin = Anchor.BottomCentre; 27 | Anchor = Anchor.BottomCentre; 28 | Width = 15; 29 | } 30 | 31 | protected override void OnApply () { 32 | Alpha = 0; 33 | main.Colour = Colours.Miss; 34 | damageTime = 0; 35 | } 36 | List bonuses = new(); 37 | protected override void OnFree () { 38 | base.OnFree(); 39 | foreach ( var i in bonuses ) RemoveInternal( i, disposeImmediately: false ); 40 | bonuses.Clear(); 41 | } 42 | protected override void AddNestedHitObject ( DrawableHitObject hitObject ) { 43 | bonuses.Add( hitObject as DrawableBonus ); 44 | AddInternal( hitObject ); 45 | } 46 | 47 | BeatDetector beat; 48 | [BackgroundDependencyLoader] 49 | private void load ( BeatDetector beat ) { 50 | this.beat = beat; 51 | beat.OnBeat += OnBeat; 52 | } 53 | 54 | private void OnBeat ( int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes ) { 55 | // thanks, hishi ;)))))) 56 | if ( effectPoint.KiaiMode ) { 57 | float sickoMode = 30 * amplitudes.Average * beat.RandomFloat( 0 ); 58 | 59 | r.MoveToOffset( beat.RandomVector( 0 ) * sickoMode, 50 ).Then().MoveTo( Vector2.Zero, 100 ); 60 | g.MoveToOffset( beat.RandomVector( 1 ) * sickoMode, 50 ).Then().MoveTo( Vector2.Zero, 100 ); 61 | b.MoveToOffset( beat.RandomVector( 2 ) * sickoMode, 50 ).Then().MoveTo( Vector2.Zero, 100 ); 62 | } 63 | } 64 | 65 | protected override void Update () { 66 | base.Update(); 67 | var a = Lane.HeightAtTime( Clock.CurrentTime, HitObject.StartTime, Lane.LazerSpeed.Value ); 68 | var b = Lane.HeightAtTime( Clock.CurrentTime, HitObject.EndTime, Lane.LazerSpeed.Value ); 69 | 70 | Y = -(float)a; 71 | Height = MathF.Max( (float)( b - a ), 10 ); // imagine putting a 0 length hold in your map lmao couldnt be me 72 | } 73 | 74 | double damageTime; 75 | protected override void CheckForResult ( bool userTriggered, double timeOffset ) { 76 | if ( timeOffset + HitObject.Duration >= 0 ) { 77 | if ( timeOffset >= 0 ) 78 | if ( damageTime > HitObject.Duration * 0.1 ) 79 | ApplyResult( j => j.Type = HitResult.Miss ); 80 | else if ( damageTime > HitObject.Duration * 0.05 ) 81 | ApplyResult( j => j.Type = HitResult.Ok ); 82 | else if ( damageTime > 0 ) 83 | ApplyResult( j => j.Type = HitResult.Great ); 84 | else 85 | ApplyResult( j => j.Type = HitResult.Perfect ); 86 | else if ( Player.Lane == HitObject.Lane ) { 87 | damageTime += Time.Elapsed; 88 | if ( damageTime > HitObject.Duration * 0.1 ) 89 | ApplyResult( j => j.Type = HitResult.Miss ); 90 | } 91 | } 92 | } 93 | 94 | protected override void UpdateInitialTransforms () { 95 | this.FadeIn( 100 ); 96 | } 97 | 98 | protected override void UpdateHitStateTransforms ( ArmedState state ) { 99 | const double fadeDuration = 400; 100 | this.FadeOut( fadeDuration ); 101 | 102 | LifetimeEnd = HitStateUpdateTime + fadeDuration; 103 | } 104 | 105 | private class StreamVisual : PoolableDrawable { 106 | public StreamVisual () { 107 | Origin = Anchor.BottomCentre; 108 | Anchor = Anchor.BottomCentre; 109 | RelativeSizeAxes = Axes.Both; 110 | 111 | AddInternal( new Circle { RelativeSizeAxes = Axes.Both } ); 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29123.88 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "osu.Game.Rulesets.Solosu", "osu.Game.Rulesets.Solosu\osu.Game.Rulesets.Solosu.csproj", "{5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "osu.Game.Rulesets.Solosu.Tests", "osu.Game.Rulesets.Solosu.Tests\osu.Game.Rulesets.Solosu.Tests.csproj", "{B4577C85-CB83-462A-BCE3-22FFEB16311D}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | VisualTests|Any CPU = VisualTests|Any CPU 15 | EndGlobalSection 16 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 17 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.Release|Any CPU.Build.0 = Release|Any CPU 21 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.ActiveCfg = Release|Any CPU 22 | {5AE1F0F1-DAFA-46E7-959C-DA233B7C87E9}.VisualTests|Any CPU.Build.0 = Release|Any CPU 23 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.ActiveCfg = Debug|Any CPU 28 | {B4577C85-CB83-462A-BCE3-22FFEB16311D}.VisualTests|Any CPU.Build.0 = Debug|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(NestedProjects) = preSolution 34 | EndGlobalSection 35 | GlobalSection(ExtensibilityGlobals) = postSolution 36 | SolutionGuid = {671B0BEC-2403-45B0-9357-2C97CC517668} 37 | EndGlobalSection 38 | GlobalSection(MonoDevelopProperties) = preSolution 39 | Policies = $0 40 | $0.TextStylePolicy = $1 41 | $1.EolMarker = Windows 42 | $1.inheritsSet = VisualStudio 43 | $1.inheritsScope = text/plain 44 | $1.scope = text/x-csharp 45 | $0.CSharpFormattingPolicy = $2 46 | $2.IndentSwitchSection = True 47 | $2.NewLinesForBracesInProperties = True 48 | $2.NewLinesForBracesInAccessors = True 49 | $2.NewLinesForBracesInAnonymousMethods = True 50 | $2.NewLinesForBracesInControlBlocks = True 51 | $2.NewLinesForBracesInAnonymousTypes = True 52 | $2.NewLinesForBracesInObjectCollectionArrayInitializers = True 53 | $2.NewLinesForBracesInLambdaExpressionBody = True 54 | $2.NewLineForElse = True 55 | $2.NewLineForCatch = True 56 | $2.NewLineForFinally = True 57 | $2.NewLineForMembersInObjectInit = True 58 | $2.NewLineForMembersInAnonymousTypes = True 59 | $2.NewLineForClausesInQuery = True 60 | $2.SpacingAfterMethodDeclarationName = False 61 | $2.SpaceAfterMethodCallName = False 62 | $2.SpaceBeforeOpenSquareBracket = False 63 | $2.inheritsSet = Mono 64 | $2.inheritsScope = text/x-csharp 65 | $2.scope = text/x-csharp 66 | EndGlobalSection 67 | GlobalSection(MonoDevelopProperties) = preSolution 68 | Policies = $0 69 | $0.TextStylePolicy = $1 70 | $1.EolMarker = Windows 71 | $1.inheritsSet = VisualStudio 72 | $1.inheritsScope = text/plain 73 | $1.scope = text/x-csharp 74 | $0.CSharpFormattingPolicy = $2 75 | $2.IndentSwitchSection = True 76 | $2.NewLinesForBracesInProperties = True 77 | $2.NewLinesForBracesInAccessors = True 78 | $2.NewLinesForBracesInAnonymousMethods = True 79 | $2.NewLinesForBracesInControlBlocks = True 80 | $2.NewLinesForBracesInAnonymousTypes = True 81 | $2.NewLinesForBracesInObjectCollectionArrayInitializers = True 82 | $2.NewLinesForBracesInLambdaExpressionBody = True 83 | $2.NewLineForElse = True 84 | $2.NewLineForCatch = True 85 | $2.NewLineForFinally = True 86 | $2.NewLineForMembersInObjectInit = True 87 | $2.NewLineForMembersInAnonymousTypes = True 88 | $2.NewLineForClausesInQuery = True 89 | $2.SpacingAfterMethodDeclarationName = False 90 | $2.SpaceAfterMethodCallName = False 91 | $2.SpaceBeforeOpenSquareBracket = False 92 | $2.inheritsSet = Mono 93 | $2.inheritsScope = text/x-csharp 94 | $2.scope = text/x-csharp 95 | EndGlobalSection 96 | EndGlobal 97 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Drawables/DrawableMultiStream.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Pooling; 3 | using osu.Framework.Graphics.Shapes; 4 | using osu.Game.Rulesets.Objects.Drawables; 5 | using osu.Game.Rulesets.Scoring; 6 | using osu.Game.Rulesets.Solosu.UI; 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | namespace osu.Game.Rulesets.Solosu.Objects.Drawables { 11 | public class DrawableMultiStream : DrawableSolosuHitObject { 12 | public DrawableMultiStream () { 13 | Origin = Anchor.BottomCentre; 14 | RelativeSizeAxes = Axes.Y; 15 | } 16 | 17 | DrawablePool straightPool = new( 10 ); 18 | DrawablePool connectorPool = new( 9 ); 19 | List parts = new(); 20 | 21 | protected override void OnApply () { 22 | base.OnApply(); 23 | Colour = Colours.Miss; 24 | StraightMultiStreamVisual prev = null; 25 | foreach ( var i in HitObject.Lanes.Ranges( HitObject.EndTime ) ) { 26 | var straight = straightPool.Get( x => { x.Lane = Lanes[ i.value ]; x.LaneEnum = i.value; x.StartTime = i.startTime; x.EndTime = i.endTime; } ); 27 | parts.Add( straight ); 28 | AddInternal( straight ); 29 | 30 | if ( prev is not null ) { 31 | var connector = connectorPool.Get( x => { x.From = prev; x.To = straight; } ); 32 | parts.Add( connector ); 33 | AddInternal( connector ); 34 | } 35 | 36 | prev = straight; 37 | } 38 | } 39 | List bonuses = new(); 40 | protected override void OnFree () { 41 | base.OnFree(); 42 | foreach ( var i in parts ) { 43 | RemoveInternal( i, disposeImmediately: false ); 44 | } 45 | parts.Clear(); 46 | foreach ( var i in bonuses ) RemoveInternal( i, disposeImmediately: false ); 47 | bonuses.Clear(); 48 | } 49 | protected override void AddNestedHitObject ( DrawableHitObject hitObject ) { 50 | bonuses.Add( hitObject as DrawableBonus ); 51 | AddInternal( hitObject ); 52 | } 53 | 54 | protected override void UpdateInitialTransforms () { 55 | this.FadeInFromZero( 100 ); 56 | } 57 | 58 | protected override void CheckForResult ( bool userTriggered, double timeOffset ) { 59 | if ( timeOffset + HitObject.Duration >= 0 ) { 60 | if ( timeOffset >= 0 ) 61 | ApplyResult( j => j.Type = HitResult.Perfect ); 62 | else if ( Player.Lane == HitObject.Lanes.At( HitObject.EndTime + timeOffset ) ) 63 | ApplyResult( j => j.Type = HitResult.Miss ); 64 | } 65 | } 66 | 67 | protected override void UpdateHitStateTransforms ( ArmedState state ) { 68 | if ( state == ArmedState.Hit ) { 69 | this.FadeOutFromOne( 400 ); 70 | } 71 | else if ( state == ArmedState.Miss ) { 72 | this.FadeOutFromOne( 400 ); 73 | } 74 | } 75 | 76 | private class StraightMultiStreamVisual : PoolableDrawable { 77 | public SolosuLane LaneEnum; 78 | public Lane Lane; 79 | public double StartTime; 80 | public double EndTime; 81 | 82 | Drawable box; 83 | public StraightMultiStreamVisual () { 84 | AddInternal( box = new Box { RelativeSizeAxes = Axes.Both, Origin = Anchor.BottomCentre, Anchor = Anchor.BottomCentre } ); 85 | Anchor = Anchor.BottomCentre; 86 | Origin = Anchor.BottomCentre; 87 | Width = 15; 88 | } 89 | 90 | protected override void Update () { 91 | base.Update(); 92 | Height = MathF.Max( (float)( TopHeight - BottomHeight ), 10 ); 93 | Y = -(float)BottomHeight; 94 | X = Lane.X; 95 | } 96 | 97 | public double BottomHeight => Lane.HeightAtTime( Clock.CurrentTime, StartTime, Lane.LazerSpeed.Value ); 98 | public double TopHeight => Lane.HeightAtTime( Clock.CurrentTime, EndTime, Lane.LazerSpeed.Value ); 99 | } 100 | 101 | private class MultiStreamVisualConnector : PoolableDrawable { 102 | public StraightMultiStreamVisual From; 103 | public StraightMultiStreamVisual To; 104 | 105 | Drawable boxA; 106 | Drawable boxB; 107 | public MultiStreamVisualConnector () { 108 | Anchor = Anchor.BottomCentre; 109 | Origin = Anchor.BottomCentre; 110 | 111 | AddInternal( boxA = new Box { Origin = Anchor.CentreLeft, Anchor = Anchor.Centre, Width = 40, Height = 15 } ); 112 | AddInternal( boxB = new Box { Origin = Anchor.CentreLeft, Anchor = Anchor.Centre, Width = 40, Height = 15 } ); 113 | } 114 | 115 | protected override void Update () { 116 | base.Update(); 117 | 118 | boxA.Y = -(float)From.TopHeight + 15f / 2; 119 | boxB.Y = -(float)To.BottomHeight - 15f / 2; 120 | 121 | boxA.X = From.Lane.X; 122 | boxB.X = To.Lane.X; 123 | 124 | boxA.Rotation = (float)( Math.Atan2( boxB.Y - boxA.Y, boxB.X - boxA.X ) / Math.PI * 180 ); 125 | boxB.Rotation = (float)( Math.Atan2( boxA.Y - boxB.Y, boxA.X - boxB.X ) / Math.PI * 180 ); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/SolosuRuleset.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Sprites; 5 | using osu.Framework.Graphics.Textures; 6 | using osu.Framework.Input.Bindings; 7 | using osu.Framework.Localisation; 8 | using osu.Framework.Platform; 9 | using osu.Game.Beatmaps; 10 | using osu.Game.Rulesets.Difficulty; 11 | using osu.Game.Rulesets.Mods; 12 | using osu.Game.Rulesets.Replays.Types; 13 | using osu.Game.Rulesets.Scoring; 14 | using osu.Game.Rulesets.Solosu.Beatmaps; 15 | using osu.Game.Rulesets.Solosu.Mods; 16 | using osu.Game.Rulesets.Solosu.Replays; 17 | using osu.Game.Rulesets.Solosu.UI; 18 | using osu.Game.Rulesets.UI; 19 | using osu.Game.Scoring; 20 | using osu.Game.Screens.Ranking.Statistics; 21 | using System; 22 | using System.Collections.Generic; 23 | using System.Linq; 24 | 25 | namespace osu.Game.Rulesets.Solosu { 26 | public class SolosuRuleset : Ruleset { 27 | public override string ShortName => "solosu"; 28 | public override string Description => ShortName; 29 | public override string PlayingVerb => "glitching out"; 30 | 31 | public override DrawableRuleset CreateDrawableRulesetWith ( IBeatmap beatmap, IReadOnlyList mods = null ) => new DrawableSolosuRuleset( this, beatmap, mods ); 32 | 33 | public override IBeatmapConverter CreateBeatmapConverter ( IBeatmap beatmap ) => new SolosuBeatmapConverter( beatmap, this ); 34 | public override IBeatmapProcessor CreateBeatmapProcessor ( IBeatmap beatmap ) => new SolosuBeatmapProcessor( beatmap ); 35 | 36 | public override DifficultyCalculator CreateDifficultyCalculator ( IWorkingBeatmap beatmap ) => new SolosuDifficultyCalculator( RulesetInfo, beatmap ); 37 | public override IConvertibleReplayFrame CreateConvertibleReplayFrame () 38 | => new SolosuReplayFrame(); 39 | 40 | public override IEnumerable GetModsFor ( ModType type ) { // TODO autoclick and automovement mod 41 | switch ( type ) { 42 | case ModType.Automation: 43 | return new Mod[] { new SolosuModAutoplay() }; 44 | 45 | case ModType.DifficultyReduction: 46 | return new[] { new SolosuModNoFail() }; 47 | 48 | default: 49 | return Array.Empty(); 50 | } 51 | } 52 | 53 | 54 | public override IEnumerable GetDefaultKeyBindings ( int variant = 0 ) => new[] { 55 | new KeyBinding(InputKey.Left, SolosuAction.Left), 56 | new KeyBinding(InputKey.Up, SolosuAction.Center), 57 | new KeyBinding(InputKey.Right, SolosuAction.Right) 58 | }; 59 | 60 | public override Drawable CreateIcon () => new SolosuIcon( this ); 61 | 62 | private Dictionary results = new Dictionary { 63 | [ HitResult.Perfect ] = Localisation.JudgementStrings.Perfect, 64 | [ HitResult.Great ] = Localisation.JudgementStrings.Great, 65 | [ HitResult.Meh ] = Localisation.JudgementStrings.Ok, 66 | [ HitResult.Miss ] = Localisation.JudgementStrings.Miss 67 | }; 68 | 69 | protected override IEnumerable GetValidHitResults () 70 | => results.Keys; 71 | 72 | public override LocalisableString GetDisplayNameForHitResult ( HitResult result ) 73 | => results[ result ]; 74 | 75 | public override StatisticItem[] CreateStatisticsForScore ( ScoreInfo score, IBeatmap playableBeatmap ) => new StatisticItem[] 76 | { 77 | 78 | }; 79 | 80 | static Dictionary localisationHack = new(); 81 | public static Func LocalisationHackFactory; 82 | public static string GetLocalisedHack ( LocalisableString str ) { 83 | if ( !localisationHack.TryGetValue( str, out var bindable ) ) { 84 | if ( LocalisationHackFactory is null ) { 85 | return str.ToString(); 86 | } 87 | else { 88 | localisationHack.Add( str, bindable = LocalisationHackFactory( str ) ); 89 | } 90 | } 91 | 92 | return bindable.Value; 93 | } 94 | 95 | private class SolosuIcon : Sprite { 96 | private readonly Ruleset ruleset; 97 | [Resolved] 98 | private LocalisationManager localisation { get; set; } 99 | public SolosuIcon ( Ruleset ruleset ) { 100 | this.ruleset = ruleset; 101 | Size = new osuTK.Vector2( 40 ); 102 | FillMode = FillMode.Fit; 103 | Origin = Anchor.Centre; 104 | Anchor = Anchor.Centre; 105 | 106 | if ( LocalisationHackFactory is null ) { 107 | LocalisationHackFactory = str => { 108 | return localisation.GetLocalisedBindableString( str ); 109 | }; 110 | } 111 | } 112 | 113 | [BackgroundDependencyLoader] 114 | private void load ( TextureStore textures, GameHost host ) { 115 | if ( !textures.GetAvailableResources().Contains( "Textures/SolosuIcon.png" ) ) 116 | textures.AddTextureSource( host.CreateTextureLoaderStore( ruleset.CreateResourceStore() ) ); 117 | 118 | Texture = textures.Get( "Textures/SolosuIcon" ); 119 | } 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/UI/Wireframe.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Primitives; 4 | using osu.Framework.Graphics.Rendering; 5 | using osu.Framework.Graphics.Shaders; 6 | using osu.Framework.Graphics.Textures; 7 | using osu.Framework.Layout; 8 | using osuTK; 9 | using System; 10 | using System.Collections.Generic; 11 | 12 | namespace osu.Game.Rulesets.Solosu.UI { 13 | public class Wireframe : Drawable { 14 | public Quaternion Rotation3D; 15 | public Vector3 Offset; 16 | public Vector2 FOV = new Vector2( 120 ); 17 | public readonly List Lines = new(); 18 | 19 | protected override void Update () { 20 | base.Update(); 21 | 22 | Invalidate( Invalidation.MiscGeometry, InvalidationSource.Self ); 23 | } 24 | 25 | public Vector2 ToLocalSpace ( Vector3 pos ) { 26 | pos = ( Rotation3D * new Vector4( pos ) ).Xyz - Offset; 27 | 28 | return new Vector2( 29 | Size.X * MathF.Atan2( pos.X, pos.Z ) / ( FOV.X / 180 * MathF.PI ), 30 | Size.Y * MathF.Atan2( pos.Y, pos.Z ) / ( FOV.X / 180 * MathF.PI ) 31 | ); 32 | } 33 | 34 | public void AddLine ( Vertice from, Vertice to, int segments ) { 35 | for ( int i = 0; i < segments; i++ ) { 36 | float t1 = (float)i / segments; 37 | float t2 = (float)( i + 1 ) / segments; 38 | 39 | Lines.Add( new Line( from.Position + ( to.Position - from.Position ) * t1, from.Position + ( to.Position - from.Position ) * t2 ) ); 40 | } 41 | } 42 | 43 | [BackgroundDependencyLoader] 44 | private void load ( ShaderManager shaders ) { 45 | TextureShader = shaders.Load( VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE ); 46 | } 47 | 48 | public IShader TextureShader { get; protected set; } 49 | 50 | protected override DrawNode CreateDrawNode () 51 | => new WireframeDrawNode( this ); 52 | 53 | private class WireframeDrawNode : DrawNode { 54 | private Wireframe wf; 55 | readonly List lines; 56 | Vector2 size; 57 | IShader shader; 58 | float width = 3; 59 | public WireframeDrawNode ( Wireframe source ) : base( source ) { 60 | wf = source; 61 | lines = wf.Lines; 62 | } 63 | 64 | public override void ApplyState () { 65 | base.ApplyState(); 66 | size = wf.Size; 67 | shader = wf.TextureShader; 68 | } 69 | 70 | public override void Draw ( IRenderer renderer ) { 71 | base.Draw( renderer ); 72 | 73 | shader.Bind(); 74 | 75 | for ( int i = 0; i < lines.Count; i++ ) { 76 | var line = lines[ i ]; 77 | 78 | var from = Project( line.From.Position ); 79 | var to = Project( line.To.Position ); 80 | var dif = ( from - to ).Normalized(); // make them a bit longer so they overlap 81 | from += dif; 82 | to -= dif; 83 | var perp = dif.PerpendicularLeft * width / 2; 84 | 85 | renderer.DrawQuad( renderer.WhitePixel, new Quad( from + perp, from - perp, to + perp, to - perp ), DrawColourInfo.Colour ); 86 | } 87 | 88 | shader.Unbind(); 89 | } 90 | protected override bool CanDrawOpaqueInterior => false; 91 | 92 | Vector2 Project ( Vector3 pos ) { 93 | pos = ( wf.Rotation3D * new Vector4( pos ) ).Xyz - wf.Offset; 94 | 95 | return ToScreen( new Vector2( 96 | size.X * ( 0.5f + MathF.Atan2( pos.X, pos.Z ) / ( wf.FOV.X / 180 * MathF.PI ) ), 97 | size.Y * ( 0.5f + MathF.Atan2( pos.Y, pos.Z ) / ( wf.FOV.Y / 180 * MathF.PI ) ) 98 | ) ); 99 | } 100 | 101 | Vector2 ToScreen ( Vector2 pos ) 102 | => Vector2Extensions.Transform( pos, DrawInfo.Matrix ); 103 | } 104 | } 105 | 106 | public record Vertice { 107 | public readonly Vector3 Position; 108 | public Vertice ( Vector3 position ) { 109 | Position = position; 110 | } 111 | 112 | public static implicit operator Vertice ( Vector3 v ) 113 | => new Vertice( v ); 114 | } 115 | public record Line { 116 | public readonly Vertice From; 117 | public readonly Vertice To; 118 | public Line ( Vertice from, Vertice to ) { 119 | From = from; 120 | To = to; 121 | } 122 | 123 | public static implicit operator Line ( (Vertice a, Vertice b) v ) 124 | => new Line( v.a, v.b ); 125 | } 126 | 127 | public class Cube : Wireframe { 128 | public Cube () { 129 | Offset = new Vector3( 0, 0, -2 ); 130 | 131 | Vertice a = new Vector3( 1, 1, 1 ); 132 | Vertice b = new Vector3( -1, 1, 1 ); 133 | Vertice c = new Vector3( 1, -1, 1 ); 134 | Vertice d = new Vector3( 1, 1, -1 ); 135 | Vertice e = new Vector3( -1, -1, 1 ); 136 | Vertice f = new Vector3( -1, 1, -1 ); 137 | Vertice g = new Vector3( 1, -1, -1 ); 138 | Vertice h = new Vector3( -1, -1, -1 ); 139 | 140 | int seg = 20; 141 | 142 | AddLine( a, b, seg ); 143 | AddLine( a, c, seg ); 144 | AddLine( a, d, seg ); 145 | 146 | AddLine( b, e, seg ); 147 | AddLine( b, f, seg ); 148 | 149 | AddLine( c, e, seg ); 150 | AddLine( c, g, seg ); 151 | 152 | AddLine( d, f, seg ); 153 | AddLine( d, g, seg ); 154 | 155 | AddLine( e, h, seg ); 156 | AddLine( f, h, seg ); 157 | AddLine( g, h, seg ); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Objects/Drawables/DrawablePacket.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Audio.Track; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Graphics.Containers; 6 | using osu.Framework.Graphics.Shapes; 7 | using osu.Framework.Graphics.Transforms; 8 | using osu.Game.Beatmaps.ControlPoints; 9 | using osu.Game.Rulesets.Objects.Drawables; 10 | using osu.Game.Rulesets.Scoring; 11 | using osu.Game.Rulesets.Solosu.UI; 12 | using osuTK; 13 | using System; 14 | 15 | namespace osu.Game.Rulesets.Solosu.Objects.Drawables { 16 | public class DrawablePacket : DrawableLanedSolosuHitObject { 17 | PacketVisual main; 18 | 19 | public DrawablePacket () { 20 | AddRangeInternal( new Drawable[] { 21 | main = new PacketVisual( fillProgress ) 22 | } ); 23 | } 24 | 25 | BeatDetector beat; 26 | [BackgroundDependencyLoader] 27 | private void load ( BeatDetector beat ) { 28 | this.beat = beat; 29 | beat.OnBeat += OnBeat; 30 | } 31 | 32 | private void OnBeat ( int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes ) { 33 | // thanks, hishi ;)))))) 34 | if ( effectPoint.KiaiMode ) { 35 | float sickoMode = 10 * amplitudes.Average * beat.RandomFloat( 0 ); 36 | 37 | main.ScaleTo( 1 - sickoMode / 28, 50 ).Then().ScaleTo( 1, 100 ); 38 | } 39 | } 40 | 41 | protected override void Update () { 42 | base.Update(); 43 | var cubeTime = Lane.TimeAtHeight( Lane.CubeHeight, HitObject.StartTime ); 44 | 45 | var timeToApply = Math.Clamp( Clock.CurrentTime, cubeTime - 100, cubeTime + 500 ); 46 | if ( timeToApply != lastAppliedPositionalTransformTime ) { 47 | lastAppliedPositionalTransformTime = timeToApply; 48 | 49 | var fadeInProgress = Math.Clamp( ( timeToApply - cubeTime + 100 ) / 500, 0, 1 ); 50 | var xAndRotateProgress = Math.Clamp( ( timeToApply - cubeTime ) / ( 200 / SolosuPlayfield.SCROLL_MULTIPLIER ), 0, 1 ); 51 | 52 | Alpha = (float)fadeInProgress; 53 | X = -(float)( Lane.X * ( 1 - xAndRotateProgress ) ); 54 | Rotation = (float)( Math.Atan2( -20, -Lane.X ) * 180 / Math.PI + 90 ).LerpTo( 0, xAndRotateProgress ); 55 | } 56 | } 57 | 58 | protected double lastAppliedPositionalTransformTime { get; private set; } 59 | protected override void UpdateInitialTransforms () { 60 | lastAppliedPositionalTransformTime = TransformStartTime; 61 | } 62 | 63 | protected override void OnApply () { 64 | base.OnApply(); 65 | main.Colour = Colours.Regular; 66 | Size = new Vector2( 30 ); 67 | Origin = Anchor.Centre; 68 | 69 | fillProgress.Value = 0; 70 | Scale = Vector2.One; 71 | lastValidOffset = null; 72 | } 73 | 74 | double? lastValidOffset; 75 | protected override void CheckForResult ( bool userTriggered, double timeOffset ) { 76 | if ( lastValidOffset is null || Math.Abs( timeOffset ) <= Math.Abs( lastValidOffset.Value ) ) { // can get better result 77 | if ( Player.Lane == HitObject.Lane && HitWindows.ResultFor( timeOffset ) != HitResult.None ) lastValidOffset = timeOffset; 78 | } 79 | else { 80 | var result = HitWindows.ResultFor( lastValidOffset.Value ); 81 | ApplyResult( r => r.Type = result ); 82 | } 83 | 84 | if ( !HitWindows.CanBeHit( timeOffset ) ) { 85 | ApplyResult( r => r.Type = HitResult.Miss ); 86 | } 87 | } 88 | 89 | protected override void UpdateHitStateTransforms ( ArmedState state ) { 90 | const double fillDuration = 150; 91 | const double delayDuration = 150; 92 | const double fadeDuration = 150; 93 | 94 | if ( state == ArmedState.Hit ) { 95 | main.FadeColour( Colours.ColourFor( Result.Type ), fillDuration ); 96 | FillTo( 1, fillDuration ) 97 | .Then().Delay( delayDuration ) 98 | .FadeOut( fadeDuration ).ScaleTo( 0.5f, fadeDuration ); 99 | } 100 | else if ( state == ArmedState.Miss ) { 101 | main.FadeColour( Colours.ColourFor( Result.Type ), fillDuration ); 102 | FillTo( 1, fillDuration ) 103 | .Then().Delay( delayDuration ).FadeOut( fadeDuration ); 104 | this.RotateTo( (float)HitObject.MissRotation, delayDuration + fillDuration + fadeDuration ); 105 | } 106 | 107 | LifetimeEnd = HitStateUpdateTime + fillDuration + delayDuration + fadeDuration; 108 | } 109 | 110 | private BindableDouble fillProgress = new( 0 ); 111 | private TransformSequence FillTo ( double fill, double duration = 0, Easing easing = Easing.None ) 112 | => this.TransformBindableTo( fillProgress, fill, duration, easing ); 113 | 114 | private class PacketFill : Container { 115 | public PacketFill () { 116 | AddInternal( new Box { RelativeSizeAxes = Axes.Both } ); 117 | Masking = true; 118 | } 119 | 120 | new public float CornerRadius { 121 | get => base.CornerRadius; 122 | set => base.CornerRadius = value; 123 | } 124 | } 125 | 126 | private class PacketVisual : CompositeDrawable { 127 | PacketFill fill; 128 | 129 | public PacketVisual ( BindableDouble fillProgress ) { 130 | AddRangeInternal( new Drawable[] { 131 | fill = new PacketFill { RelativeSizeAxes = Axes.Both, Size = new Vector2( 0.4f ), Anchor = Anchor.Centre, Origin = Anchor.Centre, CornerRadius = 30 * 0.4f / 2 }, 132 | new Box { AlwaysPresent = true, Alpha = 0, RelativeSizeAxes = Axes.Both } 133 | } ); 134 | 135 | fillProgress.BindValueChanged( v => { 136 | fill.Size = new Vector2( (float)( 0.4 + 0.6 * v.NewValue ) ); 137 | } ); 138 | 139 | Masking = true; 140 | BorderThickness = 4f; 141 | CornerRadius = 5; 142 | BorderColour = Colour4.White; 143 | RelativeSizeAxes = Axes.Both; 144 | 145 | Origin = Anchor.Centre; 146 | Anchor = Anchor.Centre; 147 | } 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Replays/SolosuAutoGenerator.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Extensions.IEnumerableExtensions; 2 | using osu.Game.Beatmaps; 3 | using osu.Game.Replays; 4 | using osu.Game.Rulesets.Replays; 5 | using osu.Game.Rulesets.Solosu.Objects; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | 10 | namespace osu.Game.Rulesets.Solosu.Replays { 11 | public class SolosuAutoGenerator : AutoGenerator { 12 | protected Replay Replay; 13 | protected List Frames => Replay.Frames; 14 | 15 | public new Beatmap Beatmap => (Beatmap)base.Beatmap; 16 | 17 | public SolosuAutoGenerator ( IBeatmap beatmap ) : base( beatmap ) { 18 | Replay = new Replay(); 19 | } 20 | 21 | public override Replay Generate () { 22 | DifficultyFlowPlayfield flow = new(); 23 | foreach ( var i in Beatmap.HitObjects.OfType() ) { 24 | flow.Add( i ); 25 | } 26 | 27 | SolosuAction? buffered = null; 28 | SolosuAction held = SolosuAction.Center; 29 | 30 | List<(double time, IEnumerable actions)> movement = new(); 31 | List<(double time, IEnumerable actions)> presses = new(); 32 | 33 | void addMovementFrame ( double time, params SolosuAction[] buttons ) { // we dont want auto to hold the center 34 | if ( buttons.Length > 0 && buttons[ 0 ] == SolosuAction.Center ) { 35 | movement.Add( (time, buttons.Skip( 1 )) ); 36 | } 37 | else { 38 | movement.Add( (time, buttons) ); 39 | } 40 | } 41 | 42 | void moveTo ( SolosuAction action, double time ) { // the AI autually uses the flexible inputs! 43 | if ( held != action ) { 44 | if ( buffered is null || buffered == SolosuAction.Center ) { 45 | if ( action == SolosuAction.Center ) { 46 | buffered = null; 47 | held = action; 48 | addMovementFrame( time ); 49 | } 50 | else { 51 | buffered = held; 52 | held = action; 53 | addMovementFrame( time, buffered.Value, held ); 54 | } 55 | } 56 | else { 57 | if ( buffered == action ) { 58 | held = action; 59 | buffered = null; 60 | addMovementFrame( time, action ); 61 | } 62 | else { 63 | held = action; 64 | buffered = null; 65 | addMovementFrame( time, action ); 66 | } 67 | } 68 | } 69 | } 70 | 71 | double lastPressTime = 0; 72 | void press ( double time ) { 73 | if ( time - lastPressTime > KEY_UP_DELAY ) { 74 | presses.Add( (lastPressTime + KEY_UP_DELAY, Array.Empty()) ); 75 | } 76 | else { 77 | presses.Add( (( lastPressTime + time ) / 2, Array.Empty()) ); 78 | } 79 | lastPressTime = time; 80 | } 81 | 82 | var rng = Beatmap.Random(); 83 | double time = 0; 84 | 85 | while ( flow.AnyConcernsLeft( time, held.GetLane() ) ) { 86 | var (type, list) = flow.NextConcern( time, held.GetLane() ); 87 | if ( type == FlowObjectType.Dangerous ) { 88 | var safeLanes = Enum.Values.Except( list.Select( x => ( x.Source as LanedSolosuHitObject ).Lane ) ); 89 | 90 | if ( safeLanes.Any() ) { 91 | var latestPossibleTime = flow.LastSafeTimeAfter( time, held.GetLane() ); 92 | var lane = flow.GetChillestLane( latestPossibleTime ); 93 | var earliestPossibleTime = Math.Max( time, flow.FirstSafeTimeBefore( latestPossibleTime, lane ) ); 94 | 95 | time = ( earliestPossibleTime + latestPossibleTime ) / 2; 96 | moveTo( lane.GetAction(), time ); 97 | } 98 | else { 99 | time = list.Where( x => ( x.Source as LanedSolosuHitObject ).Lane == held.GetLane() ).First().StartTime; 100 | } 101 | } 102 | else if ( type == FlowObjectType.Intercept ) { 103 | if ( list.Count() == 1 ) { 104 | var intercept = list.Single().Source as LanedSolosuHitObject; 105 | var safeTime = Math.Max( time, flow.FirstSafeTimeBefore( intercept.StartTime, intercept.Lane ) ); 106 | moveTo( intercept.Lane.GetAction(), ( safeTime + intercept.StartTime ) / 2 ); 107 | //press( intercept.StartTime ); 108 | time = intercept.StartTime; 109 | } 110 | else { 111 | var first = list.First().Source as LanedSolosuHitObject; 112 | var safeTime = Math.Max( time, flow.FirstSafeTimeBefore( first.StartTime, first.Lane ) ); 113 | time = ( safeTime + list.First().StartTime ) / 2; 114 | double offset = 0; 115 | foreach ( var i in list ) { 116 | moveTo( ( i.Source as LanedSolosuHitObject ).Lane.GetAction(), time ); 117 | //press( time ); 118 | time = i.StartTime + ++offset; 119 | } 120 | time = first.StartTime; 121 | } 122 | } 123 | } 124 | 125 | foreach ( var i in Beatmap.HitObjects.OfType() ) { 126 | press( i.StartTime ); 127 | } 128 | 129 | presses.Add( (lastPressTime + KEY_UP_DELAY, Array.Empty()) ); 130 | if ( !flow.AnyDangerAfterOrAt( time, SolosuLane.Center ) ) movement.Add( (time + KEY_UP_DELAY, Array.Empty()) ); 131 | 132 | movement.Sort( ( x, y ) => x.time > y.time ? 1 : -1 ); 133 | presses.Sort( ( x, y ) => x.time > y.time ? 1 : -1 ); 134 | 135 | int movementIndex = 0; 136 | int pressIndex = 0; 137 | time = -9999999; 138 | 139 | Frames.Add( new SolosuReplayFrame() { Time = 0 } ); 140 | 141 | IEnumerable currentMovement = Array.Empty(); 142 | IEnumerable currentPresses = Array.Empty(); 143 | 144 | while ( movementIndex < movement.Count || pressIndex < presses.Count ) { 145 | (double time, IEnumerable actions) chosen; 146 | if ( movementIndex < movement.Count && pressIndex < presses.Count ) { 147 | var a = movement[ movementIndex ]; 148 | var b = presses[ pressIndex ]; 149 | 150 | if ( a.time < b.time ) { 151 | chosen = a; 152 | movementIndex++; 153 | currentMovement = chosen.actions; 154 | } 155 | else { 156 | chosen = b; 157 | pressIndex++; 158 | currentPresses = chosen.actions; 159 | } 160 | } 161 | else if ( movementIndex < movement.Count ) { 162 | chosen = movement[ movementIndex++ ]; 163 | currentMovement = chosen.actions; 164 | } 165 | else { // pressIndex < presses.Count 166 | chosen = presses[ pressIndex++ ]; 167 | currentPresses = chosen.actions; 168 | } 169 | 170 | time = chosen.time; 171 | Frames.Add( new SolosuReplayFrame( currentMovement.Concat( currentPresses ) ) { Time = time } ); 172 | } 173 | 174 | return Replay; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/Replays/DifficultyFlowLane.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Rulesets.Solosu.Objects; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace osu.Game.Rulesets.Solosu.Replays { 7 | public class DifficultyFlowLane { 8 | List flowObjects = new(); 9 | 10 | public void Clear () 11 | => flowObjects.Clear(); 12 | 13 | public void Add ( FlowObject fo ) { 14 | flowObjects.Add( fo ); 15 | 16 | flowObjects.Sort( ( a, b ) => Math.Sign( a.StartTime - b.StartTime ) ); 17 | } 18 | 19 | public void AddRange ( IEnumerable fo ) { 20 | flowObjects.AddRange( fo ); 21 | 22 | flowObjects.Sort( ( a, b ) => Math.Sign( a.StartTime - b.StartTime ) ); 23 | } 24 | 25 | public bool AnyDangersAfter ( double time ) 26 | => flowObjects.Any( x => x.Type == FlowObjectType.Dangerous && x.StartTime > time ); 27 | public bool AnyInterceptsAfter ( double time ) 28 | => flowObjects.Any( x => x.Type == FlowObjectType.Intercept && x.StartTime > time ); 29 | public FlowObject FirstDangerAfter ( double time ) 30 | => flowObjects.First( x => x.Type == FlowObjectType.Dangerous && x.StartTime > time ); 31 | public FlowObject FirstInterceptAfter ( double time ) 32 | => flowObjects.First( x => x.Type == FlowObjectType.Intercept && x.StartTime > time ); 33 | public IEnumerable GetInterceptsAt ( double time ) 34 | => flowObjects.Where( x => x.Type == FlowObjectType.Intercept ).Where( x => x.StartTime == time ); 35 | public IEnumerable GetDangersAt ( double time ) 36 | => flowObjects.Where( x => x.Type == FlowObjectType.Dangerous ).Where( x => x.StartTime <= time && x.EndTime >= time ); 37 | public bool AnyDangersBeforeOrAt ( double time ) 38 | => flowObjects.Any( x => x.Type == FlowObjectType.Dangerous && ( ( x.StartTime <= time && x.EndTime >= time ) || ( x.EndTime < time ) ) ); 39 | public FlowObject LastDangerBeforeOrAt ( double time ) 40 | => flowObjects.Last( x => x.Type == FlowObjectType.Dangerous && ( ( x.StartTime <= time && x.EndTime >= time ) || ( x.EndTime < time ) ) ); 41 | public bool AnyDangersAfterOrAt ( double time ) 42 | => flowObjects.Any( x => x.Type == FlowObjectType.Dangerous && ( ( x.StartTime <= time && x.EndTime >= time ) || ( x.StartTime > time ) ) ); 43 | public FlowObject FirstDangerAfterOrAt ( double time ) 44 | => flowObjects.First( x => x.Type == FlowObjectType.Dangerous && ( ( x.StartTime <= time && x.EndTime >= time ) || ( x.StartTime > time ) ) ); 45 | public bool HasInterceptBeforeDangerAfter ( double time ) { 46 | if ( AnyInterceptsAfter( time ) ) { 47 | if ( AnyDangersAfterOrAt( time ) ) { 48 | return FirstDangerAfterOrAt( time ).StartTime > FirstInterceptAfter( time ).StartTime; 49 | } 50 | else return true; 51 | } 52 | else return false; 53 | } 54 | } 55 | 56 | public struct FlowObject : IHasLane { 57 | public double StartTime; 58 | public double EndTime; 59 | public FlowObjectType Type; 60 | public SolosuHitObject Source; 61 | 62 | public FlowObject ( double startTime, double endTime, FlowObjectType type, SolosuLane lane ) { 63 | StartTime = startTime; 64 | EndTime = endTime; 65 | Type = type; 66 | Source = null; 67 | Lane = lane; 68 | } 69 | 70 | public SolosuLane Lane { get; set; } 71 | } 72 | 73 | public enum FlowObjectType { 74 | Dangerous, 75 | Intercept 76 | } 77 | 78 | public class DifficultyFlowPlayfield { 79 | Dictionary lanes = new(); 80 | public DifficultyFlowPlayfield () { 81 | foreach ( var i in Enum.Values ) { 82 | lanes.Add( i, new() ); 83 | } 84 | } 85 | 86 | public void Clear () { 87 | foreach ( var i in lanes ) i.Value.Clear(); 88 | } 89 | 90 | public void Add ( IFlowObject lho ) { 91 | if ( lho is IHasLane laned ) { 92 | lanes[ laned.Lane ].AddRange( lho.CreateFlowObjects().Mutate( ( ref FlowObject x ) => x.Lane = laned.Lane ) ); 93 | } 94 | else { 95 | foreach ( var i in lho.CreateFlowObjects() ) { 96 | lanes[ i.Lane ].Add( i ); 97 | } 98 | } 99 | } 100 | 101 | public bool AnyConcernsLeft ( double time, SolosuLane currentLane ) 102 | => lanes[ currentLane ].AnyDangersAfter( time ) || lanes.Any( x => x.Value.AnyInterceptsAfter( time ) ); 103 | public (FlowObjectType type, IEnumerable list) NextConcern ( double time, SolosuLane currentLane ) { 104 | if ( lanes[ currentLane ].AnyDangersAfter( time ) ) { 105 | var dangerTime = Math.Max( time, lanes[ currentLane ].FirstDangerAfter( time ).StartTime ); 106 | if ( lanes.Any( x => x.Value.AnyInterceptsAfter( time ) ) ) { 107 | var intercepts = FirstInterceptsAfter( time ); 108 | var interceptTime = intercepts.First().StartTime; 109 | 110 | return ( dangerTime < interceptTime ) ? (FlowObjectType.Dangerous, lanes.SelectMany( x => x.Value.GetDangersAt( dangerTime ) )) : (FlowObjectType.Intercept, intercepts); 111 | } 112 | else { 113 | return (FlowObjectType.Dangerous, lanes.SelectMany( x => x.Value.GetDangersAt( dangerTime ) )); 114 | } 115 | } 116 | else { // must be intercept 117 | return (FlowObjectType.Intercept, FirstInterceptsAfter( time )); 118 | } 119 | } 120 | public SolosuLane GetChillestLane ( double time ) { 121 | Dictionary alignedIntercepts = new(); 122 | 123 | foreach ( var lane in lanes ) { 124 | if ( lane.Value.HasInterceptBeforeDangerAfter( time ) ) { 125 | alignedIntercepts.Add( lane.Key, lane.Value.FirstInterceptAfter( time ).StartTime ); 126 | } 127 | } 128 | if ( alignedIntercepts.Any() ) { 129 | double earliest = alignedIntercepts.Min( x => x.Value ); 130 | return alignedIntercepts.First( x => x.Value == earliest ).Key; 131 | } 132 | 133 | foreach ( var lane in lanes ) { 134 | if ( !lane.Value.AnyDangersAfterOrAt( time ) ) return lane.Key; 135 | alignedIntercepts.Add( lane.Key, lane.Value.FirstDangerAfterOrAt( time ).StartTime ); 136 | } 137 | 138 | double latest = alignedIntercepts.Max( x => x.Value ); 139 | return alignedIntercepts.First( x => x.Value == latest ).Key; 140 | } 141 | public IEnumerable FirstInterceptsAfter ( double time ) { 142 | var firsties = lanes.Where( x => x.Value.AnyInterceptsAfter( time ) ).Select( x => x.Value.FirstInterceptAfter( time ) ); 143 | var earliestTime = firsties.Min( x => x.StartTime ); 144 | return lanes.SelectMany( x => x.Value.GetInterceptsAt( earliestTime ) ); 145 | } 146 | public double FirstSafeTimeBefore ( double time, SolosuLane lane ) 147 | => lanes[ lane ].AnyDangersBeforeOrAt( time ) ? Math.Min( lanes[ lane ].LastDangerBeforeOrAt( time ).EndTime, time ) : 0; 148 | public double LastSafeTimeAfter ( double time, SolosuLane lane ) 149 | => lanes[ lane ].AnyDangersAfter( time ) ? Math.Max( lanes[ lane ].FirstDangerAfter( time ).StartTime, time ) : double.PositiveInfinity; 150 | public bool AnyDangerAfterOrAt ( double time, SolosuLane lane ) 151 | => lanes[ lane ].AnyDangersAfterOrAt( time ); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/UI/SolosuPlayfield.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Audio.Track; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Graphics.Containers; 6 | using osu.Framework.Graphics.Pooling; 7 | using osu.Game.Beatmaps.ControlPoints; 8 | using osu.Game.Rulesets.Judgements; 9 | using osu.Game.Rulesets.Objects; 10 | using osu.Game.Rulesets.Objects.Drawables; 11 | using osu.Game.Rulesets.Scoring; 12 | using osu.Game.Rulesets.Solosu.Objects; 13 | using osu.Game.Rulesets.Solosu.Objects.Drawables; 14 | using osu.Game.Rulesets.Solosu.Replays; 15 | using osu.Game.Rulesets.UI; 16 | using osuTK; 17 | using System; 18 | using System.Collections.Generic; 19 | 20 | namespace osu.Game.Rulesets.Solosu.UI { 21 | [Cached] 22 | public class SolosuPlayfield : Playfield { 23 | [Cached] 24 | private PlayerByte player = new PlayerByte { Anchor = Anchor.BottomCentre, Depth = -9999 }; 25 | public readonly Container CubeContainer; 26 | private Wireframe cube; 27 | private Wireframe cubeR; 28 | private Wireframe cubeG; 29 | private Wireframe cubeB; 30 | 31 | Container judgements; 32 | DrawablePool judgementPool; 33 | 34 | [Cached( name: nameof( RelaxBindable ) )] 35 | public readonly BindableBool RelaxBindable = new( false ); 36 | [Cached( name: nameof( AutopilotBindable ) )] 37 | public readonly BindableBool AutopilotBindable = new( false ); 38 | [Cached] 39 | public readonly ActualReplay replay = new(); 40 | 41 | // these are here because it was already implemented, so if we ever add tapping back it will be useful 42 | public bool IsRelaxEnabled { 43 | get => RelaxBindable.Value; 44 | set => RelaxBindable.Value = value; 45 | } 46 | 47 | public bool IsAutopilotEnabled { 48 | get => AutopilotBindable.Value; 49 | set => AutopilotBindable.Value = value; 50 | } 51 | 52 | public SolosuPlayfield () { 53 | AddInternal( solosuColours = new() ); 54 | AddInternal( player ); 55 | AddInternal( judgements = new() { RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre } ); 56 | AddInternal( judgementPool = new( 10 ) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre } ); 57 | AddInternal( beat ); 58 | AddInternal( CubeContainer = new Container { 59 | Origin = Anchor.TopCentre, 60 | Anchor = Anchor.TopCentre, 61 | Y = 70, 62 | Height = 150, 63 | Children = new[] { 64 | cubeR = new Cube { Size = new Vector2( 150 ), Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = Colour4.Red, Alpha = 0.4f }, 65 | cubeG = new Cube { Size = new Vector2( 150 ), Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = Colour4.Green, Alpha = 0.4f }, 66 | cubeB = new Cube { Size = new Vector2( 150 ), Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = Colour4.Blue, Alpha = 0.4f }, 67 | cube = new Cube { Size = new Vector2( 150 ), Anchor = Anchor.Centre, Origin = Anchor.Centre } 68 | } 69 | } ); 70 | AddInternal( HitObjectContainer ); 71 | HitObjectContainer.Origin = Anchor.BottomCentre; 72 | HitObjectContainer.Anchor = Anchor.BottomCentre; 73 | 74 | foreach ( var i in Enum.Values ) { 75 | var lane = new Lane { 76 | RelativeSizeAxes = Axes.Y, 77 | Width = (float)LANE_WIDTH, 78 | X = (float)( i.NormalizedOffset() * LANE_WIDTH ), 79 | 80 | Origin = Anchor.BottomCentre, 81 | Anchor = Anchor.BottomCentre 82 | }; 83 | Lanes.Add( i, lane ); 84 | AddNested( lane ); 85 | AddInternal( lane ); 86 | } 87 | 88 | HitHeight.BindValueChanged( v => player.Y = -(float)v.NewValue, true ); 89 | beat.OnBeat += OnBeat; 90 | 91 | NewResult += OnNewResult; 92 | KiaiBindable.BindValueChanged( v => { 93 | if ( v.NewValue ) { 94 | this.TransformBindableTo( ScrollDuration, 3000 / KIAI_SPEEDUP / SCROLL_MULTIPLIER, 400 ); 95 | } 96 | else { 97 | this.TransformBindableTo( ScrollDuration, 3000 / SCROLL_MULTIPLIER, 400 ); 98 | } 99 | } ); 100 | 101 | RegisterPool( 2 ); 102 | RegisterPool( 30 ); 103 | RegisterPool( 4 ); 104 | } 105 | 106 | protected override void Update () { 107 | base.Update(); 108 | cube.Rotation3D = Quaternion.FromAxisAngle( new Vector3( 0.3f, 1, 0 ), (float)Time.Current / 1000 ); 109 | cubeR.Rotation3D = Quaternion.FromAxisAngle( new Vector3( 0.3f, 1, 0 ), (float)Time.Current / 1000 ); 110 | cubeG.Rotation3D = Quaternion.FromAxisAngle( new Vector3( 0.3f, 1, 0 ), (float)Time.Current / 1000 ); 111 | cubeB.Rotation3D = Quaternion.FromAxisAngle( new Vector3( 0.3f, 1, 0 ), (float)Time.Current / 1000 ); 112 | 113 | if ( Time.Elapsed < 0 ) { 114 | FinishTransforms(); 115 | ClearTransformsAfter( Time.Current ); 116 | } 117 | } 118 | 119 | [Cached( name: nameof( KiaiBindable ) )] 120 | public readonly BindableBool KiaiBindable = new( false ); 121 | private void OnBeat ( int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes ) { 122 | // thanks, hishi ;)))))) 123 | if ( effectPoint.KiaiMode ) { 124 | float sickoMode = 30 * amplitudes.Average * beat.RandomFloat( 0 ); 125 | 126 | cubeR.MoveToOffset( beat.RandomVector( 0 ) * sickoMode, 50 ).Then().MoveTo( Vector2.Zero, 100 ); 127 | cubeG.MoveToOffset( beat.RandomVector( 1 ) * sickoMode, 50 ).Then().MoveTo( Vector2.Zero, 100 ); 128 | cubeB.MoveToOffset( beat.RandomVector( 2 ) * sickoMode, 50 ).Then().MoveTo( Vector2.Zero, 100 ); 129 | } 130 | 131 | KiaiBindable.Value = effectPoint.KiaiMode; 132 | } 133 | 134 | private void OnNewResult ( DrawableHitObject dho, JudgementResult j ) { 135 | DrawableSolosuHitObject dsho = dho as DrawableSolosuHitObject; 136 | 137 | if ( j.Type is HitResult.SmallTickHit ) { 138 | 139 | } 140 | else if ( dsho is DrawableHardBeat ) { 141 | // TODO burst here? 142 | } 143 | else if ( j.Type != HitResult.Miss ) { 144 | CubeContainer.ScaleTo( MathF.Max( CubeContainer.Scale.X * 0.92f, 0.5f ), 50 ).Then().ScaleTo( 1, 100 ); 145 | if ( dho is DrawablePacket ) { 146 | judgements.Add( judgementPool.Get( d => { 147 | d.Apply( dsho, j ); 148 | d.Scale = new Vector2( 1 ); 149 | } ) ); 150 | 151 | player.@byte.ScaleTo( 0.8f, 20 ).Then().ScaleTo( 1, 50 ); 152 | } 153 | else if ( dho is DrawableStream or DrawableMultiStream ) { 154 | judgements.Add( judgementPool.Get( d => { 155 | d.Apply( dsho, j ); 156 | d.Scale = new Vector2( 1.1f ); 157 | } ) ); 158 | } 159 | } 160 | else if ( dho is not DrawablePacket ) { 161 | player.TakeDamage(); 162 | } 163 | } 164 | 165 | protected override HitObjectLifetimeEntry CreateLifetimeEntry ( HitObject hitObject ) => new SolosuLifetimeEntry( hitObject, ScrollDuration ); 166 | [Cached] 167 | BeatDetector beat = new BeatDetector(); 168 | [Cached( name: nameof( ScrollDuration ) )] 169 | public readonly BindableDouble ScrollDuration = new( 3000 / SCROLL_MULTIPLIER ); 170 | public const double SCROLL_MULTIPLIER = 1.6; 171 | public const double KIAI_SPEEDUP = 4.0 / 3.0; 172 | public const double SCROLL_HEIGHT = 900; 173 | [Cached( name: nameof( HitHeight ) )] 174 | public readonly BindableDouble HitHeight = new( 160 ); 175 | [Cached] 176 | public readonly Dictionary Lanes = new(); 177 | public const double LANE_WIDTH = 200; 178 | [Cached] 179 | public readonly SolosuColours solosuColours; 180 | 181 | public override void Add ( HitObject hitObject ) { 182 | if ( hitObject is LanedSolosuHitObject lsho ) 183 | Lanes[ lsho.Lane ].Add( lsho ); 184 | else base.Add( hitObject ); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /osu.Game.Rulesets.Solosu/UI/PlayerByte.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Containers; 5 | using osu.Framework.Graphics.Rendering; 6 | using osu.Framework.Graphics.Shapes; 7 | using osu.Framework.Graphics.Sprites; 8 | using osu.Framework.Input.Bindings; 9 | using osu.Framework.Input.Events; 10 | using osu.Framework.Utils; 11 | using osu.Game.Rulesets.Solosu.Collections; 12 | using osu.Game.Rulesets.Solosu.Objects; 13 | using osu.Game.Rulesets.Solosu.Replays; 14 | using osuTK; 15 | using System; 16 | using System.Collections.Generic; 17 | using System.Linq; 18 | 19 | namespace osu.Game.Rulesets.Solosu.UI { 20 | public class PlayerByte : CompositeDrawable, IKeyBindingHandler { 21 | List moves = new(); 22 | TimeSeekableList allMoves = new(); // we need this because otherwise its impossible to tell what movement was buffered when rewinding ( you cant tell which index a move was removed from ) 23 | 24 | [Resolved] 25 | Dictionary lanes { get; set; } 26 | public SolosuLane Lane { get; private set; } = SolosuLane.Center; 27 | 28 | [Resolved( name: nameof( SolosuPlayfield.RelaxBindable ) )] 29 | BindableBool RelaxBindable { get; set; } 30 | [Resolved( name: nameof( SolosuPlayfield.AutopilotBindable ) )] 31 | BindableBool AutopilotBindable { get; set; } 32 | [Resolved] 33 | ActualReplay replay { get; set; } 34 | 35 | public PlayerVisual @byte; 36 | PlayerLine line; 37 | Dictionary bufferIndicators = new(); 38 | public PlayerByte () { 39 | AddInternal( line = new PlayerLine { Origin = Anchor.Centre, Anchor = Anchor.Centre } ); 40 | AddInternal( @byte = new PlayerVisual { Origin = Anchor.Centre, Anchor = Anchor.Centre } ); 41 | Origin = Anchor.Centre; 42 | AutoSizeAxes = Axes.Y; 43 | Width = 500; 44 | Masking = true; 45 | allMoves.Add( double.NegativeInfinity, new InputState( moves ) ); 46 | } 47 | 48 | List held = new(); 49 | List allHeld = new(); 50 | public bool OnPressed ( KeyBindingPressEvent action ) { 51 | allHeld.Add( action.Action ); 52 | if ( action.Action.IsMovement() ) { 53 | if ( AutopilotBindable.Value ) return false; 54 | 55 | if ( allMoves.AnyAfter( Clock.CurrentTime ) ) { 56 | allMoves.ClearAfter( Clock.CurrentTime ); 57 | allMoves.At( Clock.CurrentTime ).Restore( moves ); 58 | moves.RemoveAll( x => !allHeld.Contains( x ) ); 59 | updatePosition(); 60 | FinishTransforms( true ); 61 | 62 | return false; 63 | } 64 | 65 | moves.Add( action.Action ); 66 | allMoves.Add( Clock.CurrentTime, new InputState( moves ) ); 67 | updatePosition(); 68 | } 69 | else if ( action.Action.IsAction() ) { 70 | if ( RelaxBindable.Value ) return false; 71 | 72 | held.Add( action.Action ); 73 | @byte.ScaleTo( 0.8f, 20 ); 74 | } 75 | 76 | return false; 77 | } 78 | 79 | public void OnReleased ( KeyBindingReleaseEvent action ) { 80 | allHeld.Remove( action.Action ); 81 | if ( action.Action.IsMovement() ) { 82 | if ( AutopilotBindable.Value ) return; 83 | 84 | if ( allMoves.AnyAfter( Clock.CurrentTime ) ) { 85 | allMoves.ClearAfter( Clock.CurrentTime ); 86 | allMoves.At( Clock.CurrentTime ).Restore( moves ); 87 | moves.RemoveAll( x => !allHeld.Contains( x ) ); 88 | updatePosition(); 89 | FinishTransforms( true ); 90 | 91 | return; 92 | } 93 | 94 | moves.Remove( action.Action ); 95 | allMoves.Add( Clock.CurrentTime, new InputState( moves ) ); 96 | updatePosition(); 97 | } 98 | else if ( action.Action.IsAction() ) { 99 | if ( RelaxBindable.Value ) return; 100 | 101 | held.Remove( action.Action ); 102 | if ( held.IsEmpty() ) @byte.ScaleTo( 1, 50 ); 103 | } 104 | } 105 | 106 | void updatePosition () { 107 | SolosuLane lane = SolosuLane.Center; 108 | if ( moves.Any() ) { 109 | lane = moves.Last().GetLane(); 110 | } 111 | 112 | if ( lane != this.Lane ) { 113 | this.Lane = lane; 114 | @byte.MoveTo( lanes[ lane ] ); 115 | } 116 | 117 | foreach ( var i in bufferIndicators ) { 118 | i.Value.IsVisible = moves.Contains( i.Key.GetAction() ) && moves.Last() != i.Key.GetAction(); 119 | } 120 | } 121 | 122 | public void TakeDamage () { 123 | @byte.FlashColour( Colours.Miss, 500, Easing.Out ); 124 | @byte.RotateTo( RNG.NextSingle( -30, 30 ) ).Then().RotateTo( 0, 500, Easing.In ); 125 | } 126 | 127 | protected override void Update () { 128 | if ( AutopilotBindable.Value || RelaxBindable.Value ) { 129 | SolosuReplayFrame frame; 130 | while ( ( frame = replay.FrameAt( Clock.CurrentTime ) ) is not null ) { 131 | if ( RelaxBindable.Value ) held.Clear(); 132 | 133 | foreach ( var action in Enum.Values ) { 134 | if ( action.IsMovement() && AutopilotBindable.Value ) { 135 | if ( frame.Actions.Contains( action ) && !moves.Contains( action ) ) { 136 | moves.Add( action ); 137 | } 138 | else if ( !frame.Actions.Contains( action ) && moves.Contains( action ) ) { 139 | moves.Remove( action ); 140 | } 141 | } 142 | else if ( action.IsAction() && RelaxBindable.Value && frame.Actions.Contains( action ) ) { 143 | held.Add( action ); 144 | } 145 | } 146 | 147 | if ( RelaxBindable.Value ) { 148 | if ( held.IsEmpty() ) @byte.ScaleTo( 1, 50 ); 149 | else @byte.ScaleTo( 0.8f, 20 ); // TODO do this better 150 | } 151 | updatePosition(); 152 | } 153 | } 154 | 155 | if ( !AutopilotBindable.Value && allMoves.AnyAfter( Clock.CurrentTime ) ) { 156 | allMoves.ClearAfter( Clock.CurrentTime ); 157 | allMoves.At( Clock.CurrentTime ).Restore( moves ); 158 | moves.RemoveAll( x => !allHeld.Contains( x ) ); 159 | updatePosition(); 160 | FinishTransforms( true ); 161 | } 162 | 163 | line.X = @byte.X / 4; 164 | } 165 | 166 | [Resolved] 167 | public SolosuColours Colours { get; private set; } 168 | [BackgroundDependencyLoader] 169 | private void load () { 170 | foreach ( var i in lanes ) { 171 | var indicator = new BufferIndicator { Size = new Vector2( 12 ), X = i.Value.X, Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = Colours.Perfect }; 172 | AddInternal( indicator ); 173 | bufferIndicators.Add( i.Key, indicator ); 174 | } 175 | @byte.Colour = Colours.Perfect; 176 | line.Colour = Colours.Regular; 177 | } 178 | 179 | private class BufferIndicator : SpriteIcon { 180 | public BufferIndicator () { 181 | Alpha = 0; 182 | Icon = FontAwesome.Solid.Star; 183 | } 184 | bool isVisible = false; 185 | public bool IsVisible { 186 | get => isVisible; 187 | set { 188 | if ( isVisible == value ) return; 189 | isVisible = value; 190 | 191 | FinishTransforms(); 192 | if ( IsVisible ) 193 | Alpha = 1; 194 | else 195 | this.FadeOut( 100 ); 196 | } 197 | } 198 | } 199 | 200 | public class PlayerVisual : CompositeDrawable { 201 | Box box; 202 | public PlayerVisual () { 203 | AddInternal( box = new Box { Size = new Vector2( MathF.Sqrt( volume ) ), Origin = Anchor.Centre, Anchor = Anchor.Centre } ); 204 | Origin = Anchor.Centre; 205 | Masking = true; 206 | CornerRadius = 8; 207 | AutoSizeAxes = Axes.Both; 208 | } 209 | private float volume = 32 * 32; 210 | public void MoveTo ( Lane lane ) { 211 | FinishTransforms( true ); 212 | this.MoveToX( lane.X, 50, Easing.Out ); 213 | 214 | float distance = MathF.Abs( X - lane.X ); 215 | float height = MathF.Sqrt( volume ) - MathF.Sqrt( distance ) / 4; 216 | 217 | box.ResizeTo( new Vector2( volume / height, height ), 40 ).Then().ResizeTo( MathF.Sqrt( volume ), 10 ); 218 | } 219 | } 220 | 221 | private class PlayerLine : CompositeDrawable { 222 | [BackgroundDependencyLoader] 223 | private void load ( IRenderer renderer ) { 224 | Origin = Anchor.Centre; 225 | Anchor = Anchor.Centre; 226 | AddInternal( new Sprite { Width = 500, Height = 2, Origin = Anchor.Centre, Anchor = Anchor.Centre, Texture = SolosuTextures.WidthFade( renderer, 500, 2 ), Alpha = 0.6f } ); 227 | // TODO maybe add the key being held effect from the key overlay when on the side 228 | } 229 | } 230 | } 231 | 232 | public struct InputState { 233 | public int LeftIndex; 234 | public int RightIndex; 235 | public int CenterIndex; 236 | 237 | public InputState ( List actions ) { 238 | LeftIndex = actions.IndexOf( SolosuAction.Left ); 239 | RightIndex = actions.IndexOf( SolosuAction.Right ); 240 | CenterIndex = actions.IndexOf( SolosuAction.Center ); 241 | } 242 | 243 | public void Restore ( List actions ) { 244 | actions.Clear(); 245 | if ( !tryAddIndex( 0, actions ) ) return; 246 | if ( !tryAddIndex( 1, actions ) ) return; 247 | if ( !tryAddIndex( 2, actions ) ) return; 248 | } 249 | 250 | private bool tryAddIndex ( int index, List actions ) { 251 | if ( LeftIndex == index ) 252 | actions.Add( SolosuAction.Left ); 253 | else if ( RightIndex == index ) 254 | actions.Add( SolosuAction.Right ); 255 | else if ( CenterIndex == index ) 256 | actions.Add( SolosuAction.Center ); 257 | else return false; 258 | 259 | return true; 260 | } 261 | } 262 | } 263 | --------------------------------------------------------------------------------