1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-30 00:53:10 +08:00

Merge pull request #31206 from peppy/christmas

Add christmas / seasonal mode
This commit is contained in:
Dean Herbert 2024-12-23 16:49:38 +09:00 committed by GitHub
commit 7b9f776a14
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 838 additions and 42 deletions

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.
using NUnit.Framework;
using osu.Game.Screens.Menu;
using osu.Game.Seasonal;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public partial class TestSceneIntroChristmas : IntroTestScene
{
protected override bool IntroReliesOnTrack => true;
protected override IntroScreen CreateScreen() => new IntroChristmas();
}
}

View File

@ -0,0 +1,43 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Seasonal;
namespace osu.Game.Tests.Visual.Menus
{
public partial class TestSceneMainMenuSeasonalLighting : OsuTestScene
{
[Resolved]
private BeatmapManager beatmaps { get; set; } = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("prepare beatmap", () =>
{
var setInfo = beatmaps.QueryBeatmapSet(b => b.Protected && b.Hash == IntroChristmas.CHRISTMAS_BEATMAP_SET_HASH);
if (setInfo != null)
Beatmap.Value = beatmaps.GetWorkingBeatmap(setInfo.Value.Beatmaps.First());
});
AddStep("create lighting", () => Child = new MainMenuSeasonalLighting());
AddStep("restart beatmap", () =>
{
Beatmap.Value.Track.Start();
Beatmap.Value.Track.Seek(4000);
});
}
[Test]
public void TestBasic()
{
}
}
}

View File

@ -4,20 +4,52 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.Menu;
using osu.Game.Seasonal;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneOsuLogo : OsuTestScene
{
private OsuLogo? logo;
private float scale = 1;
protected override void LoadComplete()
{
base.LoadComplete();
AddSliderStep("scale", 0.1, 2, 1, scale =>
{
if (logo != null)
Child.Scale = new Vector2(this.scale = (float)scale);
});
}
[Test]
public void TestBasic()
{
AddStep("Add logo", () =>
{
Child = new OsuLogo
Child = logo = new OsuLogo
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(scale),
};
});
}
[Test]
public void TestChristmas()
{
AddStep("Add logo", () =>
{
Child = logo = new OsuLogoChristmas
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(scale),
};
});
}

View File

@ -68,6 +68,7 @@ using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Seasonal;
using osu.Game.Skinning;
using osu.Game.Updater;
using osu.Game.Users;
@ -361,7 +362,10 @@ namespace osu.Game
{
SentryLogger.AttachUser(API.LocalUser);
dependencies.Cache(osuLogo = new OsuLogo { Alpha = 0 });
if (SeasonalUIConfig.ENABLED)
dependencies.CacheAs(osuLogo = new OsuLogoChristmas { Alpha = 0 });
else
dependencies.CacheAs(osuLogo = new OsuLogo { Alpha = 0 });
// bind config int to database RulesetInfo
configRuleset = LocalConfig.GetBindable<string>(OsuSetting.Ruleset);

View File

@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shaders;
@ -15,6 +16,7 @@ using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Seasonal;
using IntroSequence = osu.Game.Configuration.IntroSequence;
namespace osu.Game.Screens
@ -37,6 +39,11 @@ namespace osu.Game.Screens
private IntroScreen getIntroSequence()
{
// Headless tests run too fast to load non-circles intros correctly.
// They will hit the "audio can't play" notification and cause random test failures.
if (SeasonalUIConfig.ENABLED && !DebugUtils.IsNUnitRunning)
return new IntroChristmas(createMainMenu);
if (introSequence == IntroSequence.Random)
introSequence = (IntroSequence)RNG.Next(0, (int)IntroSequence.Random);

View File

@ -207,7 +207,7 @@ namespace osu.Game.Screens.Menu
Text = NotificationsStrings.AudioPlaybackIssue
});
}
}, 5000);
}, 8000);
}
public override void OnResuming(ScreenTransitionEvent e)

View File

@ -36,6 +36,7 @@ using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Select;
using osu.Game.Seasonal;
using osuTK;
using osuTK.Graphics;
@ -125,6 +126,7 @@ namespace osu.Game.Screens.Menu
AddRangeInternal(new[]
{
SeasonalUIConfig.ENABLED ? new MainMenuSeasonalLighting() : Empty(),
new GlobalScrollAdjustsVolume(),
buttonsContainer = new ParallaxContainer
{
@ -161,14 +163,15 @@ namespace osu.Game.Screens.Menu
}
},
logoTarget = new Container { RelativeSizeAxes = Axes.Both, },
sideFlashes = new MenuSideFlashes(),
sideFlashes = SeasonalUIConfig.ENABLED ? new SeasonalMenuSideFlashes() : new MenuSideFlashes(),
songTicker = new SongTicker
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Margin = new MarginPadding { Right = 15, Top = 5 }
},
new KiaiMenuFountains(),
// For now, this is too much alongside the seasonal lighting.
SeasonalUIConfig.ENABLED ? Empty() : new KiaiMenuFountains(),
bottomElementsFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,

View File

@ -1,21 +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.
#nullable disable
using osuTK.Graphics;
using osu.Game.Skinning;
using osu.Game.Online.API;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
internal partial class MenuLogoVisualisation : LogoVisualisation
public partial class MenuLogoVisualisation : LogoVisualisation
{
private IBindable<APIUser> user;
private Bindable<Skin> skin;
private IBindable<APIUser> user = null!;
private Bindable<Skin> skin = null!;
[BackgroundDependencyLoader]
private void load(IAPIProvider api, SkinManager skinManager)
@ -23,11 +21,11 @@ namespace osu.Game.Screens.Menu
user = api.LocalUser.GetBoundCopy();
skin = skinManager.CurrentSkin.GetBoundCopy();
user.ValueChanged += _ => updateColour();
skin.BindValueChanged(_ => updateColour(), true);
user.ValueChanged += _ => UpdateColour();
skin.BindValueChanged(_ => UpdateColour(), true);
}
private void updateColour()
protected virtual void UpdateColour()
{
if (user.Value?.IsSupporter ?? false)
Colour = skin.Value.GetConfig<GlobalSkinColours, Color4>(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White;

View File

@ -3,8 +3,10 @@
#nullable disable
using osuTK.Graphics;
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@ -13,17 +15,19 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Skinning;
using osu.Game.Online.API;
using System;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
{
public partial class MenuSideFlashes : BeatSyncedContainer
{
protected virtual bool RefreshColoursEveryFlash => false;
protected virtual float Intensity => 2;
private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
private Box leftBox;
@ -67,7 +71,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Y,
Width = box_width * 2,
Width = box_width * Intensity,
Height = 1.5f,
// align off-screen to make sure our edges don't become visible during parallax.
X = -box_width,
@ -79,7 +83,7 @@ namespace osu.Game.Screens.Menu
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
Width = box_width * 2,
Width = box_width * Intensity,
Height = 1.5f,
X = box_width,
Alpha = 0,
@ -87,8 +91,11 @@ namespace osu.Game.Screens.Menu
}
};
user.ValueChanged += _ => updateColour();
skin.BindValueChanged(_ => updateColour(), true);
if (!RefreshColoursEveryFlash)
{
user.ValueChanged += _ => updateColour();
skin.BindValueChanged(_ => updateColour(), true);
}
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
@ -104,18 +111,28 @@ namespace osu.Game.Screens.Menu
private void flash(Drawable d, double beatLength, bool kiai, ChannelAmplitudes amplitudes)
{
d.FadeTo(Math.Max(0, ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier)), box_fade_in_time)
if (RefreshColoursEveryFlash)
updateColour();
d.FadeTo(Math.Clamp(0.1f + ((ReferenceEquals(d, leftBox) ? amplitudes.LeftChannel : amplitudes.RightChannel) - amplitude_dead_zone) / (kiai ? kiai_multiplier : alpha_multiplier), 0.1f, 1),
box_fade_in_time)
.Then()
.FadeOut(beatLength, Easing.In);
}
private void updateColour()
protected virtual Color4 GetBaseColour()
{
Color4 baseColour = colours.Blue;
if (user.Value?.IsSupporter ?? false)
baseColour = skin.Value.GetConfig<GlobalSkinColours, Color4>(GlobalSkinColours.MenuGlow)?.Value ?? baseColour;
return baseColour;
}
private void updateColour()
{
var baseColour = GetBaseColour();
// linear colour looks better in this case, so let's use it for now.
Color4 gradientDark = baseColour.Opacity(0).ToLinear();
Color4 gradientLight = baseColour.Opacity(0.6f).ToLinear();

View File

@ -53,8 +53,12 @@ namespace osu.Game.Screens.Menu
private Sample sampleClick;
private SampleChannel sampleClickChannel;
private Sample sampleBeat;
private Sample sampleDownbeat;
protected virtual MenuLogoVisualisation CreateMenuLogoVisualisation() => new MenuLogoVisualisation();
protected virtual double BeatSampleVariance => 0.1;
protected Sample SampleBeat;
protected Sample SampleDownbeat;
private readonly Container colourAndTriangles;
private readonly TrianglesV2 triangles;
@ -151,15 +155,15 @@ namespace osu.Game.Screens.Menu
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
visualizer = new MenuLogoVisualisation
visualizer = CreateMenuLogoVisualisation().With(v =>
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Alpha = visualizer_default_alpha,
Size = SCALE_ADJUST
},
new Container
v.RelativeSizeAxes = Axes.Both;
v.Origin = Anchor.Centre;
v.Anchor = Anchor.Centre;
v.Alpha = visualizer_default_alpha;
v.Size = SCALE_ADJUST;
}),
LogoElements = new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
@ -243,6 +247,8 @@ namespace osu.Game.Screens.Menu
};
}
public Container LogoElements { get; private set; }
/// <summary>
/// Schedule a new external animation. Handled queueing and finishing previous animations in a sane way.
/// </summary>
@ -271,8 +277,9 @@ namespace osu.Game.Screens.Menu
private void load(TextureStore textures, AudioManager audio)
{
sampleClick = audio.Samples.Get(@"Menu/osu-logo-select");
sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat");
SampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
SampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat");
logo.Texture = textures.Get(@"Menu/logo");
ripple.Texture = textures.Get(@"Menu/logo");
@ -298,12 +305,13 @@ namespace osu.Game.Screens.Menu
{
if (beatIndex % timingPoint.TimeSignature.Numerator == 0)
{
sampleDownbeat?.Play();
SampleDownbeat?.Play();
}
else
{
var channel = sampleBeat.GetChannel();
channel.Frequency.Value = 0.95 + RNG.NextDouble(0.1);
var channel = SampleBeat.GetChannel();
channel.Frequency.Value = 1 - BeatSampleVariance / 2 + RNG.NextDouble(BeatSampleVariance);
channel.Play();
}
});

View File

@ -0,0 +1,332 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Screens.Menu;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Seasonal
{
public partial class IntroChristmas : IntroScreen
{
// nekodex - circle the halls
public const string CHRISTMAS_BEATMAP_SET_HASH = "7e26183e72a496f672c3a21292e6b469fdecd084d31c259ea10a31df5b46cd77";
protected override string BeatmapHash => CHRISTMAS_BEATMAP_SET_HASH;
protected override string BeatmapFile => "christmas2024.osz";
private const double beat_length = 60000 / 172.0;
private const double offset = 5924;
protected override string SeeyaSampleName => "Intro/Welcome/seeya";
private TrianglesIntroSequence intro = null!;
public IntroChristmas(Func<MainMenu>? createNextScreen = null)
: base(createNextScreen)
{
}
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
if (!resuming)
{
PrepareMenuLoad();
var decouplingClock = new DecouplingFramedClock(UsingThemedIntro ? Track : null);
LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground())
{
RelativeSizeAxes = Axes.Both,
Clock = new InterpolatingFramedClock(decouplingClock),
LoadMenu = LoadMenu
}, _ =>
{
AddInternal(intro);
// There is a chance that the intro timed out before being displayed, and this scheduled callback could
// happen during the outro rather than intro.
// In such a scenario, we don't want to play the intro sample, nor attempt to start the intro track
// (that may have already been since disposed by MusicController).
if (DidLoadMenu)
return;
// If the user has requested no theme, fallback to the same intro voice and delay as IntroCircles.
// The triangles intro voice and theme are combined which makes it impossible to use.
StartTrack();
// no-op for the case of themed intro, no harm in calling for both scenarios as a safety measure.
decouplingClock.Start();
});
}
}
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);
// important as there is a clock attached to a track which will likely be disposed before returning to this screen.
intro.Expire();
}
private partial class TrianglesIntroSequence : CompositeDrawable
{
private readonly OsuLogo logo;
private readonly Action showBackgroundAction;
private OsuSpriteText welcomeText = null!;
private Container logoContainerSecondary = null!;
private LazerLogo lazerLogo = null!;
private Drawable triangles = null!;
public Action LoadMenu = null!;
[Resolved]
private OsuGameBase game { get; set; } = null!;
public TrianglesIntroSequence(OsuLogo logo, Action showBackgroundAction)
{
this.logo = logo;
this.showBackgroundAction = showBackgroundAction;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new[]
{
welcomeText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 10 },
Font = OsuFont.GetFont(weight: FontWeight.Light, size: 42),
Alpha = 1,
Spacing = new Vector2(5),
},
logoContainerSecondary = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = lazerLogo = new LazerLogo
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
}
},
triangles = new CircularContainer
{
Alpha = 0,
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(960),
Child = new GlitchingTriangles
{
RelativeSizeAxes = Axes.Both,
},
}
};
}
private static double getTimeForBeat(int beat) => offset + beat_length * beat;
protected override void LoadComplete()
{
base.LoadComplete();
lazerLogo.Hide();
using (BeginAbsoluteSequence(0))
{
using (BeginDelayedSequence(getTimeForBeat(-16)))
welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!");
using (BeginDelayedSequence(getTimeForBeat(-15)))
welcomeText.FadeIn().OnComplete(t => t.Text = "");
using (BeginDelayedSequence(getTimeForBeat(-14)))
welcomeText.FadeIn().OnComplete(t => t.Text = "welcome to osu!");
using (BeginDelayedSequence(getTimeForBeat(-13)))
welcomeText.FadeIn().OnComplete(t => t.Text = "");
using (BeginDelayedSequence(getTimeForBeat(-12)))
welcomeText.FadeIn().OnComplete(t => t.Text = "merry christmas!");
using (BeginDelayedSequence(getTimeForBeat(-11)))
welcomeText.FadeIn().OnComplete(t => t.Text = "");
using (BeginDelayedSequence(getTimeForBeat(-10)))
welcomeText.FadeIn().OnComplete(t => t.Text = "merry osumas!");
using (BeginDelayedSequence(getTimeForBeat(-9)))
{
welcomeText.FadeIn().OnComplete(t => t.Text = "");
}
lazerLogo.Scale = new Vector2(0.2f);
triangles.Scale = new Vector2(0.2f);
for (int i = 0; i < 8; i++)
{
using (BeginDelayedSequence(getTimeForBeat(-8 + i)))
{
triangles.FadeIn();
lazerLogo.ScaleTo(new Vector2(0.2f + (i + 1) / 8f * 0.3f), beat_length * 1, Easing.OutQuint);
triangles.ScaleTo(new Vector2(0.2f + (i + 1) / 8f * 0.3f), beat_length * 1, Easing.OutQuint);
lazerLogo.FadeTo((i + 1) * 0.06f);
lazerLogo.TransformTo(nameof(LazerLogo.Progress), (i + 1) / 10f);
}
}
GameWideFlash flash = new GameWideFlash();
using (BeginDelayedSequence(getTimeForBeat(-2)))
{
lazerLogo.FadeIn().OnComplete(_ => game.Add(flash));
}
flash.FadeInCompleted = () =>
{
logoContainerSecondary.Remove(lazerLogo, true);
triangles.FadeOut();
logo.FadeIn();
showBackgroundAction();
LoadMenu();
};
}
}
private partial class GameWideFlash : Box
{
public Action? FadeInCompleted;
public GameWideFlash()
{
Colour = Color4.White;
RelativeSizeAxes = Axes.Both;
Blending = BlendingParameters.Additive;
}
protected override void LoadComplete()
{
base.LoadComplete();
Alpha = 0;
this.FadeTo(0.5f, beat_length * 2, Easing.In)
.OnComplete(_ => FadeInCompleted?.Invoke());
this.Delay(beat_length * 2)
.Then()
.FadeOutFromOne(3000, Easing.OutQuint);
}
}
private partial class LazerLogo : CompositeDrawable
{
private LogoAnimation highlight = null!;
private LogoAnimation background = null!;
public float Progress
{
get => background.AnimationProgress;
set
{
background.AnimationProgress = value;
highlight.AnimationProgress = value;
}
}
public LazerLogo()
{
Size = new Vector2(960);
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
InternalChildren = new Drawable[]
{
highlight = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-highlight"),
Colour = Color4.White,
},
background = new LogoAnimation
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-background"),
Colour = OsuColour.Gray(0.6f),
},
};
}
}
private partial class GlitchingTriangles : BeatSyncedContainer
{
private int beatsHandled;
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
Divisor = beatsHandled < 4 ? 1 : 4;
for (int i = 0; i < (beatsHandled + 1); i++)
{
float angle = (float)(RNG.NextDouble() * 2 * Math.PI);
float randomRadius = (float)(Math.Sqrt(RNG.NextDouble()));
float x = 0.5f + 0.5f * randomRadius * (float)Math.Cos(angle);
float y = 0.5f + 0.5f * randomRadius * (float)Math.Sin(angle);
Color4 christmasColour = RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2;
Drawable triangle = new Triangle
{
Size = new Vector2(RNG.NextSingle() + 1.2f) * 80,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Both,
Position = new Vector2(x, y),
Colour = christmasColour
};
if (beatsHandled >= 10)
triangle.Blending = BlendingParameters.Additive;
AddInternal(triangle);
triangle
.ScaleTo(0.9f)
.ScaleTo(1, beat_length / 2, Easing.Out);
triangle.FadeInFromZero(100, Easing.OutQuint);
}
beatsHandled += 1;
}
}
}
}
}

View File

@ -0,0 +1,206 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Seasonal
{
public partial class MainMenuSeasonalLighting : CompositeDrawable
{
private IBindable<WorkingBeatmap> working = null!;
private InterpolatingFramedClock? beatmapClock;
private List<HitObject> hitObjects = null!;
private RulesetInfo? osuRuleset;
private int? lastObjectIndex;
public MainMenuSeasonalLighting()
{
// match beatmap playfield
RelativeChildSize = new Vector2(512, 384);
RelativeSizeAxes = Axes.X;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> working, RulesetStore rulesets)
{
// operate in osu! ruleset to keep things simple for now.
osuRuleset = rulesets.GetRuleset(0);
this.working = working.GetBoundCopy();
this.working.BindValueChanged(_ => Scheduler.AddOnce(updateBeatmap), true);
}
private void updateBeatmap()
{
lastObjectIndex = null;
if (osuRuleset == null)
{
beatmapClock = new InterpolatingFramedClock(Clock);
hitObjects = new List<HitObject>();
return;
}
// Intentionally maintain separately so the lighting is not in audio clock space (it shouldn't rewind etc.)
beatmapClock = new InterpolatingFramedClock(new FramedClock(working.Value.Track));
hitObjects = working.Value
.GetPlayableBeatmap(osuRuleset)
.HitObjects
.SelectMany(h => h.NestedHitObjects.Prepend(h))
.OrderBy(h => h.StartTime)
.ToList();
}
protected override void Update()
{
base.Update();
if (osuRuleset == null || beatmapClock == null)
return;
Height = DrawWidth / 16 * 10;
beatmapClock.ProcessFrame();
// intentionally slightly early since we are doing fades on the lighting.
double time = beatmapClock.CurrentTime + 50;
// handle seeks or OOB by skipping to current.
if (lastObjectIndex == null || lastObjectIndex >= hitObjects.Count || (lastObjectIndex >= 0 && hitObjects[lastObjectIndex.Value].StartTime > time)
|| Math.Abs(beatmapClock.ElapsedFrameTime) > 500)
lastObjectIndex = hitObjects.Count(h => h.StartTime < time) - 1;
while (lastObjectIndex < hitObjects.Count - 1)
{
var h = hitObjects[lastObjectIndex.Value + 1];
if (h.StartTime > time)
break;
// Don't add lighting if the game is running too slow.
if (Clock.ElapsedFrameTime < 20)
addLight(h);
lastObjectIndex++;
}
}
private void addLight(HitObject h)
{
var light = new Light
{
RelativePositionAxes = Axes.Both,
Position = ((IHasPosition)h).Position
};
AddInternal(light);
if (h.GetType().Name.Contains("Tick"))
{
light.Colour = SeasonalUIConfig.AMBIENT_COLOUR_1;
light.Scale = new Vector2(0.5f);
light
.FadeInFromZero(250)
.Then()
.FadeOutFromOne(1000, Easing.Out);
light.MoveToOffset(new Vector2(RNG.Next(-20, 20), RNG.Next(-20, 20)), 1400, Easing.Out);
}
else
{
// default are green
Color4 col = SeasonalUIConfig.PRIMARY_COLOUR_2;
// whistles are red
if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_WHISTLE))
col = SeasonalUIConfig.PRIMARY_COLOUR_1;
// clap is third ambient (yellow) colour
else if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP))
col = SeasonalUIConfig.AMBIENT_COLOUR_1;
light.Colour = col;
// finish results in larger lighting
if (h.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH))
light.Scale = new Vector2(3);
light
.FadeInFromZero(150)
.Then()
.FadeOutFromOne(1000, Easing.In);
}
light.Expire();
}
private partial class Light : CompositeDrawable
{
private readonly Circle circle;
public new Color4 Colour
{
set
{
circle.Colour = value.Darken(0.8f);
circle.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = value,
Radius = 80,
};
}
}
public Light()
{
InternalChildren = new Drawable[]
{
circle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(12),
Colour = SeasonalUIConfig.AMBIENT_COLOUR_1,
Blending = BlendingParameters.Additive,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = SeasonalUIConfig.AMBIENT_COLOUR_2,
Radius = 80,
}
}
};
Origin = Anchor.Centre;
Alpha = 0.5f;
}
}
}
}

View File

@ -0,0 +1,76 @@
// 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.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Screens.Menu;
using osuTK;
namespace osu.Game.Seasonal
{
public partial class OsuLogoChristmas : OsuLogo
{
protected override double BeatSampleVariance => 0.02;
private Sprite? hat;
private bool hasHat;
protected override MenuLogoVisualisation CreateMenuLogoVisualisation() => new SeasonalMenuLogoVisualisation();
[BackgroundDependencyLoader]
private void load(TextureStore textures, AudioManager audio)
{
LogoElements.Add(hat = new Sprite
{
BypassAutoSizeAxes = Axes.Both,
Alpha = 0,
Origin = Anchor.BottomCentre,
Scale = new Vector2(-1, 1),
Texture = textures.Get(@"Menu/hat"),
});
// override base samples with our preferred ones.
SampleDownbeat = SampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat-bell");
}
protected override void Update()
{
base.Update();
updateHat();
}
private void updateHat()
{
if (hat == null)
return;
bool shouldHat = DrawWidth * Scale.X < 400;
if (shouldHat != hasHat)
{
hasHat = shouldHat;
if (hasHat)
{
hat.Delay(400)
.Then()
.MoveTo(new Vector2(120, 160))
.RotateTo(0)
.RotateTo(-20, 500, Easing.OutQuint)
.FadeIn(250, Easing.OutQuint);
}
else
{
hat.Delay(100)
.Then()
.MoveToOffset(new Vector2(0, -5), 500, Easing.OutQuint)
.FadeOut(500, Easing.OutQuint);
}
}
}
}
}

View File

@ -0,0 +1,12 @@
// 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.Screens.Menu;
namespace osu.Game.Seasonal
{
internal partial class SeasonalMenuLogoVisualisation : MenuLogoVisualisation
{
protected override void UpdateColour() => Colour = SeasonalUIConfig.AMBIENT_COLOUR_1;
}
}

View File

@ -0,0 +1,18 @@
// 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.Utils;
using osu.Game.Screens.Menu;
using osuTK.Graphics;
namespace osu.Game.Seasonal
{
public partial class SeasonalMenuSideFlashes : MenuSideFlashes
{
protected override bool RefreshColoursEveryFlash => true;
protected override float Intensity => 4;
protected override Color4 GetBaseColour() => RNG.NextBool() ? SeasonalUIConfig.PRIMARY_COLOUR_1 : SeasonalUIConfig.PRIMARY_COLOUR_2;
}
}

View File

@ -0,0 +1,24 @@
// 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 osuTK.Graphics;
namespace osu.Game.Seasonal
{
/// <summary>
/// General configuration setting for seasonal event adjustments to the game.
/// </summary>
public static class SeasonalUIConfig
{
public static readonly bool ENABLED = false;
public static readonly Color4 PRIMARY_COLOUR_1 = Color4Extensions.FromHex(@"D32F2F");
public static readonly Color4 PRIMARY_COLOUR_2 = Color4Extensions.FromHex(@"388E3C");
public static readonly Color4 AMBIENT_COLOUR_1 = Color4Extensions.FromHex(@"FFFFCC");
public static readonly Color4 AMBIENT_COLOUR_2 = Color4Extensions.FromHex(@"FFE4B5");
}
}