Merge branch 'master' into mania-slidertick-lighting

This commit is contained in:
Dean Herbert 2020-08-19 21:19:25 +09:00 committed by GitHub
commit 37f40f4a09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 343 additions and 86 deletions

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Mania.Tests.Mods
{
public class TestSceneManiaModInvert : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Test]
public void TestInversion() => CreateModTest(new ModTestData
{
Mod = new ManiaModInvert(),
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2
});
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
@ -15,7 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components
AccentColour.Value = colours.Yellow;
Background.Alpha = 0.5f;
Foreground.Alpha = 0;
}
protected override Drawable CreateForeground() => base.CreateForeground().With(d => d.Alpha = 0);
}
}

View File

@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Mania.Judgements
{
public class HoldNoteTickJudgement : ManiaJudgement
{
protected override int NumericResultFor(HitResult result) => 20;
protected override int NumericResultFor(HitResult result) => result == MaxResult ? 20 : 0;
protected override double HealthIncreaseFor(HitResult result)
{

View File

@ -220,6 +220,7 @@ namespace osu.Game.Rulesets.Mania
new ManiaModDualStages(),
new ManiaModMirror(),
new ManiaModDifficultyAdjust(),
new ManiaModInvert(),
};
case ModType.Automation:

View File

@ -0,0 +1,81 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModInvert : Mod, IApplicableAfterBeatmapConversion
{
public override string Name => "Invert";
public override string Acronym => "IN";
public override double ScoreMultiplier => 1;
public override string Description => "Hold the keys. To the beat.";
public override IconUsage? Icon => FontAwesome.Solid.YinYang;
public override ModType Type => ModType.Conversion;
public void ApplyToBeatmap(IBeatmap beatmap)
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
var newObjects = new List<ManiaHitObject>();
foreach (var column in maniaBeatmap.HitObjects.GroupBy(h => h.Column))
{
var newColumnObjects = new List<ManiaHitObject>();
var locations = column.OfType<Note>().Select(n => (startTime: n.StartTime, samples: n.Samples))
.Concat(column.OfType<HoldNote>().SelectMany(h => new[]
{
(startTime: h.StartTime, samples: h.GetNodeSamples(0)),
(startTime: h.EndTime, samples: h.GetNodeSamples(1))
}))
.OrderBy(h => h.startTime).ToList();
for (int i = 0; i < locations.Count - 1; i++)
{
// Full duration of the hold note.
double duration = locations[i + 1].startTime - locations[i].startTime;
// Beat length at the end of the hold note.
double beatLength = beatmap.ControlPointInfo.TimingPointAt(locations[i + 1].startTime).BeatLength;
// Decrease the duration by at most a 1/4 beat to ensure there's no instantaneous notes.
duration = Math.Max(duration / 2, duration - beatLength / 4);
newColumnObjects.Add(new HoldNote
{
Column = column.Key,
StartTime = locations[i].startTime,
Duration = duration,
Samples = locations[i].samples,
NodeSamples = new List<IList<HitSampleInfo>>
{
locations[i].samples,
locations[i + 1].samples
}
});
}
newObjects.AddRange(newColumnObjects);
}
maniaBeatmap.HitObjects = newObjects.OrderBy(h => h.StartTime).ToList();
// No breaks
maniaBeatmap.Breaks.Clear();
}
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
private readonly Container<DrawableHoldNoteTail> tailContainer;
private readonly Container<DrawableHoldNoteTick> tickContainer;
private readonly Drawable bodyPiece;
private readonly SkinnableDrawable bodyPiece;
/// <summary>
/// Time at which the user started holding this hold note. Null if the user is not holding this hold note.
@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
RelativeSizeAxes = Axes.X;
AddRangeInternal(new[]
AddRangeInternal(new Drawable[]
{
bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece
{
@ -135,6 +135,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// Samples are played by the head/tail notes.
}
public override void OnKilled()
{
base.OnKilled();
(bodyPiece.Drawable as IHoldNoteBody)?.Recycle();
}
protected override void Update()
{
base.Update();

View File

@ -19,24 +19,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
/// <summary>
/// Represents length-wise portion of a hold note.
/// </summary>
public class DefaultBodyPiece : CompositeDrawable
public class DefaultBodyPiece : CompositeDrawable, IHoldNoteBody
{
protected readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
private readonly IBindable<bool> isHitting = new Bindable<bool>();
protected readonly IBindable<bool> IsHitting = new Bindable<bool>();
protected Drawable Background { get; private set; }
protected BufferedContainer Foreground { get; private set; }
private BufferedContainer subtractionContainer;
private Container subtractionLayer;
private Container foregroundContainer;
public DefaultBodyPiece()
{
Blending = BlendingParameters.Additive;
AddLayout(subtractionCache);
}
[BackgroundDependencyLoader(true)]
@ -45,7 +38,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
InternalChildren = new[]
{
Background = new Box { RelativeSizeAxes = Axes.Both },
Foreground = new BufferedContainer
foregroundContainer = new Container { RelativeSizeAxes = Axes.Both }
};
if (drawableObject != null)
{
var holdNote = (DrawableHoldNote)drawableObject;
AccentColour.BindTo(drawableObject.AccentColour);
IsHitting.BindTo(holdNote.IsHitting);
}
AccentColour.BindValueChanged(onAccentChanged, true);
Recycle();
}
public void Recycle() => foregroundContainer.Child = CreateForeground();
protected virtual Drawable CreateForeground() => new ForegroundPiece
{
AccentColour = { BindTarget = AccentColour },
IsHitting = { BindTarget = IsHitting }
};
private void onAccentChanged(ValueChangedEvent<Color4> accent) => Background.Colour = accent.NewValue.Opacity(0.7f);
private class ForegroundPiece : CompositeDrawable
{
public readonly Bindable<Color4> AccentColour = new Bindable<Color4>();
public readonly IBindable<bool> IsHitting = new Bindable<bool>();
private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize);
private BufferedContainer foregroundBuffer;
private BufferedContainer subtractionBuffer;
private Container subtractionLayer;
public ForegroundPiece()
{
RelativeSizeAxes = Axes.Both;
AddLayout(subtractionCache);
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = foregroundBuffer = new BufferedContainer
{
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
@ -53,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
Children = new Drawable[]
{
new Box { RelativeSizeAxes = Axes.Both },
subtractionContainer = new BufferedContainer
subtractionBuffer = new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
// This is needed because we're blending with another object
@ -77,60 +117,51 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
}
}
};
if (drawableObject != null)
{
var holdNote = (DrawableHoldNote)drawableObject;
AccentColour.BindTo(drawableObject.AccentColour);
isHitting.BindTo(holdNote.IsHitting);
}
AccentColour.BindValueChanged(onAccentChanged, true);
isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value)), true);
}
private void onAccentChanged(ValueChangedEvent<Color4> accent)
{
Foreground.Colour = accent.NewValue.Opacity(0.5f);
Background.Colour = accent.NewValue.Opacity(0.7f);
const float animation_length = 50;
Foreground.ClearTransforms(false, nameof(Foreground.Colour));
if (isHitting.Value)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (Foreground.BeginDelayedSequence(synchronisedOffset))
Foreground.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(Foreground.Colour, animation_length).Loop();
}
subtractionCache.Invalidate();
}
protected override void Update()
{
base.Update();
if (!subtractionCache.IsValid)
{
subtractionLayer.Width = 5;
subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
subtractionLayer.EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.White,
Type = EdgeEffectType.Glow,
Radius = DrawWidth
};
Foreground.ForceRedraw();
subtractionContainer.ForceRedraw();
AccentColour.BindValueChanged(onAccentChanged, true);
IsHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent<Color4>(AccentColour.Value, AccentColour.Value)), true);
}
subtractionCache.Validate();
private void onAccentChanged(ValueChangedEvent<Color4> accent)
{
foregroundBuffer.Colour = accent.NewValue.Opacity(0.5f);
const float animation_length = 50;
foregroundBuffer.ClearTransforms(false, nameof(foregroundBuffer.Colour));
if (IsHitting.Value)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (foregroundBuffer.BeginDelayedSequence(synchronisedOffset))
foregroundBuffer.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(foregroundBuffer.Colour, animation_length).Loop();
}
subtractionCache.Invalidate();
}
protected override void Update()
{
base.Update();
if (!subtractionCache.IsValid)
{
subtractionLayer.Width = 5;
subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth);
subtractionLayer.EdgeEffect = new EdgeEffectParameters
{
Colour = Color4.White,
Type = EdgeEffectType.Glow,
Radius = DrawWidth
};
foregroundBuffer.ForceRedraw();
subtractionBuffer.ForceRedraw();
subtractionCache.Validate();
}
}
}
}

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
{
/// <summary>
/// Interface for mania hold note bodies.
/// </summary>
public interface IHoldNoteBody
{
/// <summary>
/// Recycles the contents of this <see cref="IHoldNoteBody"/> to free used resources.
/// </summary>
void Recycle();
}
}

View File

@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mania.Objects
{
StartTime = StartTime,
Column = Column,
Samples = getNodeSamples(0),
Samples = GetNodeSamples(0),
});
AddNested(Tail = new TailNote
{
StartTime = EndTime,
Column = Column,
Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1),
Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1),
});
}
@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
public IList<HitSampleInfo> GetNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples;
}
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
{
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK;
protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0;
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
}

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Gameplay
{
[HeadlessTest]
public class TestSceneScoreProcessor : OsuTestScene
{
[Test]
public void TestNoScoreIncreaseFromMiss()
{
var beatmap = new Beatmap<TestHitObject> { HitObjects = { new TestHitObject() } };
var scoreProcessor = new ScoreProcessor();
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a miss judgement
scoreProcessor.ApplyResult(new JudgementResult(new TestHitObject(), new TestJudgement()) { Type = HitResult.Miss });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0));
}
private class TestHitObject : HitObject
{
public override Judgement CreateJudgement() => new TestJudgement();
}
private class TestJudgement : Judgement
{
protected override int NumericResultFor(HitResult result) => 100;
}
}
}

View File

@ -4,8 +4,10 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets;
using osuTK.Input;
@ -15,7 +17,7 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture]
public class TestSceneToolbar : OsuManualInputManagerTestScene
{
private Toolbar toolbar;
private TestToolbar toolbar;
[Resolved]
private RulesetStore rulesets { get; set; }
@ -23,7 +25,7 @@ namespace osu.Game.Tests.Visual.Menus
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = toolbar = new Toolbar { State = { Value = Visibility.Visible } };
Child = toolbar = new TestToolbar { State = { Value = Visibility.Visible } };
});
[Test]
@ -72,5 +74,24 @@ namespace osu.Game.Tests.Visual.Menus
AddUntilStep("ruleset switched", () => rulesetSelector.Current.Value.Equals(expected));
}
}
[TestCase(OverlayActivation.All)]
[TestCase(OverlayActivation.Disabled)]
public void TestRespectsOverlayActivation(OverlayActivation mode)
{
AddStep($"set activation mode to {mode}", () => toolbar.OverlayActivationMode.Value = mode);
AddStep("hide toolbar", () => toolbar.Hide());
AddStep("try to show toolbar", () => toolbar.Show());
if (mode == OverlayActivation.Disabled)
AddAssert("toolbar still hidden", () => toolbar.State.Value == Visibility.Hidden);
else
AddAssert("toolbar is visible", () => toolbar.State.Value == Visibility.Visible);
}
public class TestToolbar : Toolbar
{
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode;
}
}
}

View File

@ -109,6 +109,15 @@ namespace osu.Game.Beatmaps
// Convert
IBeatmap converted = converter.Convert();
// Apply conversion mods to the result
foreach (var mod in mods.OfType<IApplicableAfterBeatmapConversion>())
{
if (cancellationSource.IsCancellationRequested)
throw new BeatmapLoadTimeoutException(BeatmapInfo);
mod.ApplyToBeatmap(converted);
}
// Apply difficulty mods
if (mods.Any(m => m is IApplicableToDifficulty))
{

View File

@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar
private const double transition_time = 500;
private readonly Bindable<OverlayActivation> overlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
protected readonly Bindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
// Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden.
public override bool PropagateNonPositionalInputSubTree => true;
@ -89,14 +89,8 @@ namespace osu.Game.Overlays.Toolbar
// Bound after the selector is added to the hierarchy to give it a chance to load the available rulesets
rulesetSelector.Current.BindTo(parentRuleset);
State.ValueChanged += visibility =>
{
if (overlayActivationMode.Value == OverlayActivation.Disabled)
Hide();
};
if (osuGame != null)
overlayActivationMode.BindTo(osuGame.OverlayActivationMode);
OverlayActivationMode.BindTo(osuGame.OverlayActivationMode);
}
public class ToolbarBackground : Container
@ -137,6 +131,17 @@ namespace osu.Game.Overlays.Toolbar
}
}
protected override void UpdateState(ValueChangedEvent<Visibility> state)
{
if (state.NewValue == Visibility.Visible && OverlayActivationMode.Value == OverlayActivation.Disabled)
{
State.Value = Visibility.Hidden;
return;
}
base.UpdateState(state);
}
protected override void PopIn()
{
this.MoveToY(0, transition_time, Easing.OutQuint);

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// Interface for a <see cref="Mod"/> that applies changes to the <see cref="IBeatmap"/> generated by the <see cref="BeatmapConverter{TObject}"/>.
/// </summary>
public interface IApplicableAfterBeatmapConversion : IApplicableMod
{
/// <summary>
/// Applies this <see cref="Mod"/> to the <see cref="IBeatmap"/> after conversion has taken place.
/// </summary>
/// <param name="beatmap">The converted <see cref="IBeatmap"/>.</param>
void ApplyToBeatmap(IBeatmap beatmap);
}
}

View File

@ -133,17 +133,19 @@ namespace osu.Game.Rulesets.Scoring
}
}
double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result);
if (result.Judgement.IsBonus)
{
if (result.IsHit)
bonusScore += result.Judgement.NumericResultFor(result);
bonusScore += scoreIncrease;
}
else
{
if (result.HasResult)
scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1;
baseScore += result.Judgement.NumericResultFor(result);
baseScore += scoreIncrease;
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
}
@ -169,17 +171,19 @@ namespace osu.Game.Rulesets.Scoring
if (result.FailedAtJudgement)
return;
double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result);
if (result.Judgement.IsBonus)
{
if (result.IsHit)
bonusScore -= result.Judgement.NumericResultFor(result);
bonusScore -= scoreIncrease;
}
else
{
if (result.HasResult)
scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1;
baseScore -= result.Judgement.NumericResultFor(result);
baseScore -= scoreIncrease;
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
}