1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 09:27:34 +08:00

Merge branch 'master' into fix-triangle-smoothness

This commit is contained in:
Dean Herbert 2017-05-30 08:58:05 +09:00 committed by GitHub
commit b8461cd02d
33 changed files with 1402 additions and 161 deletions

@ -1 +1 @@
Subproject commit 0b9053ec3d39b486165992374752b280c5eeef06
Subproject commit 0f3db5da09d0e7c4d2ef3057030e018f34ba536e

View File

@ -0,0 +1,39 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Framework.Graphics;
namespace osu.Desktop.VisualTests.Tests
{
internal class TestCaseBreadcrumbs : TestCase
{
public override string Description => @"breadcrumb > control";
public override void Reset()
{
base.Reset();
BreadcrumbControl<BreadcrumbTab> c;
Add(c = new BreadcrumbControl<BreadcrumbTab>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
});
AddStep(@"first", () => c.Current.Value = BreadcrumbTab.Click);
AddStep(@"second", () => c.Current.Value = BreadcrumbTab.The);
AddStep(@"third", () => c.Current.Value = BreadcrumbTab.Circles);
}
private enum BreadcrumbTab
{
Click,
The,
Circles,
}
}
}

View File

@ -11,6 +11,9 @@ using OpenTK;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Timing;
using osu.Framework.Configuration;
using OpenTK.Input;
using osu.Framework.Timing;
namespace osu.Desktop.VisualTests.Tests
{
@ -59,6 +62,51 @@ namespace osu.Desktop.VisualTests.Tests
}
};
Action createPlayfieldWithNotesAcceptingInput = () =>
{
Clear();
var rateAdjustClock = new StopwatchClock(true) { Rate = 0.5 };
ManiaPlayfield playField;
Add(playField = new ManiaPlayfield(4, new List<TimingChange> { new TimingChange { BeatLength = 200 } })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1, -1),
Clock = new FramedClock(rateAdjustClock)
});
for (int t = 1000; t <= 2000; t += 100)
{
playField.Add(new DrawableNote(new Note
{
StartTime = t,
Column = 0
}, new Bindable<Key>(Key.D)));
playField.Add(new DrawableNote(new Note
{
StartTime = t,
Column = 3
}, new Bindable<Key>(Key.K)));
}
playField.Add(new DrawableHoldNote(new HoldNote
{
StartTime = 1000,
Duration = 1000,
Column = 1
}, new Bindable<Key>(Key.F)));
playField.Add(new DrawableHoldNote(new HoldNote
{
StartTime = 1000,
Duration = 1000,
Column = 2
}, new Bindable<Key>(Key.J)));
};
AddStep("1 column", () => createPlayfield(1, SpecialColumnPosition.Normal));
AddStep("4 columns", () => createPlayfield(4, SpecialColumnPosition.Normal));
AddStep("Left special style", () => createPlayfield(4, SpecialColumnPosition.Left));
@ -76,6 +124,8 @@ namespace osu.Desktop.VisualTests.Tests
AddWaitStep(10);
AddStep("Right special style", () => createPlayfieldWithNotes(4, SpecialColumnPosition.Right));
AddWaitStep(10);
AddStep("Notes with input", () => createPlayfieldWithNotesAcceptingInput());
}
private void triggerKeyDown(Column column)

View File

@ -223,6 +223,7 @@
<Compile Include="Tests\TestCaseDrawableRoom.cs" />
<Compile Include="Tests\TestCaseUserPanel.cs" />
<Compile Include="Tests\TestCaseDirect.cs" />
<Compile Include="Tests\TestCaseBreadcrumbs.cs" />
</ItemGroup>
<ItemGroup />
<ItemGroup />

View File

@ -13,6 +13,7 @@ using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Database;
using osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy;
using OpenTK;
using osu.Game.Audio;
namespace osu.Game.Rulesets.Mania.Beatmaps
{
@ -161,9 +162,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
pattern.Add(new HoldNote
{
StartTime = HitObject.StartTime,
Samples = HitObject.Samples,
Duration = endTimeData.Duration,
Column = column,
Head = { Samples = sampleInfoListAt(HitObject.StartTime) },
Tail = { Samples = sampleInfoListAt(endTimeData.EndTime) },
});
}
else if (positionData != null)
@ -178,6 +180,24 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
return pattern;
}
/// <summary>
/// Retrieves the sample info list at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param>
/// <returns></returns>
private SampleInfoList sampleInfoListAt(double time)
{
var curveData = HitObject as IHasCurve;
if (curveData == null)
return HitObject.Samples;
double segmentTime = (curveData.EndTime - HitObject.StartTime) / curveData.RepeatCount;
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
return curveData.RepeatSamples[index];
}
}
}
}

View File

@ -471,14 +471,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
else
{
newObject = new HoldNote
var holdNote = new HoldNote
{
StartTime = startTime,
Samples = sampleInfoListAt(startTime),
EndSamples = sampleInfoListAt(endTime),
Column = column,
Duration = endTime - startTime
Duration = endTime - startTime,
Head = { Samples = sampleInfoListAt(startTime) },
Tail = { Samples = sampleInfoListAt(endTime) }
};
newObject = holdNote;
}
pattern.Add(newObject);

View File

@ -69,18 +69,21 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (holdNote)
{
newObject = new HoldNote
var hold = new HoldNote
{
StartTime = HitObject.StartTime,
EndSamples = HitObject.Samples,
Column = column,
Duration = endTime - HitObject.StartTime
};
newObject.Samples.Add(new SampleInfo
hold.Head.Samples.Add(new SampleInfo
{
Name = SampleInfo.HIT_NORMAL
});
hold.Tail.Samples = HitObject.Samples;
newObject = hold;
}
else
{

View File

@ -0,0 +1,37 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTailJudgement : ManiaJudgement
{
/// <summary>
/// Whether the hold note has been released too early and shouldn't give full score for the release.
/// </summary>
public bool HasBroken;
public override int NumericResultForScore(ManiaHitResult result)
{
switch (result)
{
default:
return base.NumericResultForScore(result);
case ManiaHitResult.Great:
case ManiaHitResult.Perfect:
return base.NumericResultForScore(HasBroken ? ManiaHitResult.Good : result);
}
}
public override int NumericResultForAccuracy(ManiaHitResult result)
{
switch (result)
{
default:
return base.NumericResultForAccuracy(result);
case ManiaHitResult.Great:
case ManiaHitResult.Perfect:
return base.NumericResultForAccuracy(HasBroken ? ManiaHitResult.Good : result);
}
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
public override bool AffectsCombo => false;
public override int NumericResultForScore(ManiaHitResult result) => 20;
public override int NumericResultForAccuracy(ManiaHitResult result) => 0; // Don't count ticks into accuracy
}
}

View File

@ -2,11 +2,37 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Judgements
{
public class ManiaJudgement : Judgement
{
/// <summary>
/// The maximum possible hit result.
/// </summary>
public const ManiaHitResult MAX_HIT_RESULT = ManiaHitResult.Perfect;
/// <summary>
/// The result value for the combo portion of the score.
/// </summary>
public int ResultValueForScore => Result == HitResult.Miss ? 0 : NumericResultForScore(ManiaResult);
/// <summary>
/// The result value for the accuracy portion of the score.
/// </summary>
public int ResultValueForAccuracy => Result == HitResult.Miss ? 0 : NumericResultForAccuracy(ManiaResult);
/// <summary>
/// The maximum result value for the combo portion of the score.
/// </summary>
public int MaxResultValueForScore => NumericResultForScore(MAX_HIT_RESULT);
/// <summary>
/// The maximum result value for the accuracy portion of the score.
/// </summary>
public int MaxResultValueForAccuracy => NumericResultForAccuracy(MAX_HIT_RESULT);
public override string ResultString => string.Empty;
public override string MaxResultString => string.Empty;
@ -15,5 +41,42 @@ namespace osu.Game.Rulesets.Mania.Judgements
/// The hit result.
/// </summary>
public ManiaHitResult ManiaResult;
public virtual int NumericResultForScore(ManiaHitResult result)
{
switch (result)
{
default:
return 0;
case ManiaHitResult.Bad:
return 50;
case ManiaHitResult.Ok:
return 100;
case ManiaHitResult.Good:
return 200;
case ManiaHitResult.Great:
case ManiaHitResult.Perfect:
return 300;
}
}
public virtual int NumericResultForAccuracy(ManiaHitResult result)
{
switch (result)
{
default:
return 0;
case ManiaHitResult.Bad:
return 50;
case ManiaHitResult.Ok:
return 100;
case ManiaHitResult.Good:
return 200;
case ManiaHitResult.Great:
return 300;
case ManiaHitResult.Perfect:
return 305;
}
}
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Mania.Objects
{
public class BarLine : ManiaHitObject
{
/// <summary>
/// The control point which this bar line is part of.
/// </summary>
public TimingControlPoint ControlPoint;
/// <summary>
/// The index of the beat which this bar line represents within the control point.
/// This is a "major" bar line if <see cref="BeatIndex"/> % <see cref="TimingControlPoint.TimeSignature"/> == 0.
/// </summary>
public int BeatIndex;
}
}

View File

@ -0,0 +1,74 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="BarLine"/>. Although this derives DrawableManiaHitObject,
/// this does not handle input/sound like a normal hit object.
/// </summary>
public class DrawableBarLine : DrawableManiaHitObject<BarLine>
{
/// <summary>
/// Height of major bar line triangles.
/// </summary>
private const float triangle_height = 12;
/// <summary>
/// Offset of the major bar line triangles from the sides of the bar line.
/// </summary>
private const float triangle_offset = 9;
public DrawableBarLine(BarLine barLine)
: base(barLine)
{
RelativeSizeAxes = Axes.X;
Height = 1;
Add(new Box
{
Name = "Bar line",
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
});
bool isMajor = barLine.BeatIndex % (int)barLine.ControlPoint.TimeSignature == 0;
if (isMajor)
{
Add(new EquilateralTriangle
{
Name = "Left triangle",
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopCentre,
Size = new Vector2(triangle_height),
X = -triangle_offset,
Rotation = 90
});
Add(new EquilateralTriangle
{
Name = "Right triangle",
Anchor = Anchor.BottomRight,
Origin = Anchor.TopCentre,
Size = new Vector2(triangle_height),
X = triangle_offset,
Rotation = -90
});
}
if (!isMajor && barLine.BeatIndex % 2 == 1)
Alpha = 0.2f;
}
protected override void UpdateState(ArmedState state)
{
}
}
}

View File

@ -7,14 +7,34 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using OpenTK.Graphics;
using osu.Framework.Configuration;
using OpenTK.Input;
using osu.Framework.Input;
using OpenTK;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Framework.Extensions.IEnumerableExtensions;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="HoldNote"/> hit object.
/// </summary>
public class DrawableHoldNote : DrawableManiaHitObject<HoldNote>
{
private readonly NotePiece headPiece;
private readonly DrawableNote head;
private readonly DrawableNote tail;
private readonly BodyPiece bodyPiece;
private readonly NotePiece tailPiece;
private readonly Container<DrawableHoldNoteTick> tickContainer;
/// <summary>
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
/// </summary>
private double? holdStartTime;
/// <summary>
/// Whether the hold note has been released too early and shouldn't give full score for the release.
/// </summary>
private bool hasBroken;
public DrawableHoldNote(HoldNote hitObject, Bindable<Key> key = null)
: base(hitObject, key)
@ -32,17 +52,39 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
headPiece = new NotePiece
tickContainer = new Container<DrawableHoldNoteTick>
{
RelativeSizeAxes = Axes.Both,
RelativeCoordinateSpace = new Vector2(1, (float)HitObject.Duration)
},
head = new DrawableHeadNote(this, key)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
tailPiece = new NotePiece
tail = new DrawableTailNote(this, key)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.TopCentre
}
});
foreach (var tick in HitObject.Ticks)
{
var drawableTick = new DrawableHoldNoteTick(tick)
{
HoldStartTime = () => holdStartTime
};
// To make the ticks relative to ourselves we need to offset them backwards
drawableTick.Y -= (float)HitObject.StartTime;
tickContainer.Add(drawableTick);
AddNested(drawableTick);
}
AddNested(head);
AddNested(tail);
}
public override Color4 AccentColour
@ -54,9 +96,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
return;
base.AccentColour = value;
headPiece.AccentColour = value;
tickContainer.Children.ForEach(t => t.AccentColour = value);
bodyPiece.AccentColour = value;
tailPiece.AccentColour = value;
head.AccentColour = value;
tail.AccentColour = value;
}
}
@ -64,14 +108,132 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
}
protected override void Update()
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (Time.Current > HitObject.StartTime)
headPiece.Colour = Color4.Green;
if (Time.Current > HitObject.EndTime)
// Make sure the keypress happened within the body of the hold note
if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime)
return false;
if (args.Key != Key)
return false;
if (args.Repeat)
return false;
// The user has pressed during the body of the hold note, after the head note and its hit windows have passed
// and within the limited range of the above if-statement. This state will be managed by the head note if the
// user has pressed during the hit windows of the head note.
holdStartTime = Time.Current;
return true;
}
protected override bool OnKeyUp(InputState state, KeyUpEventArgs args)
{
// Make sure that the user started holding the key during the hold note
if (!holdStartTime.HasValue)
return false;
if (args.Key != Key)
return false;
holdStartTime = null;
// If the key has been released too early, the user should not receive full score for the release
if (!tail.Judged)
hasBroken = true;
return true;
}
/// <summary>
/// The head note of a hold.
/// </summary>
private class DrawableHeadNote : DrawableNote
{
private readonly DrawableHoldNote holdNote;
public DrawableHeadNote(DrawableHoldNote holdNote, Bindable<Key> key = null)
: base(holdNote.HitObject.Head, key)
{
bodyPiece.Colour = Color4.Green;
tailPiece.Colour = Color4.Green;
this.holdNote = holdNote;
RelativePositionAxes = Axes.None;
Y = 0;
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
if (!base.OnKeyDown(state, args))
return false;
// We only want to trigger a holding state from the head if the head has received a judgement
if (!Judged)
return false;
// If the key has been released too early, the user should not receive full score for the release
if (Judgement.Result == HitResult.Miss)
holdNote.hasBroken = true;
// The head note also handles early hits before the body, but we want accurate early hits to count as the body being held
// The body doesn't handle these early early hits, so we have to explicitly set the holding state here
holdNote.holdStartTime = Time.Current;
return true;
}
}
/// <summary>
/// The tail note of a hold.
/// </summary>
private class DrawableTailNote : DrawableNote
{
private readonly DrawableHoldNote holdNote;
public DrawableTailNote(DrawableHoldNote holdNote, Bindable<Key> key = null)
: base(holdNote.HitObject.Tail, key)
{
this.holdNote = holdNote;
RelativePositionAxes = Axes.None;
Y = 0;
}
protected override ManiaJudgement CreateJudgement() => new HoldNoteTailJudgement();
protected override void CheckJudgement(bool userTriggered)
{
base.CheckJudgement(userTriggered);
var tailJudgement = Judgement as HoldNoteTailJudgement;
if (tailJudgement == null)
return;
tailJudgement.HasBroken = holdNote.hasBroken;
}
protected override bool OnKeyUp(InputState state, KeyUpEventArgs args)
{
// Make sure that the user started holding the key during the hold note
if (!holdNote.holdStartTime.HasValue)
return false;
if (Judgement.Result != HitResult.None)
return false;
if (args.Key != Key)
return false;
UpdateJudgement(true);
// Handled by the hold note, which will set holding = false
return false;
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
// Tail doesn't handle key down
return false;
}
}
}

View File

@ -0,0 +1,121 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="HoldNoteTick"/> hit object.
/// </summary>
public class DrawableHoldNoteTick : DrawableManiaHitObject<HoldNoteTick>
{
/// <summary>
/// References the time at which the user started holding the hold note.
/// </summary>
public Func<double?> HoldStartTime;
/// <summary>
/// References whether the user is currently holding the hold note.
/// </summary>
public Func<bool> IsHolding;
private readonly Container glowContainer;
public DrawableHoldNoteTick(HoldNoteTick hitObject)
: base(hitObject)
{
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
RelativeSizeAxes = Axes.X;
Size = new Vector2(1);
Children = new[]
{
glowContainer = new CircularContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
};
// Set the default glow
AccentColour = Color4.White;
}
public override Color4 AccentColour
{
get { return base.AccentColour; }
set
{
base.AccentColour = value;
glowContainer.EdgeEffect = new EdgeEffect
{
Type = EdgeEffectType.Glow,
Radius = 2f,
Roundness = 15f,
Colour = value.Opacity(0.3f)
};
}
}
protected override ManiaJudgement CreateJudgement() => new HoldNoteTickJudgement();
protected override void CheckJudgement(bool userTriggered)
{
if (!userTriggered)
return;
if (Time.Current < HitObject.StartTime)
return;
if (HoldStartTime?.Invoke() > HitObject.StartTime)
return;
Judgement.ManiaResult = ManiaHitResult.Perfect;
Judgement.Result = HitResult.Hit;
}
protected override void UpdateState(ArmedState state)
{
switch (State)
{
case ArmedState.Hit:
AccentColour = Color4.Green;
break;
}
}
protected override void Update()
{
if (Judgement.Result != HitResult.None)
return;
if (IsHolding?.Invoke() != true)
return;
UpdateJudgement(true);
}
}
}

View File

@ -13,6 +13,9 @@ using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
/// <summary>
/// Visualises a <see cref="Note"/> hit object.
/// </summary>
public class DrawableNote : DrawableManiaHitObject<Note>
{
private readonly NotePiece headPiece;

View File

@ -1,10 +1,9 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Audio;
using System.Collections.Generic;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Database;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Mania.Objects
@ -12,32 +11,99 @@ namespace osu.Game.Rulesets.Mania.Objects
/// <summary>
/// Represents a hit object which requires pressing, holding, and releasing a key.
/// </summary>
public class HoldNote : Note, IHasEndTime
public class HoldNote : ManiaHitObject, IHasEndTime
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// </summary>
private const double release_window_lenience = 1.5;
public double Duration { get; set; }
public double EndTime => StartTime + Duration;
/// <summary>
/// The samples to be played when this hold note is released.
/// </summary>
public SampleInfoList EndSamples = new SampleInfoList();
private double duration;
public double Duration
{
get { return duration; }
set
{
duration = value;
Tail.StartTime = EndTime;
}
}
public override double StartTime
{
get { return base.StartTime; }
set
{
base.StartTime = value;
Head.StartTime = value;
Tail.StartTime = EndTime;
}
}
/// <summary>
/// The key-release hit windows for this hold note.
/// The head note of the hold.
/// </summary>
public HitWindows ReleaseHitWindows { get; protected set; } = new HitWindows();
public readonly Note Head = new Note();
/// <summary>
/// The tail note of the hold.
/// </summary>
public readonly Note Tail = new TailNote();
/// <summary>
/// The time between ticks of this hold.
/// </summary>
private double tickSpacing = 50;
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(controlPointInfo, difficulty);
ReleaseHitWindows = HitWindows * release_window_lenience;
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
Head.ApplyDefaults(controlPointInfo, difficulty);
Tail.ApplyDefaults(controlPointInfo, difficulty);
}
/// <summary>
/// The scoring scoring ticks of the hold note.
/// </summary>
public IEnumerable<HoldNoteTick> Ticks => ticks ?? (ticks = createTicks());
private List<HoldNoteTick> ticks;
private List<HoldNoteTick> createTicks()
{
var ret = new List<HoldNoteTick>();
if (tickSpacing == 0)
return ret;
for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing)
{
ret.Add(new HoldNoteTick
{
StartTime = t
});
}
return ret;
}
/// <summary>
/// The tail of the hold note.
/// </summary>
private class TailNote : Note
{
/// <summary>
/// Lenience of release hit windows. This is to make cases where the hold note release
/// is timed alongside presses of other hit objects less awkward.
/// </summary>
private const double release_window_lenience = 1.5;
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(controlPointInfo, difficulty);
HitWindows *= release_window_lenience;
}
}
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Mania.Objects
{
/// <summary>
/// A scoring tick of a hold note.
/// </summary>
public class HoldNoteTick : ManiaHitObject
{
}
}

View File

@ -1,8 +1,13 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mania.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
@ -10,6 +15,143 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor<ManiaHitObject, ManiaJudgement>
{
/// <summary>
/// The maximum score achievable.
/// Does _not_ include bonus score - for bonus score see <see cref="bonusScore"/>.
/// </summary>
private const int max_score = 1000000;
/// <summary>
/// The amount of the score attributed to combo.
/// </summary>
private const double combo_portion_max = max_score * 0.2;
/// <summary>
/// The amount of the score attributed to accuracy.
/// </summary>
private const double accuracy_portion_max = max_score * 0.8;
/// <summary>
/// The factor used to determine relevance of combos.
/// </summary>
private const double combo_base = 4;
/// <summary>
/// The combo value at which hit objects result in the max score possible.
/// </summary>
private const int combo_relevance_cap = 400;
/// <summary>
/// The hit HP multiplier at OD = 0.
/// </summary>
private const double hp_multiplier_min = 0.75;
/// <summary>
/// The hit HP multiplier at OD = 0.
/// </summary>
private const double hp_multiplier_mid = 0.85;
/// <summary>
/// The hit HP multiplier at OD = 0.
/// </summary>
private const double hp_multiplier_max = 1;
/// <summary>
/// The default BAD hit HP increase.
/// </summary>
private const double hp_increase_bad = 0.005;
/// <summary>
/// The default OK hit HP increase.
/// </summary>
private const double hp_increase_ok = 0.010;
/// <summary>
/// The default GOOD hit HP increase.
/// </summary>
private const double hp_increase_good = 0.035;
/// <summary>
/// The default tick hit HP increase.
/// </summary>
private const double hp_increase_tick = 0.040;
/// <summary>
/// The default GREAT hit HP increase.
/// </summary>
private const double hp_increase_great = 0.055;
/// <summary>
/// The default PERFECT hit HP increase.
/// </summary>
private const double hp_increase_perfect = 0.065;
/// <summary>
/// The MISS HP multiplier at OD = 0.
/// </summary>
private const double hp_multiplier_miss_min = 0.5;
/// <summary>
/// The MISS HP multiplier at OD = 5.
/// </summary>
private const double hp_multiplier_miss_mid = 0.75;
/// <summary>
/// The MISS HP multiplier at OD = 10.
/// </summary>
private const double hp_multiplier_miss_max = 1;
/// <summary>
/// The default MISS HP increase.
/// </summary>
private const double hp_increase_miss = -0.125;
/// <summary>
/// The MISS HP multiplier. This is multiplied to the miss hp increase.
/// </summary>
private double hpMissMultiplier = 1;
/// <summary>
/// The HIT HP multiplier. This is multiplied to hit hp increases.
/// </summary>
private double hpMultiplier = 1;
/// <summary>
/// The cumulative combo portion of the score.
/// </summary>
private double comboScore => combo_portion_max * comboPortion / maxComboPortion;
/// <summary>
/// The cumulative accuracy portion of the score.
/// </summary>
private double accuracyScore => accuracy_portion_max * Math.Pow(Accuracy, 4) * totalHits / maxTotalHits;
/// <summary>
/// The cumulative bonus score.
/// This is added on top of <see cref="max_score"/>, thus the total score can exceed <see cref="max_score"/>.
/// </summary>
private double bonusScore;
/// <summary>
/// The <see cref="comboPortion"/> achieved by a perfect playthrough.
/// </summary>
private double maxComboPortion;
/// <summary>
/// The portion of the score dedicated to combo.
/// </summary>
private double comboPortion;
/// <summary>
/// The <see cref="totalHits"/> achieved by a perfect playthrough.
/// </summary>
private int maxTotalHits;
/// <summary>
/// The total hits.
/// </summary>
private int totalHits;
public ManiaScoreProcessor()
{
}
@ -19,8 +161,124 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
}
protected override void ComputeTargets(Beatmap<ManiaHitObject> beatmap)
{
BeatmapDifficulty difficulty = beatmap.BeatmapInfo.Difficulty;
hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max);
hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max);
while (true)
{
foreach (var obj in beatmap.HitObjects)
{
var holdNote = obj as HoldNote;
if (obj is Note)
{
AddJudgement(new ManiaJudgement
{
Result = HitResult.Hit,
ManiaResult = ManiaHitResult.Perfect
});
}
else if (holdNote != null)
{
// Head
AddJudgement(new ManiaJudgement
{
Result = HitResult.Hit,
ManiaResult = ManiaJudgement.MAX_HIT_RESULT
});
// Ticks
int tickCount = holdNote.Ticks.Count();
for (int i = 0; i < tickCount; i++)
{
AddJudgement(new HoldNoteTickJudgement
{
Result = HitResult.Hit,
ManiaResult = ManiaJudgement.MAX_HIT_RESULT,
});
}
AddJudgement(new HoldNoteTailJudgement
{
Result = HitResult.Hit,
ManiaResult = ManiaJudgement.MAX_HIT_RESULT
});
}
}
if (!HasFailed)
break;
hpMultiplier *= 1.01;
hpMissMultiplier *= 0.98;
Reset();
}
maxTotalHits = totalHits;
maxComboPortion = comboPortion;
}
protected override void OnNewJudgement(ManiaJudgement judgement)
{
bool isTick = judgement is HoldNoteTickJudgement;
if (!isTick)
totalHits++;
switch (judgement.Result)
{
case HitResult.Miss:
Health.Value += hpMissMultiplier * hp_increase_miss;
break;
case HitResult.Hit:
if (isTick)
{
Health.Value += hpMultiplier * hp_increase_tick;
bonusScore += judgement.ResultValueForScore;
}
else
{
switch (judgement.ManiaResult)
{
case ManiaHitResult.Bad:
Health.Value += hpMultiplier * hp_increase_bad;
break;
case ManiaHitResult.Ok:
Health.Value += hpMultiplier * hp_increase_ok;
break;
case ManiaHitResult.Good:
Health.Value += hpMultiplier * hp_increase_good;
break;
case ManiaHitResult.Great:
Health.Value += hpMultiplier * hp_increase_great;
break;
case ManiaHitResult.Perfect:
Health.Value += hpMultiplier * hp_increase_perfect;
break;
}
// A factor that is applied to make higher combos more relevant
double comboRelevance = Math.Min(Math.Max(0.5, Math.Log(Combo.Value, combo_base)), Math.Log(combo_relevance_cap, combo_base));
comboPortion += judgement.ResultValueForScore * comboRelevance;
}
break;
}
int scoreForAccuracy = 0;
int maxScoreForAccuracy = 0;
foreach (var j in Judgements)
{
scoreForAccuracy += j.ResultValueForAccuracy;
maxScoreForAccuracy += j.MaxResultValueForAccuracy;
}
Accuracy.Value = (double)scoreForAccuracy / maxScoreForAccuracy;
TotalScore.Value = comboScore + accuracyScore + bonusScore;
}
protected override void Reset()
@ -28,6 +286,10 @@ namespace osu.Game.Rulesets.Mania.Scoring
base.Reset();
Health.Value = 1;
bonusScore = 0;
comboPortion = 0;
totalHits = 0;
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using OpenTK;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Timing
{
@ -128,6 +129,8 @@ namespace osu.Game.Rulesets.Mania.Timing
/// </summary>
private class AutoTimeRelativeContainer : Container
{
protected override IComparer<Drawable> DepthComparer => new HitObjectReverseStartTimeComparer();
public override void InvalidateFromChild(Invalidation invalidation)
{
// We only want to re-compute our size when a child's size or position has changed

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Input;
@ -188,7 +187,11 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
public void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> hitObject) => ControlPointContainer.Add(hitObject);
public void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> hitObject)
{
hitObject.AccentColour = AccentColour;
ControlPointContainer.Add(hitObject);
}
private bool onKeyDown(InputState state, KeyDownEventArgs args)
{

View File

@ -6,9 +6,11 @@ using System.Collections.Generic;
using System.Linq;
using OpenTK;
using OpenTK.Input;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Lists;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Beatmaps;
@ -85,6 +87,34 @@ namespace osu.Game.Rulesets.Mania.UI
};
}
[BackgroundDependencyLoader]
private void load()
{
var maniaPlayfield = (ManiaPlayfield)Playfield;
double lastObjectTime = (Objects.LastOrDefault() as IHasEndTime)?.EndTime ?? Objects.LastOrDefault()?.StartTime ?? double.MaxValue;
SortedList<TimingControlPoint> timingPoints = Beatmap.ControlPointInfo.TimingPoints;
for (int i = 0; i < timingPoints.Count; i++)
{
TimingControlPoint point = timingPoints[i];
// Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - point.BeatLength : lastObjectTime + point.BeatLength * (int)point.TimeSignature;
int index = 0;
for (double t = timingPoints[i].Time; Precision.DefinitelyBigger(endTime, t); t += point.BeatLength, index++)
{
maniaPlayfield.Add(new DrawableBarLine(new BarLine
{
StartTime = t,
ControlPoint = point,
BeatIndex = index
}));
}
}
}
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this);
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter() => new ManiaBeatmapConverter();

View File

@ -21,6 +21,7 @@ using osu.Game.Rulesets.Mania.Timing;
using osu.Framework.Input;
using osu.Framework.Graphics.Transforms;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Mania.Objects.Drawables;
namespace osu.Game.Rulesets.Mania.UI
{
@ -57,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly FlowContainer<Column> columns;
public IEnumerable<Column> Columns => columns.Children;
private readonly ControlPointContainer barlineContainer;
private readonly ControlPointContainer barLineContainer;
private List<Color4> normalColumnColours = new List<Color4>();
private Color4 specialColumnColour;
@ -77,35 +78,51 @@ namespace osu.Game.Rulesets.Mania.UI
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
},
columns = new FillFlowContainer<Column>
{
Name = "Columns",
Name = "Masked elements",
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Left = 1, Right = 1 },
Spacing = new Vector2(1, 0)
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black
},
columns = new FillFlowContainer<Column>
{
Name = "Columns",
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Left = 1, Right = 1 },
Spacing = new Vector2(1, 0)
}
}
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = HIT_TARGET_POSITION },
Children = new[]
{
barlineContainer = new ControlPointContainer(timingChanges)
barLineContainer = new ControlPointContainer(timingChanges)
{
Name = "Bar lines",
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y
// Width is set in the Update method
}
}
}
@ -190,6 +207,7 @@ namespace osu.Game.Rulesets.Mania.UI
}
public override void Add(DrawableHitObject<ManiaHitObject, ManiaJudgement> h) => Columns.ElementAt(h.HitObject.Column).Add(h);
public void Add(DrawableBarLine barline) => barLineContainer.Add(barline);
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
{
@ -224,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.UI
timeSpan = MathHelper.Clamp(timeSpan, time_span_min, time_span_max);
barlineContainer.TimeSpan = value;
barLineContainer.TimeSpan = value;
Columns.ForEach(c => c.ControlPointContainer.TimeSpan = value);
}
}
@ -234,6 +252,13 @@ namespace osu.Game.Rulesets.Mania.UI
TransformTo(() => TimeSpan, newTimeSpan, duration, easing, new TransformTimeSpan());
}
protected override void Update()
{
// Due to masking differences, it is not possible to get the width of the columns container automatically
// While masking on effectively only the Y-axis, so we need to set the width of the bar line container manually
barLineContainer.Width = columns.Width;
}
private class TransformTimeSpan : Transform<double>
{
public override double CurrentValue

View File

@ -57,17 +57,23 @@
<Compile Include="Beatmaps\Patterns\Pattern.cs" />
<Compile Include="MathUtils\FastRandom.cs" />
<Compile Include="Judgements\HitWindows.cs" />
<Compile Include="Judgements\HoldNoteTailJudgement.cs" />
<Compile Include="Judgements\HoldNoteTickJudgement.cs" />
<Compile Include="Judgements\ManiaHitResult.cs" />
<Compile Include="Judgements\ManiaJudgement.cs" />
<Compile Include="ManiaDifficultyCalculator.cs" />
<Compile Include="Objects\Drawables\DrawableBarLine.cs" />
<Compile Include="Objects\Drawables\DrawableHoldNote.cs" />
<Compile Include="Objects\Drawables\DrawableHoldNoteTick.cs" />
<Compile Include="Objects\Drawables\DrawableManiaHitObject.cs" />
<Compile Include="Objects\Drawables\DrawableNote.cs" />
<Compile Include="Objects\Drawables\Pieces\BodyPiece.cs" />
<Compile Include="Objects\Drawables\Pieces\NotePiece.cs" />
<Compile Include="Objects\Types\IHasColumn.cs" />
<Compile Include="Scoring\ManiaScoreProcessor.cs" />
<Compile Include="Objects\BarLine.cs" />
<Compile Include="Objects\HoldNote.cs" />
<Compile Include="Objects\HoldNoteTick.cs" />
<Compile Include="Objects\ManiaHitObject.cs" />
<Compile Include="Objects\Note.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />

View File

@ -428,6 +428,9 @@ namespace osu.Game.Beatmaps.Formats
break;
}
}
foreach (var hitObject in beatmap.HitObjects)
hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.Difficulty);
}
internal enum LegacySampleBank

View File

@ -0,0 +1,84 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using osu.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using System.Linq;
namespace osu.Game.Graphics.UserInterface
{
public class BreadcrumbControl<T> : OsuTabControl<T>
{
private const float padding = 10;
protected override TabItem<T> CreateTabItem(T value) => new BreadcrumbTabItem(value);
public BreadcrumbControl()
{
Height = 26;
TabContainer.Spacing = new Vector2(padding, 0f);
Current.ValueChanged += tab =>
{
foreach (var t in TabContainer.Children.OfType<BreadcrumbTabItem>())
{
var tIndex = TabContainer.IndexOf(t);
var tabIndex = TabContainer.IndexOf(TabMap[tab]);
t.State = tIndex < tabIndex ? Visibility.Hidden : Visibility.Visible;
t.Chevron.FadeTo(tIndex <= tabIndex ? 0f : 1f, 500, EasingTypes.OutQuint);
}
};
}
private class BreadcrumbTabItem : OsuTabItem, IStateful<Visibility>
{
public readonly TextAwesome Chevron;
//don't allow clicking between transitions and don't make the chevron clickable
protected override bool InternalContains(Vector2 screenSpacePos) => Alpha == 1f && Text.Contains(screenSpacePos);
public override bool HandleInput => State == Visibility.Visible;
private Visibility state;
public Visibility State
{
get { return state; }
set
{
if (value == state) return;
state = value;
const float transition_duration = 500;
if (State == Visibility.Visible)
{
FadeIn(transition_duration, EasingTypes.OutQuint);
ScaleTo(new Vector2(1f), transition_duration, EasingTypes.OutQuint);
}
else
{
FadeOut(transition_duration, EasingTypes.OutQuint);
ScaleTo(new Vector2(0.8f, 1f), transition_duration, EasingTypes.OutQuint);
}
}
}
public BreadcrumbTabItem(T value) : base(value)
{
Text.TextSize = 16;
Padding = new MarginPadding { Right = padding + 8 }; //padding + chevron width
Add(Chevron = new TextAwesome
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreLeft,
TextSize = 12,
Icon = FontAwesome.fa_chevron_right,
Margin = new MarginPadding { Left = padding },
Alpha = 0f,
});
}
}
}
}

View File

@ -5,18 +5,21 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Input;
using OpenTK;
using OpenTK.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints;
using osu.Framework.Audio.Track;
using System;
namespace osu.Game.Graphics.UserInterface
{
public class TwoLayerButton : ClickableContainer
{
private readonly TextAwesome icon;
private readonly BouncingIcon bouncingIcon;
public Box IconLayer;
public Box TextLayer;
@ -95,11 +98,10 @@ namespace osu.Game.Graphics.UserInterface
},
}
},
icon = new TextAwesome
bouncingIcon = new BouncingIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
TextSize = 25,
},
}
},
@ -146,7 +148,7 @@ namespace osu.Game.Graphics.UserInterface
{
set
{
icon.Icon = value;
bouncingIcon.Icon = value;
}
}
@ -162,58 +164,20 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(InputState state)
{
icon.ClearTransforms();
ResizeTo(SIZE_EXTENDED, transform_time, EasingTypes.OutElastic);
int duration = 0; //(int)(Game.Audio.BeatLength / 2);
if (duration == 0) duration = pulse_length;
IconLayer.FadeColour(HoverColour, transform_time, EasingTypes.OutElastic);
const double offset = 0; //(1 - Game.Audio.SyncBeatProgress) * duration;
double startTime = Time.Current + offset;
// basic pulse
icon.Transforms.Add(new TransformScale
{
StartValue = new Vector2(1.1f),
EndValue = Vector2.One,
StartTime = startTime,
EndTime = startTime + duration,
Easing = EasingTypes.Out,
LoopCount = -1,
LoopDelay = duration
});
bouncingIcon.ScaleTo(1.1f, transform_time, EasingTypes.OutElastic);
return true;
}
protected override void OnHoverLost(InputState state)
{
icon.ClearTransforms();
ResizeTo(SIZE_RETRACTED, transform_time, EasingTypes.OutElastic);
IconLayer.FadeColour(TextLayer.Colour, transform_time, EasingTypes.OutElastic);
int duration = 0; //(int)(Game.Audio.BeatLength);
if (duration == 0) duration = pulse_length * 2;
const double offset = 0; //(1 - Game.Audio.SyncBeatProgress) * duration;
double startTime = Time.Current + offset;
// slow pulse
icon.Transforms.Add(new TransformScale
{
StartValue = new Vector2(1.1f),
EndValue = Vector2.One,
StartTime = startTime,
EndTime = startTime + duration,
Easing = EasingTypes.Out,
LoopCount = -1,
LoopDelay = duration
});
bouncingIcon.ScaleTo(1, transform_time, EasingTypes.OutElastic);
}
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
@ -239,5 +203,45 @@ namespace osu.Game.Graphics.UserInterface
return base.OnClick(state);
}
private class BouncingIcon : BeatSyncedContainer
{
private const double beat_in_time = 60;
private readonly TextAwesome icon;
public FontAwesome Icon { set { icon.Icon = value; } }
public BouncingIcon()
{
EarlyActivationMilliseconds = beat_in_time;
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
icon = new TextAwesome
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
TextSize = 25
}
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
var beatLength = timingPoint.BeatLength;
float amplitudeAdjust = Math.Min(1, 0.4f + amplitudes.Maximum);
if (beatIndex < 0) return;
icon.ScaleTo(1 - 0.1f * amplitudeAdjust, beat_in_time, EasingTypes.Out);
using (icon.BeginDelayedSequence(beat_in_time))
icon.ScaleTo(1, beatLength * 2, EasingTypes.OutQuint);
}
}
}
}

View File

@ -27,6 +27,7 @@ namespace osu.Game.Overlays.Mods
public class ModButton : ModButtonEmpty, IHasTooltip
{
private ModIcon foregroundIcon;
private ModIcon backgroundIcon;
private readonly SpriteText text;
private readonly Container<ModIcon> iconsContainer;
private SampleChannel sampleOn, sampleOff;
@ -35,38 +36,67 @@ namespace osu.Game.Overlays.Mods
public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
private int _selectedIndex = -1;
private int selectedIndex
private const EasingTypes mod_switch_easing = EasingTypes.InOutSine;
private const double mod_switch_duration = 120;
// A selected index of -1 means not selected.
private int selectedIndex = -1;
protected int SelectedIndex
{
get
{
return _selectedIndex;
return selectedIndex;
}
set
{
if (value == _selectedIndex) return;
_selectedIndex = value;
if (value == selectedIndex) return;
int direction = value < selectedIndex ? -1 : 1;
bool beforeSelected = Selected;
Mod modBefore = SelectedMod ?? Mods[0];
if (value >= Mods.Length)
selectedIndex = -1;
else if (value < -1)
selectedIndex = Mods.Length - 1;
else
selectedIndex = value;
Mod modAfter = SelectedMod ?? Mods[0];
if (beforeSelected != Selected)
{
_selectedIndex = -1;
}
else if (value <= -2)
{
_selectedIndex = Mods.Length - 1;
iconsContainer.RotateTo(Selected ? 5f : 0f, 300, EasingTypes.OutElastic);
iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, EasingTypes.OutElastic);
}
if (modBefore != modAfter)
{
const float rotate_angle = 16;
foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing);
backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing);
backgroundIcon.Icon = modAfter.Icon;
using (iconsContainer.BeginDelayedSequence(mod_switch_duration, true))
{
foregroundIcon.RotateTo(-rotate_angle * direction);
foregroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing);
backgroundIcon.RotateTo(rotate_angle * direction);
backgroundIcon.RotateTo(0f, mod_switch_duration, mod_switch_easing);
iconsContainer.Schedule(() => displayMod(modAfter));
}
}
iconsContainer.RotateTo(Selected ? 5f : 0f, 300, EasingTypes.OutElastic);
iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, EasingTypes.OutElastic);
foregroundIcon.Highlighted = Selected;
if (mod != null)
displayMod(SelectedMod ?? Mods[0]);
}
}
public bool Selected => selectedIndex != -1;
public bool Selected => SelectedIndex != -1;
private Color4 selectedColour;
public Color4 SelectedColour
@ -117,7 +147,7 @@ namespace osu.Game.Overlays.Mods
// the mods from Mod, only multiple if Mod is a MultiMod
public override Mod SelectedMod => Mods.ElementAtOrDefault(selectedIndex);
public override Mod SelectedMod => Mods.ElementAtOrDefault(SelectedIndex);
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@ -142,23 +172,25 @@ namespace osu.Game.Overlays.Mods
public void SelectNext()
{
(++selectedIndex == -1 ? sampleOff : sampleOn).Play();
(++SelectedIndex == -1 ? sampleOff : sampleOn).Play();
Action?.Invoke(SelectedMod);
}
public void SelectPrevious()
{
(--selectedIndex == -1 ? sampleOff : sampleOn).Play();
(--SelectedIndex == -1 ? sampleOff : sampleOn).Play();
Action?.Invoke(SelectedMod);
}
public void Deselect()
{
selectedIndex = -1;
SelectedIndex = -1;
}
private void displayMod(Mod mod)
{
if (backgroundIcon != null)
backgroundIcon.Icon = foregroundIcon.Icon;
foregroundIcon.Icon = mod.Icon;
text.Text = mod.Name;
}
@ -170,17 +202,17 @@ namespace osu.Game.Overlays.Mods
{
iconsContainer.Add(new[]
{
new ModIcon(Mods[0])
backgroundIcon = new ModIcon(Mods[1])
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Position = new Vector2(1.5f),
},
foregroundIcon = new ModIcon(Mods[0])
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Position = new Vector2(-1.5f),
},

View File

@ -266,24 +266,27 @@ namespace osu.Game.Overlays
{
progressBar.IsEnabled = beatmap != null;
bool audioEquals = beatmapBacking.Value?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) ?? false;
TransformDirection direction = TransformDirection.None;
TransformDirection direction;
if (audioEquals)
direction = TransformDirection.None;
else if (queuedDirection.HasValue)
if (current != null)
{
direction = queuedDirection.Value;
queuedDirection = null;
}
else
{
//figure out the best direction based on order in playlist.
var last = current == null ? -1 : playlist.BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo.ID).Count();
var next = beatmapBacking.Value == null ? -1 : playlist.BeatmapSets.TakeWhile(b => b.ID != beatmapBacking.Value.BeatmapSetInfo.ID).Count();
bool audioEquals = beatmapBacking.Value?.BeatmapInfo?.AudioEquals(current.BeatmapInfo) ?? false;
direction = last > next ? TransformDirection.Prev : TransformDirection.Next;
if (audioEquals)
direction = TransformDirection.None;
else if (queuedDirection.HasValue)
{
direction = queuedDirection.Value;
queuedDirection = null;
}
else
{
//figure out the best direction based on order in playlist.
var last = playlist.BeatmapSets.TakeWhile(b => b.ID != current.BeatmapSetInfo.ID).Count();
var next = beatmapBacking.Value == null ? -1 : playlist.BeatmapSets.TakeWhile(b => b.ID != beatmapBacking.Value.BeatmapSetInfo.ID).Count();
direction = last > next ? TransformDirection.Prev : TransformDirection.Next;
}
}
current = beatmapBacking.Value;

View File

@ -15,28 +15,51 @@ using System.Linq;
namespace osu.Game.Rulesets.Objects.Drawables
{
public abstract class DrawableHitObject<TObject, TJudgement> : Container
where TObject : HitObject
where TJudgement : Judgement
public abstract class DrawableHitObject : Container
{
public event Action<DrawableHitObject<TObject, TJudgement>> OnJudgement;
public TObject HitObject;
public readonly HitObject HitObject;
/// <summary>
/// The colour used for various elements of this DrawableHitObject.
/// </summary>
public virtual Color4 AccentColour { get; set; }
protected DrawableHitObject(HitObject hitObject)
{
HitObject = hitObject;
}
}
public abstract class DrawableHitObject<TObject> : DrawableHitObject
where TObject : HitObject
{
public new readonly TObject HitObject;
protected DrawableHitObject(TObject hitObject)
: base(hitObject)
{
HitObject = hitObject;
}
}
public abstract class DrawableHitObject<TObject, TJudgement> : DrawableHitObject<TObject>
where TObject : HitObject
where TJudgement : Judgement
{
public event Action<DrawableHitObject<TObject, TJudgement>> OnJudgement;
public override bool HandleInput => Interactive;
public bool Interactive = true;
public TJudgement Judgement;
protected abstract TJudgement CreateJudgement();
protected List<SampleChannel> Samples = new List<SampleChannel>();
protected abstract void UpdateState(ArmedState state);
protected DrawableHitObject(TObject hitObject)
: base(hitObject)
{
}
private ArmedState state;
public ArmedState State
@ -59,8 +82,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
}
protected List<SampleChannel> Samples = new List<SampleChannel>();
protected void PlaySamples()
{
Samples.ForEach(s => s?.Play());
@ -79,11 +100,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary>
public bool Judged => (Judgement?.Result ?? HitResult.None) != HitResult.None && (NestedHitObjects?.All(h => h.Judged) ?? true);
protected DrawableHitObject(TObject hitObject)
{
HitObject = hitObject;
}
/// <summary>
/// Process a hit of this hitobject. Carries out judgement.
/// </summary>
@ -176,5 +192,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
h.OnJudgement += d => OnJudgement?.Invoke(d);
nestedHitObjects.Add(h);
}
protected abstract TJudgement CreateJudgement();
protected abstract void UpdateState(ArmedState state);
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Objects
/// <summary>
/// The time at which the HitObject starts.
/// </summary>
public double StartTime;
public virtual double StartTime { get; set; }
/// <summary>
/// The samples to be played when this hit object is hit.

View File

@ -0,0 +1,55 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Objects
{
/// <summary>
/// Compares two hit objects by their start time, falling back to creation order if their start time is equal.
/// </summary>
public class HitObjectStartTimeComparer : Drawable.CreationOrderDepthComparer
{
public override int Compare(Drawable x, Drawable y)
{
var hitObjectX = x as DrawableHitObject;
var hitObjectY = y as DrawableHitObject;
// If either of the two drawables are not hit objects, fall back to the base comparer
if (hitObjectX?.HitObject == null || hitObjectY?.HitObject == null)
return base.Compare(x, y);
// Compare by start time
int i = hitObjectX.HitObject.StartTime.CompareTo(hitObjectY.HitObject.StartTime);
if (i != 0)
return i;
return base.Compare(x, y);
}
}
/// <summary>
/// Compares two hit objects by their start time, falling back to creation order if their start time is equal.
/// This will compare the two hit objects in reverse order.
/// </summary>
public class HitObjectReverseStartTimeComparer : Drawable.ReverseCreationOrderDepthComparer
{
public override int Compare(Drawable x, Drawable y)
{
var hitObjectX = x as DrawableHitObject;
var hitObjectY = y as DrawableHitObject;
// If either of the two drawables are not hit objects, fall back to the base comparer
if (hitObjectX?.HitObject == null || hitObjectY?.HitObject == null)
return base.Compare(x, y);
// Compare by start time
int i = hitObjectY.HitObject.StartTime.CompareTo(hitObjectX.HitObject.StartTime);
if (i != 0)
return i;
return base.Compare(x, y);
}
}
}

View File

@ -6,20 +6,30 @@ using System;
using System.Collections.Generic;
using OpenTK;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Database;
namespace osu.Game.Rulesets.Objects.Legacy
{
internal abstract class ConvertSlider : HitObject, IHasCurve
{
/// <summary>
/// Scoring distance with a speed-adjusted beat length of 1 second.
/// </summary>
private const float base_scoring_distance = 100;
public List<Vector2> ControlPoints { get; set; }
public CurveType CurveType { get; set; }
public double Distance { get; set; }
public List<SampleInfoList> RepeatSamples { get; set; }
public int RepeatCount { get; set; } = 1;
public double EndTime { get; set; }
public double Duration { get; set; }
public double EndTime => StartTime + RepeatCount * Distance / Velocity;
public double Duration => EndTime - StartTime;
public double Velocity = 1;
public Vector2 PositionAt(double progress)
{
@ -35,5 +45,17 @@ namespace osu.Game.Rulesets.Objects.Legacy
{
throw new NotImplementedException();
}
public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaults(controlPointInfo, difficulty);
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime);
double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier / difficultyPoint.SpeedMultiplier;
Velocity = scoringDistance / timingPoint.BeatLength;
}
}
}

View File

@ -176,6 +176,7 @@
<Compile Include="Rulesets\Objects\Drawables\IDrawableHitObjectWithProxiedApproach.cs" />
<Compile Include="Rulesets\Judgements\Judgement.cs" />
<Compile Include="Rulesets\Objects\HitObjectParser.cs" />
<Compile Include="Rulesets\Objects\HitObjectStartTimeComparer.cs" />
<Compile Include="Rulesets\Objects\Types\IHasCombo.cs" />
<Compile Include="Rulesets\Objects\Types\IHasEndTime.cs" />
<Compile Include="Rulesets\Objects\Types\IHasDistance.cs" />
@ -450,6 +451,7 @@
<Compile Include="Overlays\Direct\SlimEnumDropdown.cs" />
<Compile Include="Graphics\Containers\ReverseDepthFillFlowContainer.cs" />
<Compile Include="Database\RankStatus.cs" />
<Compile Include="Graphics\UserInterface\BreadcrumbControl.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu-framework\osu.Framework\osu.Framework.csproj">