Improve challenge party mode (#911)

- Instead of a number of rounds, you now choose how many songs each player sings
- New option to remember sung songs, so a new challenge doesn't repeat songs from the previous ones
- Up to 16 players
- The number of jokers and the joker refill setting are kept instead of being reset every time
- The overview shows each player's avatar, the top three in gold, silver and bronze and highlights the leader
- The winner of each round is highlighted in the round list
- The winner is celebrated at the end with avatar, a pulsing gold name and stars

Co-authored-by: Florian Ostertag <flokuep@users.noreply.github.com>
This commit is contained in:
McSchneidarandFlorian Ostertag authored and GitHub committed 2026-10-17 21:18:49 +02:00
1 parent aac5253862
commit b4c29e1b55
7 files changed
+381 -83

No files matched your search

@@ -86,7 +86,7 @@ namespace VocaluxeLib.PartyModes.Challenge
}
public override int MaxPlayers
{
get { return 12; }
get { return 16; }
}
public override int MinTeams
{
@@ -121,8 +121,10 @@ namespace VocaluxeLib.PartyModes.Challenge
public int NumPlayer;
public int NumPlayerAtOnce;
public int NumRounds;
public int SongsPerPlayer;
public int NumJokers;
public bool RefillJokers;
public bool RememberSongs;
public int[] Jokers;
public List<Guid> ProfileIds;
@@ -153,6 +155,7 @@ namespace VocaluxeLib.PartyModes.Challenge
public SData GameData;
private EStage _Stage;
private readonly HashSet<int> _SungSongs = new HashSet<int>();
public CPartyModeChallenge(int id) : base(id)
{
@@ -171,9 +174,11 @@ namespace VocaluxeLib.PartyModes.Challenge
{
NumPlayer = 4,
NumPlayerAtOnce = 2,
NumRounds = 12,
NumRounds = 6,
SongsPerPlayer = 3,
NumJokers = 5,
RefillJokers = true,
RememberSongs = true,
CurrentRoundNr = 1,
ProfileIds = new List<Guid>(),
Sorting = CBase.Config.GetSongSorting(),
@@ -258,6 +263,19 @@ namespace VocaluxeLib.PartyModes.Challenge
case EStage.Names:
_Stage = EStage.Main;
CBase.Songs.ResetSongSung();
if (GameData.RememberSongs)
{
foreach (var songId in _SungSongs)
{
CBase.Songs.AddPartySongSung(songId);
}
}
else
{
_SungSongs.Clear();
}
GameData.Songs.Clear();
GameData.ResultTable = new List<CResultTableRow>();
GameData.Rounds = new CChallengeRounds(GameData.NumRounds, GameData.NumPlayer, GameData.NumPlayerAtOnce);
GameData.CurrentRoundNr = 1;
@@ -398,6 +416,7 @@ namespace VocaluxeLib.PartyModes.Challenge
for (var i = 0; i < CBase.Game.GetNumSongs(); i++)
{
CBase.Songs.AddPartySongSung(CBase.Game.GetSong(i).Id);
_SungSongs.Add(CBase.Game.GetSong(i).Id);
}
GameData.CurrentRoundNr++;
@@ -609,6 +628,12 @@ namespace VocaluxeLib.PartyModes.Challenge
break;
}
var notSung = GameData.Songs.Where(songId => !_SungSongs.Contains(songId)).ToList();
if (notSung.Count >= GameData.NumMedleySongs)
{
GameData.Songs = notSung;
}
GameData.Songs.Shuffle();
}
@@ -27,25 +27,25 @@ namespace VocaluxeLib.PartyModes.Challenge
// Version number for theme files. Increment it, if you've changed something on the theme files!
protected override int _ScreenVersion
{
get { return 3; }
get { return 4; }
}
private const string _SelectSlideNumPlayers = "SelectSlideNumPlayers";
private const string _SelectSlideNumMics = "SelectSlideNumMics";
private const string _SelectSlideNumRounds = "SelectSlideNumRounds";
private const string _SelectSlideSongsPerPlayer = "SelectSlideSongsPerPlayer";
private const string _SelectSlideNumJokers = "SelectSlideNumJokers";
private const string _SelectSlideRefillJokers = "SelectSlideRefillJokers";
private const string _SelectSlideRememberSongs = "SelectSlideRememberSongs";
private const string _ButtonNext = "ButtonNext";
private const string _ButtonBack = "ButtonBack";
private const int _MaxNumRounds = 100;
private int _RoundSteps = 1;
private const int _MaxSongsPerPlayer = 10;
public override void Init()
{
base.Init();
_ThemeSelectSlides = new string[] { _SelectSlideNumPlayers, _SelectSlideNumMics, _SelectSlideNumRounds, _SelectSlideNumJokers, _SelectSlideRefillJokers };
_ThemeSelectSlides = new string[] { _SelectSlideNumPlayers, _SelectSlideNumMics, _SelectSlideSongsPerPlayer, _SelectSlideNumJokers, _SelectSlideRefillJokers, _SelectSlideRememberSongs };
_ThemeButtons = new string[] { _ButtonNext, _ButtonBack };
}
@@ -147,44 +147,48 @@ namespace VocaluxeLib.PartyModes.Challenge
_SelectSlides[_SelectSlideNumJokers].AddValue(i.ToString());
}
_SelectSlides[_SelectSlideNumJokers].SelectedValue = "5";
_SelectSlides[_SelectSlideNumJokers].SelectedValue = _PartyMode.GameData.NumJokers.ToString();
//build joker config slide
_SelectSlides[_SelectSlideRefillJokers].Clear();
_SelectSlides[_SelectSlideRefillJokers].AddValue(CBase.Language.Translate("TR_BUTTON_NO", PartyModeId));
_SelectSlides[_SelectSlideRefillJokers].AddValue(CBase.Language.Translate("TR_BUTTON_YES", PartyModeId));
_SelectSlides[_SelectSlideRefillJokers].SelectLastValue();
_SelectSlides[_SelectSlideRefillJokers].Selection = _PartyMode.GameData.RefillJokers ? 1 : 0;
_SelectSlides[_SelectSlideSongsPerPlayer].Clear();
for (var i = 1; i <= _MaxSongsPerPlayer; i++)
{
_SelectSlides[_SelectSlideSongsPerPlayer].AddValue(i.ToString());
}
_SelectSlides[_SelectSlideSongsPerPlayer].SelectedValue = _PartyMode.GameData.SongsPerPlayer.ToString();
_SelectSlides[_SelectSlideRememberSongs].Clear();
_SelectSlides[_SelectSlideRememberSongs].AddValue(CBase.Language.Translate("TR_BUTTON_NO", PartyModeId));
_SelectSlides[_SelectSlideRememberSongs].AddValue(CBase.Language.Translate("TR_BUTTON_YES", PartyModeId));
_SelectSlides[_SelectSlideRememberSongs].Selection = _PartyMode.GameData.RememberSongs ? 1 : 0;
_UpdateMicsAtOnce();
_SetRoundSteps();
_UpdateSlideRounds();
_UpdateNumRounds();
}
private void _UpdateSlides()
{
var player = _PartyMode.GameData.NumPlayer;
var mics = _PartyMode.GameData.NumPlayerAtOnce;
_PartyMode.GameData.NumPlayer = _SelectSlides[_SelectSlideNumPlayers].Selection + _PartyMode.MinPlayers;
_PartyMode.GameData.NumPlayerAtOnce = _SelectSlides[_SelectSlideNumMics].Selection + _PartyMode.MinPlayers;
_PartyMode.GameData.NumRounds = (_SelectSlides[_SelectSlideNumRounds].Selection + 1) * _RoundSteps;
_PartyMode.GameData.SongsPerPlayer = _SelectSlides[_SelectSlideSongsPerPlayer].Selection + 1;
_PartyMode.GameData.NumJokers = _SelectSlides[_SelectSlideNumJokers].Selection + 1;
_PartyMode.GameData.RefillJokers = _SelectSlides[_SelectSlideRefillJokers].Selection == 1 ? true : false;
_PartyMode.GameData.RefillJokers = _SelectSlides[_SelectSlideRefillJokers].Selection == 1;
_PartyMode.GameData.RememberSongs = _SelectSlides[_SelectSlideRememberSongs].Selection == 1;
_UpdateMicsAtOnce();
_SetRoundSteps();
_UpdateNumRounds();
}
if (player != _PartyMode.GameData.NumPlayer || mics != _PartyMode.GameData.NumPlayerAtOnce)
{
var num = CHelper.CombinationCount(_PartyMode.GameData.NumPlayer, _PartyMode.GameData.NumPlayerAtOnce);
while (num > _MaxNumRounds)
{
num -= _RoundSteps;
}
_PartyMode.GameData.NumRounds = num;
}
_UpdateSlideRounds();
private void _UpdateNumRounds()
{
var numSongs = _PartyMode.GameData.NumPlayer * _PartyMode.GameData.SongsPerPlayer;
_PartyMode.GameData.NumRounds = Math.Max(1, (numSongs + _PartyMode.GameData.NumPlayerAtOnce - 1) / _PartyMode.GameData.NumPlayerAtOnce);
}
private void _UpdateMicsAtOnce()
@@ -205,31 +209,5 @@ namespace VocaluxeLib.PartyModes.Challenge
_SelectSlides[_SelectSlideNumMics].Selection = _PartyMode.GameData.NumPlayerAtOnce - _PartyMode.MinPlayers;
}
private void _UpdateSlideRounds()
{
// build num rounds slide
_SelectSlides[_SelectSlideNumRounds].Clear();
for (var i = _RoundSteps; i <= _MaxNumRounds; i += _RoundSteps)
{
_SelectSlides[_SelectSlideNumRounds].AddValue(i.ToString());
}
_SelectSlides[_SelectSlideNumRounds].Selection = _PartyMode.GameData.NumRounds / _RoundSteps - 1;
}
private void _SetRoundSteps()
{
if (_PartyMode.GameData.NumPlayerAtOnce < 1 || _PartyMode.GameData.NumPlayer < 1 || _PartyMode.GameData.NumPlayerAtOnce > _PartyMode.GameData.NumPlayer)
{
_RoundSteps = 1;
return;
}
var res = _PartyMode.GameData.NumPlayer / _PartyMode.GameData.NumPlayerAtOnce;
var mod = _PartyMode.GameData.NumPlayer % _PartyMode.GameData.NumPlayerAtOnce;
_RoundSteps = mod == 0 ? res : _PartyMode.GameData.NumPlayer;
}
}
}
@@ -17,6 +17,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows.Forms;
using VocaluxeLib.Menu;
@@ -30,6 +31,8 @@ namespace VocaluxeLib.PartyModes.Challenge
public CText Won;
public CText SingPoints;
public CText GamePoints;
public CStatic Avatar;
public CStatic Highlight;
}
class CRoundsTableRow
@@ -46,7 +49,7 @@ namespace VocaluxeLib.PartyModes.Challenge
// Version number for theme files. Increment it, if you've changed something on the theme files!
protected override int _ScreenVersion
{
get { return 1; }
get { return 2; }
}
private const string _TextPosition = "TextPosition";
@@ -63,6 +66,7 @@ namespace VocaluxeLib.PartyModes.Challenge
private const string _TextFinishMessage = "TextFinishMessage";
private const string _TextFinishPlayerWin = "TextFinishPlayerWin";
private const string _TextNextPlayerMessage = "TextNextPlayerMessage";
private const string _TextRoundWinner = "TextRoundWinner";
private const string _ButtonNextRound = "ButtonNextRound";
private const string _ButtonBack = "ButtonBack";
@@ -76,6 +80,17 @@ namespace VocaluxeLib.PartyModes.Challenge
private const string _StaticPopupBG = "StaticPopupBG";
private const string _StaticNextPlayer = "StaticNextPlayer";
private const string _StaticTableAvatar = "StaticTableAvatar";
private const string _StaticTableHighlight = "StaticTableHighlight";
private const string _ParticleEffectWinner = "ParticleEffectWinner";
private const float _WinnerPopDuration = 700f;
private const float _WinnerAvatarY = 885f;
private readonly Stopwatch _WinnerTimer = new Stopwatch();
private float _WinnerTextHeight;
private readonly string[] _RankColors = new string[] { "Top1Color", "Top2Color", "Top3Color" };
private bool _ExitPopupVisible;
@@ -100,14 +115,15 @@ namespace VocaluxeLib.PartyModes.Challenge
{
_TextPosition, _TextPlayerName, _TextNumPlayed, _TextWon, _TextSingPoints, _TextGamePoints, _TextNextPlayer, _TextPopupReallyExit, _TextRoundNumber,
_TextRoundPlayer,
_TextRoundScore, _TextFinishMessage, _TextFinishPlayerWin, _TextNextPlayerMessage
_TextRoundScore, _TextFinishMessage, _TextFinishPlayerWin, _TextNextPlayerMessage, _TextRoundWinner
};
_ThemeButtons = new string[]
{
_ButtonNextRound, _ButtonBack, _ButtonExit, _ButtonPopupYes, _ButtonPopupNo, _ButtonPlayerScrollDown, _ButtonPlayerScrollUp, _ButtonRoundsScrollDown,
_ButtonRoundsScrollUp
};
_ThemeStatics = new string[] { _StaticPopupBG, _StaticNextPlayer };
_ThemeStatics = new string[] { _StaticPopupBG, _StaticNextPlayer, _StaticTableAvatar, _StaticTableHighlight };
_ThemeParticleEffects = new string[] { _ParticleEffectWinner };
}
public override void LoadTheme(string xmlPath)
@@ -129,6 +145,10 @@ namespace VocaluxeLib.PartyModes.Challenge
}
_Statics[_StaticNextPlayer].Visible = false;
_Statics[_StaticTableAvatar].Visible = false;
_Statics[_StaticTableHighlight].Visible = false;
_Texts[_TextRoundWinner].Visible = false;
_WinnerTextHeight = _Texts[_TextFinishPlayerWin].Font.Height;
}
public override bool HandleInput(SKeyEvent keyEvent)
@@ -356,16 +376,80 @@ namespace VocaluxeLib.PartyModes.Challenge
_Texts[_TextFinishPlayerWin].Visible = true;
_Texts[_TextFinishPlayerWin].Text = _GetPlayerWinString();
_SelectElement(_Buttons[_ButtonExit]);
_ShowWinnerAvatars();
}
_ParticleEffects[_ParticleEffectWinner].Visible = _Texts[_TextFinishPlayerWin].Visible;
_WinnerTimer.Reset();
_WinnerTimer.Start();
_ShowPopup(false);
}
public override bool UpdateGame()
{
if (_Texts[_TextFinishPlayerWin].Visible)
{
_UpdateWinnerAnimation();
}
return true;
}
private static float _EaseOutBack(float t)
{
const float c1 = 1.70158f;
const float c3 = c1 + 1f;
return 1f + c3 * (float)Math.Pow(t - 1f, 3) + c1 * (float)Math.Pow(t - 1f, 2);
}
private void _UpdateWinnerAnimation()
{
var time = _WinnerTimer.ElapsedMilliseconds;
float scale;
if (time < _WinnerPopDuration)
{
scale = 0.3f + 0.7f * _EaseOutBack(time / _WinnerPopDuration);
}
else
{
scale = 1f + 0.03f * (float)Math.Sin((time - _WinnerPopDuration) / 1000.0 * Math.PI * 1.4);
}
_Texts[_TextFinishPlayerWin].Font.Height = _WinnerTextHeight * scale;
}
private void _ShowWinnerAvatars()
{
var winners = new List<Guid>();
foreach (var row in _PartyMode.GameData.ResultTable)
{
if (row.Position == 1 && winners.Count < _NextPlayerStatics.Count)
{
winners.Add(row.PlayerId);
}
}
const float gap = 20f;
var size = _Statics[_StaticNextPlayer].Rect.W;
var x = (CBase.Settings.GetRenderW() - winners.Count * size - (winners.Count - 1) * gap) / 2;
for (var i = 0; i < _NextPlayerStatics.Count; i++)
{
var visible = i < winners.Count;
_NextPlayerStatics[i].Visible = visible;
_NextPlayerTexts[i].Visible = false;
if (!visible)
{
continue;
}
_NextPlayerStatics[i].Texture = CBase.Profiles.GetAvatar(winners[i]);
_NextPlayerStatics[i].X = x;
_NextPlayerStatics[i].Y = _WinnerAvatarY;
x += size + gap;
}
}
private void _EndParty()
{
CBase.Graphics.FadeTo(EScreen.Party);
@@ -557,6 +641,10 @@ namespace VocaluxeLib.PartyModes.Challenge
{
_RoundsTable[i].TextScores[p].Text = "";
}
var color = _IsRoundWinner(i + _RoundsTableOffset, p) ? _Texts[_TextRoundWinner].Color : _Texts[_TextRoundPlayer].Color;
_RoundsTable[i].TextPlayer[p].Color = color;
_RoundsTable[i].TextScores[p].Color = color;
}
else
{
@@ -580,6 +668,22 @@ namespace VocaluxeLib.PartyModes.Challenge
_Buttons[_ButtonRoundsScrollDown].Visible = _PartyMode.GameData.Rounds.Count - _NumRoundsVisible - _RoundsTableOffset > 0;
}
private bool _IsRoundWinner(int round, int player)
{
if (_PartyMode.GameData.CurrentRoundNr - 1 <= round || _PartyMode.GameData.Results == null)
{
return false;
}
var max = 0;
for (var p = 0; p < _PartyMode.GameData.NumPlayerAtOnce; p++)
{
max = Math.Max(max, _PartyMode.GameData.Results[round, p]);
}
return max > 0 && _PartyMode.GameData.Results[round, player] == max;
}
private void _BuildPlayerTable()
{
_PlayerTableScrollArea = new SRectF { X = _Texts[_TextPosition].X, Y = _Texts[_TextPosition].Y, W = _Texts[_TextGamePoints].X - _Texts[_TextPosition].X };
@@ -610,6 +714,17 @@ namespace VocaluxeLib.PartyModes.Challenge
row.Pos.Text = i + 1 + ".";
row.Avatar = GetNewStatic(_Statics[_StaticTableAvatar]);
row.Avatar.Aspect = EAspect.Crop;
row.Avatar.Y = row.Name.Y + (row.Name.H - row.Avatar.H) / 2;
row.Avatar.Visible = false;
_AddStatic(row.Avatar);
row.Highlight = GetNewStatic(_Statics[_StaticTableHighlight]);
row.Highlight.Y = row.Name.Y + (row.Name.H - row.Highlight.H) / 2;
row.Highlight.Visible = false;
_AddStatic(row.Highlight);
row.Pos.Visible = false;
row.Name.Visible = false;
row.Rounds.Visible = false;
@@ -653,6 +768,21 @@ namespace VocaluxeLib.PartyModes.Challenge
row.Won.Text = _PartyMode.GameData.ResultTable[i + _PlayerTableOffset].NumWon.ToString();
row.SingPoints.Text = _PartyMode.GameData.ResultTable[i + _PlayerTableOffset].NumSingPoints.ToString();
row.GamePoints.Text = _PartyMode.GameData.ResultTable[i + _PlayerTableOffset].NumGamePoints.ToString();
var position = _PartyMode.GameData.ResultTable[i + _PlayerTableOffset].Position;
row.Avatar.Texture = CBase.Profiles.GetAvatar(_PartyMode.GameData.ResultTable[i + _PlayerTableOffset].PlayerId);
row.Avatar.Visible = true;
row.Highlight.Visible = position == 1;
SColorF rankColor;
var color = _Texts[_TextPlayerName].Color;
if (position <= _RankColors.Length && CBase.Themes.GetColor(_RankColors[position - 1], PartyModeId, out rankColor))
{
color = rankColor;
}
row.Pos.Color = color;
row.Name.Color = color;
}
else
{
@@ -662,6 +792,8 @@ namespace VocaluxeLib.PartyModes.Challenge
row.Won.Visible = false;
row.SingPoints.Visible = false;
row.GamePoints.Visible = false;
row.Avatar.Visible = false;
row.Highlight.Visible = false;
}
}