1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 15:23:14 +08:00

Merge pull request #20377 from peppy/argon-skin

Add new default "argon" skin
This commit is contained in:
Dan Balasescu 2022-09-22 14:50:43 +09:00 committed by GitHub
commit 70bab81282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 966 additions and 57 deletions

View File

@ -58,10 +58,11 @@ namespace osu.Game.Rulesets.Osu.Tests
private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null)
{
var drawable = createSingle(circleSize, auto, timeOffset, positionOffset);
var playfield = new TestOsuPlayfield();
playfield.Add(drawable);
for (double t = timeOffset; t < timeOffset + 60000; t += 2000)
playfield.Add(createSingle(circleSize, auto, t, positionOffset));
return playfield;
}

View File

@ -27,6 +27,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Osu.Skinning.Argon;
using osu.Game.Rulesets.Osu.Skinning.Legacy;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Rulesets.Osu.UI;
@ -237,6 +238,9 @@ namespace osu.Game.Rulesets.Osu
{
case LegacySkin:
return new OsuLegacySkinTransformer(skin);
case ArgonSkin:
return new OsuArgonSkinTransformer(skin);
}
return null;

View File

@ -0,0 +1,71 @@
// 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.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonFollowCircle : FollowCircle
{
public ArgonFollowCircle()
{
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 4,
BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
Blending = BlendingParameters.Additive,
Child = new Box
{
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Alpha = 0.3f,
}
};
}
protected override void OnSliderPress()
{
const float duration = 300f;
if (Precision.AlmostEquals(0, Alpha))
this.ScaleTo(1);
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA, duration, Easing.OutQuint)
.FadeIn(duration, Easing.OutQuint);
}
protected override void OnSliderRelease()
{
const float duration = 150;
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.2f, duration, Easing.OutQuint)
.FadeTo(0, duration, Easing.OutQuint);
}
protected override void OnSliderEnd()
{
const float duration = 300;
this.ScaleTo(1, duration, Easing.OutQuint)
.FadeOut(duration / 2, Easing.OutQuint);
}
protected override void OnSliderTick()
{
this.ScaleTo(DrawableSliderBall.FOLLOW_AREA * 1.08f, 40, Easing.OutQuint)
.Then()
.ScaleTo(DrawableSliderBall.FOLLOW_AREA, 200f, Easing.OutQuint);
}
protected override void OnSliderBreak()
{
}
}
}

View File

@ -0,0 +1,85 @@
// 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.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonJudgementPiece : CompositeDrawable, IAnimatableJudgement
{
protected readonly HitResult Result;
protected SpriteText JudgementText { get; private set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ArgonJudgementPiece(HitResult result)
{
Result = result;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
JudgementText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = Result.GetDescription().ToUpperInvariant(),
Colour = colours.ForHitResult(Result),
Spacing = new Vector2(5, 0),
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold),
}
};
}
/// <summary>
/// Plays the default animation for this judgement piece.
/// </summary>
/// <remarks>
/// The base implementation only handles fade (for all result types) and misses.
/// Individual rulesets are recommended to implement their appropriate hit animations.
/// </remarks>
public virtual void PlayAnimation()
{
switch (Result)
{
default:
JudgementText
.ScaleTo(Vector2.One)
.ScaleTo(new Vector2(1.2f), 1800, Easing.OutQuint);
break;
case HitResult.Miss:
this.ScaleTo(1.6f);
this.ScaleTo(1, 100, Easing.In);
this.MoveTo(Vector2.Zero);
this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint);
this.RotateTo(0);
this.RotateTo(40, 800, Easing.InQuint);
break;
}
this.FadeOutFromOne(800);
}
public Drawable? GetAboveHitObjectsProxiedContent() => null;
}
}

View File

@ -0,0 +1,220 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonMainCirclePiece : CompositeDrawable
{
public const float BORDER_THICKNESS = (OsuHitObject.OBJECT_RADIUS * 2) * (2f / 58);
public const float GRADIENT_THICKNESS = BORDER_THICKNESS * 2.5f;
public const float OUTER_GRADIENT_SIZE = (OsuHitObject.OBJECT_RADIUS * 2) - BORDER_THICKNESS * 4;
public const float INNER_GRADIENT_SIZE = OUTER_GRADIENT_SIZE - GRADIENT_THICKNESS * 2;
public const float INNER_FILL_SIZE = INNER_GRADIENT_SIZE - GRADIENT_THICKNESS * 2;
private readonly Circle outerFill;
private readonly Circle outerGradient;
private readonly Circle innerGradient;
private readonly Circle innerFill;
private readonly RingPiece border;
private readonly OsuSpriteText number;
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly IBindable<int> indexInCurrentCombo = new Bindable<int>();
private readonly FlashPiece flash;
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
public ArgonMainCirclePiece(bool withOuterFill)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
outerFill = new Circle // renders white outer border and dark fill
{
Size = Size,
Alpha = withOuterFill ? 1 : 0,
},
outerGradient = new Circle // renders the outer bright gradient
{
Size = new Vector2(OUTER_GRADIENT_SIZE),
Alpha = 1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
innerGradient = new Circle // renders the inner bright gradient
{
Size = new Vector2(INNER_GRADIENT_SIZE),
Alpha = 1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
innerFill = new Circle // renders the inner dark fill
{
Size = new Vector2(INNER_FILL_SIZE),
Alpha = 1,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
number = new OsuSpriteText
{
Font = OsuFont.Default.With(size: 52, weight: FontWeight.Bold),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = -2,
Text = @"1",
},
flash = new FlashPiece(),
border = new RingPiece(BORDER_THICKNESS),
};
}
[BackgroundDependencyLoader]
private void load()
{
var drawableOsuObject = (DrawableOsuHitObject)drawableObject;
accentColour.BindTo(drawableObject.AccentColour);
indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable);
}
protected override void LoadComplete()
{
base.LoadComplete();
accentColour.BindValueChanged(colour =>
{
outerFill.Colour = innerFill.Colour = colour.NewValue.Darken(4);
outerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.1f));
innerGradient.Colour = ColourInfo.GradientVertical(colour.NewValue.Darken(0.5f), colour.NewValue.Darken(0.6f));
flash.Colour = colour.NewValue;
}, true);
indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableObject, drawableObject.State.Value);
}
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
{
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
switch (state)
{
case ArmedState.Hit:
// Fade out time is at a maximum of 800. Must match `DrawableHitCircle`'s arbitrary lifetime spec.
const double fade_out_time = 800;
const double flash_in_duration = 150;
const double resize_duration = 300;
const float shrink_size = 0.8f;
// Animating with the number present is distracting.
// The number disappearing is hidden by the bright flash.
number.FadeOut(flash_in_duration / 2);
// The fill layers add too much noise during the explosion animation.
// They will be hidden by the additive effects anyway.
outerFill.FadeOut(flash_in_duration, Easing.OutQuint);
innerFill.FadeOut(flash_in_duration, Easing.OutQuint);
// The inner-most gradient should actually be resizing, but is only visible for
// a few milliseconds before it's hidden by the flash, so it's pointless overhead to bother with it.
innerGradient.FadeOut(flash_in_duration, Easing.OutQuint);
// The border is always white, but after hit it gets coloured by the skin/beatmap's colouring.
// A gradient is applied to make the border less prominent over the course of the animation.
// Without this, the border dominates the visual presence of the explosion animation in a bad way.
border.TransformTo(nameof
(BorderColour), ColourInfo.GradientVertical(
accentColour.Value.Opacity(0.5f),
accentColour.Value.Opacity(0)), fade_out_time);
// The outer ring shrinks immediately, but accounts for its thickness so it doesn't overlap the inner
// gradient layers.
border.ResizeTo(Size * shrink_size + new Vector2(border.BorderThickness), resize_duration, Easing.OutElasticHalf);
// The outer gradient is resize with a slight delay from the border.
// This is to give it a bomb-like effect, with the border "triggering" its animation when getting close.
using (BeginDelayedSequence(flash_in_duration / 12))
outerGradient.ResizeTo(outerGradient.Size * shrink_size, resize_duration, Easing.OutElasticHalf);
// The flash layer starts white to give the wanted brightness, but is almost immediately
// recoloured to the accent colour. This would more correctly be done with two layers (one for the initial flash)
// but works well enough with the colour fade.
flash.FadeTo(1, flash_in_duration, Easing.OutQuint);
flash.FlashColour(Color4.White, flash_in_duration, Easing.OutQuint);
this.FadeOut(fade_out_time, Easing.OutQuad);
break;
}
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (drawableObject.IsNotNull())
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
}
private class FlashPiece : Circle
{
public FlashPiece()
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS);
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Alpha = 0;
Blending = BlendingParameters.Additive;
// The edge effect provides the fill due to not being rendered hollow.
Child.Alpha = 0;
Child.AlwaysPresent = true;
}
protected override void Update()
{
base.Update();
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = Colour,
Radius = OsuHitObject.OBJECT_RADIUS * 1.2f,
};
}
}
}
}

View File

@ -0,0 +1,54 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonReverseArrow : CompositeDrawable
{
private Bindable<Color4> accentColour = null!;
private SpriteIcon icon = null!;
[BackgroundDependencyLoader]
private void load(DrawableHitObject hitObject)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
InternalChildren = new Drawable[]
{
new Circle
{
Size = new Vector2(40, 20),
Colour = Color4.White,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
icon = new SpriteIcon
{
Icon = FontAwesome.Solid.AngleDoubleRight,
Size = new Vector2(16),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
accentColour = hitObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4));
}
}
}

View File

@ -0,0 +1,109 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSliderBall : CircularContainer
{
private readonly Box fill;
private readonly SpriteIcon icon;
private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
public ArgonSliderBall()
{
Size = new Vector2(ArgonMainCirclePiece.OUTER_GRADIENT_SIZE);
Masking = true;
BorderThickness = ArgonMainCirclePiece.GRADIENT_THICKNESS;
BorderColour = Color4.White;
InternalChildren = new Drawable[]
{
fill = new Box
{
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
icon = new SpriteIcon
{
Size = new Vector2(48),
Scale = defaultIconScale,
Icon = FontAwesome.Solid.AngleRight,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
if (parentObject != null)
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(parentObject, parentObject.State.Value);
}
}
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState _)
{
// Gets called by slider ticks, tails, etc., leading to duplicated
// animations which in this case have no visual impact (due to
// instant fade) but may negatively affect performance
if (drawableObject is not DrawableSlider)
return;
const float duration = 200;
const float icon_scale = 0.9f;
using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
{
this.FadeInFromZero(duration, Easing.OutQuint);
icon.ScaleTo(0).Then().ScaleTo(defaultIconScale, duration, Easing.OutElasticHalf);
}
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
this.FadeOut(duration, Easing.OutQuint);
icon.ScaleTo(defaultIconScale * icon_scale, duration, Easing.OutQuint);
}
}
protected override void Update()
{
base.Update();
//undo rotation on layers which should not be rotated.
float appliedRotation = Parent.Rotation;
fill.Rotation = -appliedRotation;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (parentObject != null)
parentObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}

View File

@ -0,0 +1,40 @@
// 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.Framework.Extensions.Color4Extensions;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSliderBody : PlaySliderBody
{
protected override void LoadComplete()
{
const float path_radius = ArgonMainCirclePiece.OUTER_GRADIENT_SIZE / 2;
base.LoadComplete();
AccentColourBindable.BindValueChanged(accent => BorderColour = accent.NewValue, true);
ScaleBindable.BindValueChanged(scale => PathRadius = path_radius * scale.NewValue, true);
// This border size thing is kind of weird, hey.
const float intended_thickness = ArgonMainCirclePiece.GRADIENT_THICKNESS / path_radius;
BorderSize = intended_thickness / Default.DrawableSliderPath.BORDER_PORTION;
}
protected override Default.DrawableSliderPath CreateSliderPath() => new DrawableSliderPath();
private class DrawableSliderPath : Default.DrawableSliderPath
{
protected override Color4 ColourAt(float position)
{
if (CalculatedBorderPortion != 0f && position <= CalculatedBorderPortion)
return BorderColour;
return AccentColour.Darken(4);
}
}
}
}

View File

@ -0,0 +1,40 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class ArgonSliderScorePoint : CircularContainer
{
private Bindable<Color4> accentColour = null!;
private const float size = 12;
[BackgroundDependencyLoader]
private void load(DrawableHitObject hitObject)
{
Masking = true;
Origin = Anchor.Centre;
Size = new Vector2(size);
BorderThickness = 3;
BorderColour = Color4.White;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0,
};
accentColour = hitObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => BorderColour = accent.NewValue);
}
}
}

View File

@ -0,0 +1,73 @@
// 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.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public class OsuArgonSkinTransformer : ISkin
{
public OsuArgonSkinTransformer(ISkin skin)
{
}
public Drawable? GetDrawableComponent(ISkinComponent component)
{
switch (component)
{
case GameplaySkinComponent<HitResult> resultComponent:
return new ArgonJudgementPiece(resultComponent.Component);
case OsuSkinComponent osuComponent:
switch (osuComponent.Component)
{
case OsuSkinComponents.HitCircle:
return new ArgonMainCirclePiece(true);
case OsuSkinComponents.SliderHeadHitCircle:
return new ArgonMainCirclePiece(false);
case OsuSkinComponents.SliderBody:
return new ArgonSliderBody();
case OsuSkinComponents.SliderBall:
return new ArgonSliderBall();
case OsuSkinComponents.SliderFollowCircle:
return new ArgonFollowCircle();
case OsuSkinComponents.SliderScorePoint:
return new ArgonSliderScorePoint();
case OsuSkinComponents.ReverseArrow:
return new ArgonReverseArrow();
}
break;
}
return null;
}
public Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{
return null;
}
public ISample? GetSample(ISampleInfo sampleInfo)
{
return null;
}
public IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup) where TLookup : notnull where TValue : notnull
{
return null;
}
}
}

View File

@ -10,8 +10,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public abstract class DrawableSliderPath : SmoothPath
{
protected const float BORDER_PORTION = 0.128f;
protected const float GRADIENT_PORTION = 1 - BORDER_PORTION;
public const float BORDER_PORTION = 0.128f;
public const float GRADIENT_PORTION = 1 - BORDER_PORTION;
private const float border_max_size = 8f;
private const float border_min_size = 0f;

View File

@ -16,9 +16,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public abstract class PlaySliderBody : SnakingSliderBody
{
private IBindable<float> scaleBindable;
protected IBindable<float> ScaleBindable { get; private set; } = null!;
protected IBindable<Color4> AccentColourBindable { get; private set; } = null!;
private IBindable<int> pathVersion;
private IBindable<Color4> accentColour;
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
@ -30,14 +32,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
var drawableSlider = (DrawableSlider)drawableObject;
scaleBindable = drawableSlider.ScaleBindable.GetBoundCopy();
scaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
ScaleBindable = drawableSlider.ScaleBindable.GetBoundCopy();
ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
pathVersion = drawableSlider.PathVersion.GetBoundCopy();
pathVersion.BindValueChanged(_ => Refresh());
accentColour = drawableObject.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
AccentColourBindable = drawableObject.AccentColour.GetBoundCopy();
AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -14,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class RingPiece : CircularContainer
{
public RingPiece()
public RingPiece(float thickness = 9)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
@ -22,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre;
Masking = true;
BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other.
BorderThickness = thickness;
BorderColour = Color4.White;
Child = new Box

View File

@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins.IO
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
{
Assert.IsFalse(s.Protected);
Assert.AreEqual(typeof(TrianglesSkin), s.CreateInstance(skinManager).GetType());
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
@ -215,7 +215,7 @@ namespace osu.Game.Tests.Skins.IO
{
Assert.IsFalse(s.Protected);
Assert.AreNotEqual(originalSkinId, s.ID);
Assert.AreEqual(typeof(TrianglesSkin), s.CreateInstance(skinManager).GetType());
Assert.AreEqual(typeof(ArgonSkin), s.CreateInstance(skinManager).GetType());
});
return Task.CompletedTask;

View File

@ -1,8 +1,8 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
@ -51,13 +51,14 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestToggleSeeking()
{
DefaultSongProgress getDefaultProgress() => this.ChildrenOfType<DefaultSongProgress>().Single();
void applyToDefaultProgress(Action<DefaultSongProgress> action) =>
this.ChildrenOfType<DefaultSongProgress>().ForEach(action);
AddStep("allow seeking", () => getDefaultProgress().AllowSeeking.Value = true);
AddStep("hide graph", () => getDefaultProgress().ShowGraph.Value = false);
AddStep("disallow seeking", () => getDefaultProgress().AllowSeeking.Value = false);
AddStep("allow seeking", () => getDefaultProgress().AllowSeeking.Value = true);
AddStep("show graph", () => getDefaultProgress().ShowGraph.Value = true);
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
AddStep("hide graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = false));
AddStep("disallow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = false));
AddStep("allow seeking", () => applyToDefaultProgress(s => s.AllowSeeking.Value = true));
AddStep("show graph", () => applyToDefaultProgress(s => s.ShowGraph.Value = true));
}
private void setHitObjects()

View File

@ -104,6 +104,7 @@ namespace osu.Game.Overlays.Settings.Sections
// In the future we should change this to properly handle ChangeSet events.
dropdownItems.Clear();
dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.ARGON_SKIN).ToLive(realm));
dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.TRIANGLES_SKIN).ToLive(realm));
dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.CLASSIC_SKIN).ToLive(realm));

View File

@ -129,11 +129,19 @@ namespace osu.Game.Screens.Backgrounds
}
case BackgroundSource.Skin:
// default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them.
if (skin.Value is TrianglesSkin || skin.Value is DefaultLegacySkin)
break;
switch (skin.Value)
{
case TrianglesSkin:
case ArgonSkin:
case DefaultLegacySkin:
// default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them.
break;
default:
newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
break;
}
newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
break;
}
}

View File

@ -0,0 +1,198 @@
// 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.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Beatmaps.Formats;
using osu.Game.Extensions;
using osu.Game.IO;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Skinning
{
public class ArgonSkin : Skin
{
public static SkinInfo CreateInfo() => new SkinInfo
{
ID = osu.Game.Skinning.SkinInfo.ARGON_SKIN,
Name = "osu! \"argon\" (2022)",
Creator = "team osu!",
Protected = true,
InstantiationInfo = typeof(ArgonSkin).GetInvariantInstantiationInfo()
};
private readonly IStorageResourceProvider resources;
public ArgonSkin(IStorageResourceProvider resources)
: this(CreateInfo(), resources)
{
}
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
public ArgonSkin(SkinInfo skin, IStorageResourceProvider resources)
: base(skin, resources)
{
this.resources = resources;
}
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Textures?.Get(componentName, wrapModeS, wrapModeT);
public override ISample GetSample(ISampleInfo sampleInfo)
{
foreach (string lookup in sampleInfo.LookupNames)
{
var sample = Samples?.Get(lookup) ?? resources.AudioManager?.Samples.Get(lookup);
if (sample != null)
return sample;
}
return null;
}
public override Drawable GetDrawableComponent(ISkinComponent component)
{
if (base.GetDrawableComponent(component) is Drawable c)
return c;
switch (component)
{
case SkinnableTargetComponent target:
switch (target.Target)
{
case SkinnableTarget.SongSelect:
var songSelectComponents = new SkinnableTargetComponentsContainer(_ =>
{
// do stuff when we need to.
});
return songSelectComponents;
case SkinnableTarget.MainHUDComponents:
var skinnableTargetWrapper = new SkinnableTargetComponentsContainer(container =>
{
var score = container.OfType<DefaultScoreCounter>().FirstOrDefault();
var accuracy = container.OfType<DefaultAccuracyCounter>().FirstOrDefault();
var combo = container.OfType<DefaultComboCounter>().FirstOrDefault();
var ppCounter = container.OfType<PerformancePointsCounter>().FirstOrDefault();
if (score != null)
{
score.Anchor = Anchor.TopCentre;
score.Origin = Anchor.TopCentre;
// elements default to beneath the health bar
const float vertical_offset = 30;
const float horizontal_padding = 20;
score.Position = new Vector2(0, vertical_offset);
if (ppCounter != null)
{
ppCounter.Y = score.Position.Y + ppCounter.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).Y - 4;
ppCounter.Origin = Anchor.TopCentre;
ppCounter.Anchor = Anchor.TopCentre;
}
if (accuracy != null)
{
accuracy.Position = new Vector2(-accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 - horizontal_padding, vertical_offset + 5);
accuracy.Origin = Anchor.TopRight;
accuracy.Anchor = Anchor.TopCentre;
if (combo != null)
{
combo.Position = new Vector2(accuracy.ScreenSpaceDeltaToParentSpace(score.ScreenSpaceDrawQuad.Size).X / 2 + horizontal_padding, vertical_offset + 5);
combo.Anchor = Anchor.TopCentre;
}
}
var hitError = container.OfType<HitErrorMeter>().FirstOrDefault();
if (hitError != null)
{
hitError.Anchor = Anchor.CentreLeft;
hitError.Origin = Anchor.CentreLeft;
}
var hitError2 = container.OfType<HitErrorMeter>().LastOrDefault();
if (hitError2 != null)
{
hitError2.Anchor = Anchor.CentreRight;
hitError2.Scale = new Vector2(-1, 1);
// origin flipped to match scale above.
hitError2.Origin = Anchor.CentreLeft;
}
}
})
{
Children = new Drawable[]
{
new DefaultComboCounter(),
new DefaultScoreCounter(),
new DefaultAccuracyCounter(),
new DefaultHealthDisplay(),
new DefaultSongProgress(),
new BarHitErrorMeter(),
new BarHitErrorMeter(),
new PerformancePointsCounter()
}
};
return skinnableTargetWrapper;
}
return null;
}
switch (component.LookupName)
{
// Temporary until default skin has a valid hit lighting.
case @"lighting":
return Drawable.Empty();
}
if (GetTexture(component.LookupName) is Texture t)
return new Sprite { Texture = t };
return null;
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{
// todo: this code is pulled from LegacySkin and should not exist.
// will likely change based on how databased storage of skin configuration goes.
switch (lookup)
{
case GlobalSkinColours global:
switch (global)
{
case GlobalSkinColours.ComboColours:
return SkinUtils.As<TValue>(new Bindable<IReadOnlyList<Color4>>(Configuration.ComboColours));
}
break;
case SkinComboColourLookup comboColour:
return SkinUtils.As<TValue>(new Bindable<Color4>(getComboColour(Configuration, comboColour.ColourIndex)));
}
return null;
}
private static Color4 getComboColour(IHasComboColours source, int colourIndex)
=> source.ComboColours[colourIndex % source.ComboColours.Count];
}
}

View File

@ -81,6 +81,7 @@ namespace osu.Game.Skinning
}
}
// TODO: check
int lastDefaultSkinIndex = sources.IndexOf(sources.OfType<TrianglesSkin>().LastOrDefault());
// Ruleset resources should be given the ability to override game-wide defaults

View File

@ -19,6 +19,7 @@ namespace osu.Game.Skinning
public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable<SkinInfo>, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles
{
internal static readonly Guid TRIANGLES_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD");
internal static readonly Guid ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0");
internal static readonly Guid CLASSIC_SKIN = new Guid("81F02CD3-EEC6-4865-AC23-FAE26A386187");
internal static readonly Guid RANDOM_SKIN = new Guid("D39DFEFB-477C-4372-B1EA-2BCEA5FB8908");

View File

@ -39,6 +39,11 @@ namespace osu.Game.Skinning
[ExcludeFromDynamicCompile]
public class SkinManager : ModelManager<SkinInfo>, ISkinSource, IStorageResourceProvider, IModelImporter<SkinInfo>
{
/// <summary>
/// The default "classic" skin.
/// </summary>
public Skin DefaultClassicSkin { get; }
private readonly AudioManager audio;
private readonly Scheduler scheduler;
@ -49,24 +54,15 @@ namespace osu.Game.Skinning
public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>();
public readonly Bindable<Live<SkinInfo>> CurrentSkinInfo = new Bindable<Live<SkinInfo>>(TrianglesSkin.CreateInfo().ToLiveUnmanaged())
{
Default = TrianglesSkin.CreateInfo().ToLiveUnmanaged()
};
public readonly Bindable<Live<SkinInfo>> CurrentSkinInfo = new Bindable<Live<SkinInfo>>(ArgonSkin.CreateInfo().ToLiveUnmanaged());
private readonly SkinImporter skinImporter;
private readonly IResourceStore<byte[]> userFiles;
/// <summary>
/// The default "triangles" skin.
/// </summary>
public Skin DefaultSkinTriangles { get; }
private Skin argonSkin { get; }
/// <summary>
/// The default "classic" skin.
/// </summary>
public Skin DefaultClassicSkin { get; }
private Skin trianglesSkin { get; }
public SkinManager(Storage storage, RealmAccess realm, GameHost host, IResourceStore<byte[]> resources, AudioManager audio, Scheduler scheduler)
: base(storage, realm)
@ -86,7 +82,8 @@ namespace osu.Game.Skinning
var defaultSkins = new[]
{
DefaultClassicSkin = new DefaultLegacySkin(this),
DefaultSkinTriangles = new TrianglesSkin(this),
trianglesSkin = new TrianglesSkin(this),
argonSkin = new ArgonSkin(this),
};
// Ensure the default entries are present.
@ -104,7 +101,7 @@ namespace osu.Game.Skinning
CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
};
CurrentSkin.Value = DefaultSkinTriangles;
CurrentSkin.Value = argonSkin;
CurrentSkin.ValueChanged += skin =>
{
if (!skin.NewValue.SkinInfo.Equals(CurrentSkinInfo.Value))
@ -125,7 +122,7 @@ namespace osu.Game.Skinning
if (randomChoices.Length == 0)
{
CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged();
CurrentSkinInfo.Value = ArgonSkin.CreateInfo().ToLiveUnmanaged();
return;
}
@ -229,11 +226,15 @@ namespace osu.Game.Skinning
{
yield return CurrentSkin.Value;
// Skin manager provides default fallbacks.
// This handles cases where a user skin doesn't have the required resources for complete display of
// certain elements.
if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultClassicSkin)
yield return DefaultClassicSkin;
if (CurrentSkin.Value != DefaultSkinTriangles)
yield return DefaultSkinTriangles;
if (CurrentSkin.Value != trianglesSkin)
yield return trianglesSkin;
}
}
@ -294,7 +295,7 @@ namespace osu.Game.Skinning
Guid currentUserSkin = CurrentSkinInfo.Value.ID;
if (items.Any(s => s.ID == currentUserSkin))
scheduler.Add(() => CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
scheduler.Add(() => CurrentSkinInfo.Value = ArgonSkin.CreateInfo().ToLiveUnmanaged());
Delete(items.ToList(), silent);
});
@ -313,7 +314,7 @@ namespace osu.Game.Skinning
skinInfo = DefaultClassicSkin.SkinInfo;
}
CurrentSkinInfo.Value = skinInfo ?? DefaultSkinTriangles.SkinInfo;
CurrentSkinInfo.Value = skinInfo ?? trianglesSkin.SkinInfo;
}
}
}

View File

@ -113,6 +113,7 @@ namespace osu.Game.Skinning
// Temporarily used to exclude undesirable ISkin implementations
static bool isUserSkin(ISkin skin)
=> skin.GetType() == typeof(TrianglesSkin)
|| skin.GetType() == typeof(ArgonSkin)
|| skin.GetType() == typeof(DefaultLegacySkin)
|| skin.GetType() == typeof(LegacySkin);
}

View File

@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual
private TrianglesSkin trianglesSkin;
private Skin metricsSkin;
private Skin legacySkin;
private Skin argonSkin;
private Skin specialSkin;
private Skin oldSkin;
@ -48,6 +49,7 @@ namespace osu.Game.Tests.Visual
{
var dllStore = new DllResourceStore(GetType().Assembly);
argonSkin = new ArgonSkin(this);
trianglesSkin = new TrianglesSkin(this);
metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/metrics_skin"), this, true);
legacySkin = new DefaultLegacySkin(this);
@ -63,11 +65,12 @@ namespace osu.Game.Tests.Visual
var beatmap = CreateBeatmapForSkinProvider();
Cell(0).Child = createProvider(trianglesSkin, creationFunction, beatmap);
Cell(1).Child = createProvider(metricsSkin, creationFunction, beatmap);
Cell(2).Child = createProvider(legacySkin, creationFunction, beatmap);
Cell(3).Child = createProvider(specialSkin, creationFunction, beatmap);
Cell(4).Child = createProvider(oldSkin, creationFunction, beatmap);
Cell(0).Child = createProvider(argonSkin, creationFunction, beatmap);
Cell(1).Child = createProvider(trianglesSkin, creationFunction, beatmap);
Cell(2).Child = createProvider(metricsSkin, creationFunction, beatmap);
Cell(3).Child = createProvider(legacySkin, creationFunction, beatmap);
Cell(4).Child = createProvider(specialSkin, creationFunction, beatmap);
Cell(5).Child = createProvider(oldSkin, creationFunction, beatmap);
}
protected IEnumerable<Drawable> CreatedDrawables => createdDrawables;
@ -82,10 +85,7 @@ namespace osu.Game.Tests.Visual
OutlineBox outlineBox;
SkinProvidingContainer skinProvider;
ISkin provider = skin;
if (provider is LegacySkin legacyProvider)
provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(legacyProvider, beatmap);
ISkin provider = Ruleset.Value.CreateInstance().CreateSkinTransformer(skin, beatmap) ?? skin;
var children = new Container
{
@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual
},
new OsuSpriteText
{
Text = skin?.SkinInfo.Value.Name ?? "none",
Text = skin.SkinInfo.Value.Name,
Scale = new Vector2(1.5f),
Padding = new MarginPadding(5),
},