Files
Vocaluxe/VocaluxeLib/Menu/CNameSelection.cs
T
McSchneidarandFlorian Ostertag 944022f30b Add knockout tournament party mode (#913)
A new party mode for 3 to 16 players who sing against each other in one-on-one duels. Whoever scores more points moves on, the loser is out, until only the champion is left.

- The bracket is shown mirrored: both halves lead towards the final in the center, where the champion is crowned
- Players are drawn randomly; if the number of players doesn't fill the bracket, some players get a bye in the first round
- Songs are picked randomly from all songs, a category or a playlist, sung as normal, short or medley version, and no song is played twice
- Each player has up to 3 jokers for the whole tournament to skip a song they don't like
- On a tie, the duel is sung again
- The winner of each duel visibly moves on to the next round, and the champion is celebrated at the end

Also fixes "Select random" in the player selection of party modes, which only filled half of the slots.

Co-authored-by: Florian Ostertag <flokuep@users.noreply.github.com>
2026-10-18 10:36:19 +02:00

582 lines
18 KiB
C#

#region license
// This file is part of Vocaluxe.
//
// Vocaluxe is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Vocaluxe is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Vocaluxe. If not, see <http://www.gnu.org/licenses/>.
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Xml.Serialization;
using VocaluxeLib.Draw;
namespace VocaluxeLib.Menu
{
[XmlType("NameSelection")]
public struct SThemeNameSelection
{
[XmlAttribute(AttributeName = "Name")]
public string Name;
public SRectF Rect;
public string SkinEmptyTile;
public SThemeColor ColorEmptyTile;
public string SkinTileSelected;
public SThemeNameSelectionTiles Tiles;
}
public struct SThemeNameSelectionTiles
{
public int W;
public int H;
public int NumW;
public int NumH;
public float SpaceW;
public float SpaceH;
public SThemeNameSelectionName Name;
}
public struct SThemeNameSelectionName
{
public float Space;
[XmlElement("H")]
public float Height;
public string Font;
public EStyle Style;
public SThemeColor Color;
}
public class CNameSelection : CMenuElementBase, IMenuElement, IThemeable
{
private class CTile
{
public Guid ProfileId;
public readonly CStatic Avatar;
public readonly CText Name;
public CTile(CStatic av, CText tex, Guid pId)
{
Avatar = av;
Name = tex;
ProfileId = pId;
}
}
private readonly int _PartyModeId;
private SThemeNameSelection _Theme;
public string GetThemeName()
{
return _Theme.Name;
}
public bool ThemeLoaded { get; private set; }
private readonly List<CTile> _Tiles;
private CTextureRef _TextureTileSelected;
public CTextureRef TextureEmptyTile { get; private set; }
private SColorF _ColorEmptyTile;
private SColorF _ColorNameTile;
public int Offset;
private int _ActualSelection = -1;
public Guid SelectedId = Guid.Empty;
private int _Player = -1;
private readonly List<Guid> _VisibleProfiles;
private readonly List<Guid> _UsedProfiles;
private CStatic _PlayerSelector;
private SRectF _Rect;
public override SRectF Rect
{
get { return _Rect; }
}
public bool Selectable
{
get { return Visible; }
}
public CNameSelection(int partyModeId)
{
_PartyModeId = partyModeId;
_Theme = new SThemeNameSelection { Tiles = new SThemeNameSelectionTiles { Name = new SThemeNameSelectionName() } };
_Tiles = new List<CTile>();
_VisibleProfiles = new List<Guid>();
_UsedProfiles = new List<Guid>();
}
public CNameSelection(SThemeNameSelection theme, int partyModeId)
{
_PartyModeId = partyModeId;
_Theme = theme;
_Tiles = new List<CTile>();
_VisibleProfiles = new List<Guid>();
_UsedProfiles = new List<Guid>();
ThemeLoaded = true;
}
public void Init()
{
_UsedProfiles.Clear();
_PrepareTiles();
_PlayerSelector = new CStatic(_PartyModeId, _TextureTileSelected, new SColorF(), new SRectF(0, 0, _Theme.Tiles.W + 10, _Theme.Tiles.H + 10, Rect.Z - 0.5f))
{
Visible = true
};
_UpdateVisibleProfiles();
UpdateList(0);
_ActualSelection = _Tiles.FindIndex(t => t.ProfileId != Guid.Empty);
SelectedId = _ActualSelection >= 0 ? _Tiles[_ActualSelection].ProfileId : Guid.Empty;
}
public void Draw()
{
var i = 0;
foreach (var tile in _Tiles)
{
tile.Avatar.Draw();
tile.Name.Draw();
if (_PlayerSelector.Visible)
{
//Update PlayerSelector-Coords
if (_Player > -1 && _ActualSelection == i)
{
_PlayerSelector.X = tile.Avatar.Rect.X - 3;
_PlayerSelector.Y = tile.Avatar.Rect.Y - 3;
}
}
i++;
}
if (_PlayerSelector.Visible)
{
//Draw PlayerSelector
_PlayerSelector.Draw();
}
}
public void HandleInput(SKeyEvent kevent)
{
if (_Tiles.Count == 0 || _VisibleProfiles.Count == 0)
{
_ActualSelection = -1;
SelectedId = Guid.Empty;
return;
}
if (_ActualSelection < 0 || _ActualSelection >= _Tiles.Count || _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection = _Tiles.FindIndex(t => t.ProfileId != Guid.Empty);
if (_ActualSelection < 0)
{
SelectedId = Guid.Empty;
return;
}
}
switch (kevent.Key)
{
case Keys.Right:
if (_ActualSelection + 1 < _Tiles.Count)
{
if (_Tiles[_ActualSelection + 1].ProfileId != Guid.Empty)
{
_ActualSelection++;
}
}
else
{
var oldOffset = Offset;
UpdateList(Offset + 1);
if (oldOffset != Offset)
{
_ActualSelection = Math.Max(0, _ActualSelection - (_Theme.Tiles.NumW - 1));
}
}
break;
case Keys.Left:
if (_ActualSelection - 1 >= 0)
{
_ActualSelection--;
}
else if (Offset > 0)
{
UpdateList(Offset - 1);
_ActualSelection = Math.Min(_Tiles.Count - 1, _ActualSelection + (_Theme.Tiles.NumW - 1));
while (_ActualSelection >= 0 && _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection--;
}
}
break;
case Keys.Up:
if (_ActualSelection - _Theme.Tiles.NumW >= 0)
{
_ActualSelection -= _Theme.Tiles.NumW;
}
else if (Offset > 0)
{
var currentColumn = _ActualSelection % _Theme.Tiles.NumW;
UpdateList(Offset - 1);
_ActualSelection = currentColumn;
if (_ActualSelection >= _Tiles.Count)
{
_ActualSelection = _Tiles.Count - 1;
}
while (_ActualSelection >= 0 && _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection--;
}
}
break;
case Keys.Down:
if (_ActualSelection + _Theme.Tiles.NumW < _Tiles.Count &&
_Tiles[_ActualSelection + _Theme.Tiles.NumW].ProfileId != Guid.Empty)
{
_ActualSelection += _Theme.Tiles.NumW;
}
else
{
var oldOffset = Offset;
var currentColumn = _ActualSelection % _Theme.Tiles.NumW;
UpdateList(Offset + 1);
if (oldOffset != Offset)
{
_ActualSelection = currentColumn;
while (_ActualSelection < _Tiles.Count && _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection++;
}
if (_ActualSelection >= _Tiles.Count || _Tiles[_ActualSelection].ProfileId == Guid.Empty)
{
_ActualSelection = _Tiles.FindLastIndex(t => t.ProfileId != Guid.Empty);
}
}
}
break;
}
if (_ActualSelection >= 0 && _ActualSelection < _Tiles.Count && _Tiles[_ActualSelection].ProfileId != Guid.Empty)
{
SelectedId = _Tiles[_ActualSelection].ProfileId;
}
else
{
SelectedId = Guid.Empty;
}
}
public void HandleMouse(SMouseEvent mevent)
{
for (var i = 0; i < _Tiles.Count; i++)
{
if (CHelper.IsInBounds(_Tiles[i].Avatar.Rect, mevent) && _Tiles[i].ProfileId != Guid.Empty)
{
_ActualSelection = i;
if (Offset * _Theme.Tiles.NumW + _ActualSelection < _VisibleProfiles.Count)
{
SelectedId = _VisibleProfiles.ElementAt(Offset * _Theme.Tiles.NumW + _ActualSelection);
}
else
{
SelectedId = Guid.Empty;
}
}
}
}
public void FastSelection(bool active, int player)
{
//Overwrite player-selection; Same profile, but other player
if (active && SelectedId != Guid.Empty)
{
_Player = player;
_PlayerSelector.Color = CBase.Themes.GetPlayerColor(player);
}
//Normal activation
else if (active)
{
if (_VisibleProfiles.Count > 0)
{
SelectedId = _VisibleProfiles.ElementAt(0);
_ActualSelection = 0;
}
else
{
SelectedId = Guid.Empty;
_ActualSelection = -1;
}
_Player = player;
_PlayerSelector.Color = CBase.Themes.GetPlayerColor(player);
_PlayerSelector.Visible = true;
}
//Deactivate
else
{
SelectedId = Guid.Empty;
_ActualSelection = -1;
_Player = -1;
_PlayerSelector.Visible = false;
}
}
public void UpdateList()
{
_UpdateVisibleProfiles();
if (_Theme.Tiles.NumW * (Offset + 1) - _VisibleProfiles.Count >= _Theme.Tiles.NumW * Offset)
{
UpdateList(Offset - 1);
}
else
{
UpdateList(Offset);
}
}
public void UpdateList(int offset)
{
offset = offset.Clamp(0, _GetMaxOffset());
for (var i = 0; i < _Tiles.Count; i++)
{
if (i + offset * _Theme.Tiles.NumW < _VisibleProfiles.Count)
{
_Tiles[i].Avatar.Texture = CBase.Profiles.GetAvatar(_VisibleProfiles[i + offset * _Theme.Tiles.NumW]);
_Tiles[i].Avatar.Color = new SColorF(1, 1, 1, 1);
_Tiles[i].Name.Text = CBase.Profiles.GetPlayerName(_VisibleProfiles[i + offset * _Theme.Tiles.NumW]);
_Tiles[i].ProfileId = _VisibleProfiles.ElementAt(i + offset * _Theme.Tiles.NumW);
}
else
{
_Tiles[i].Avatar.Texture = TextureEmptyTile;
_Tiles[i].Avatar.Color = _ColorEmptyTile;
_Tiles[i].Name.Text = "";
_Tiles[i].ProfileId = Guid.Empty;
}
}
Offset = offset;
}
public bool IsOverTile(SMouseEvent mevent)
{
var isOver = false;
foreach (var tile in _Tiles)
{
if (CHelper.IsInBounds(tile.Avatar.Rect, mevent))
{
isOver = true;
}
}
return isOver;
}
public Guid TilePlayerId(SMouseEvent mevent)
{
foreach (var tile in _Tiles)
{
if (CHelper.IsInBounds(tile.Avatar.Rect, mevent))
{
return tile.ProfileId;
}
}
return Guid.Empty;
}
public CStatic TilePlayerAvatar(SMouseEvent mevent)
{
foreach (var tile in _Tiles)
{
if (CHelper.IsInBounds(tile.Avatar.Rect, mevent))
{
return tile.Avatar;
}
}
return new CStatic(_PartyModeId);
}
public void UnloadSkin() { }
public void LoadSkin()
{
TextureEmptyTile = CBase.Themes.GetSkinTexture(_Theme.SkinEmptyTile, _PartyModeId);
_TextureTileSelected = CBase.Themes.GetSkinTexture(_Theme.SkinTileSelected, _PartyModeId);
_Theme.ColorEmptyTile.Get(_PartyModeId, out _ColorEmptyTile);
_Theme.Tiles.Name.Color.Get(_PartyModeId, out _ColorNameTile);
MaxRect = _Theme.Rect;
}
public void ReloadSkin()
{
UnloadSkin();
LoadSkin();
_PrepareTiles();
}
public void UseProfile(Guid id)
{
if (id != Guid.Empty)
{
if (!_UsedProfiles.Contains(id) && CBase.Profiles.IsProfileIdValid(id) && !CBase.Profiles.IsGuest(id))
{
_UsedProfiles.Add(id);
UpdateList();
}
}
}
public void RemoveUsedProfile(Guid id)
{
if (id != Guid.Empty)
{
_UsedProfiles.Remove(id);
UpdateList();
}
}
public Guid GetRandomUnusedProfile()
{
var unusedProfiles = _VisibleProfiles.Where(profileId => !_UsedProfiles.Contains(profileId)).ToList();
if (unusedProfiles.Count == 0)
{
return Guid.Empty;
}
return unusedProfiles[CBase.Game.GetRandom(unusedProfiles.Count)];
}
private void _PrepareTiles()
{
_Tiles.Clear();
for (var i = 0; i < _Theme.Tiles.NumH; i++)
{
for (var j = 0; j < _Theme.Tiles.NumW; j++)
{
var rect = new SRectF(MaxRect.X + j * (_Theme.Tiles.W + _Theme.Tiles.SpaceW), MaxRect.Y + i * (_Theme.Tiles.H + _Theme.Tiles.SpaceH),
_Theme.Tiles.W, _Theme.Tiles.H, MaxRect.Z);
var tileStatic = new CStatic(_PartyModeId, TextureEmptyTile, _ColorEmptyTile, rect) { Aspect = EAspect.Crop };
var tileText = new CText(rect.X + rect.W / 2, rect.Y + rect.H + _Theme.Tiles.Name.Space, rect.Z, _Theme.Tiles.Name.Height, rect.W, EAlignment.Center,
_Theme.Tiles.Name.Style, _Theme.Tiles.Name.Font, _ColorNameTile, "");
_Tiles.Add(new CTile(tileStatic, tileText, Guid.Empty));
}
}
_Rect.X = MaxRect.X;
_Rect.Y = MaxRect.Y;
_Rect.Right = _Tiles[_Tiles.Count - 1].Avatar.Rect.Right;
_Rect.Bottom = _Tiles[_Tiles.Count - 1].Name.Rect.Bottom;
_Rect.Z = MaxRect.Z;
}
private void _UpdateVisibleProfiles()
{
_VisibleProfiles.Clear();
var profiles = CBase.Profiles.GetProfiles();
foreach (var profile in profiles)
{
var visible = profile.Active == EOffOn.TR_CONFIG_ON;
if (visible)
{
if (_UsedProfiles.Contains(profile.Id) && (int)profile.UserRole >= (int)EUserRole.TR_USERROLE_NORMAL)
{
visible = false;
}
}
if (visible)
{
_VisibleProfiles.Add(profile.Id);
}
}
}
private int _GetMaxOffset()
{
var maxOffset = (int)Math.Ceiling((decimal)(_VisibleProfiles.Count - _Tiles.Count) / _Theme.Tiles.NumW);
if (maxOffset < 0)
{
return 0;
}
return maxOffset;
}
public object GetTheme()
{
return _Theme;
}
#region ThemeEdit
public void MoveElement(int stepX, int stepY)
{
X += stepX;
Y += stepY;
_Theme.Rect.X += stepX;
_Theme.Rect.Y += stepY;
}
public void ResizeElement(int stepW, int stepH)
{
W += stepW;
if (W <= 0)
{
W = 1;
}
H += stepH;
if (H <= 0)
{
H = 1;
}
_Theme.Rect.W = W;
_Theme.Rect.H = H;
}
#endregion ThemeEdit
}
}