From bbc473a329aad916415bc27025466a383591ae7a Mon Sep 17 00:00:00 2001 From: Beu Date: Wed, 11 May 2022 20:47:37 +0200 Subject: [PATCH] initial commit of the mode LapsKnockout --- TM_LapsKnockout.Script.txt | 420 +++++++++++++++++++++++++++++++++++++ 1 file changed, 420 insertions(+) create mode 100644 TM_LapsKnockout.Script.txt diff --git a/TM_LapsKnockout.Script.txt b/TM_LapsKnockout.Script.txt new file mode 100644 index 0000000..a57fb8e --- /dev/null +++ b/TM_LapsKnockout.Script.txt @@ -0,0 +1,420 @@ +/** + * Laps Knockout mode + */ +// #RequireContext CSmMode + +#Extends "Libs/Nadeo/TMNext/TrackMania/Modes/TMNextBase.Script.txt" + +#Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race" +#Const Version "2022-05-11" +#Const ScriptName "Modes/TM2020-Gamemodes/TM_LapsKnockout.Script.txt" + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Libraries +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Include "TextLib" as TL +#Include "MathLib" as ML +#Include "Libs/Nadeo/TMNext/TrackMania/Menu/Constants.Script.txt" as MenuConsts +#Include "Libs/Nadeo/TMNext/TrackMania/Modes/Laps/StateManager.Script.txt" as StateMgr +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/ScoresTable_Server.Script.txt" as UIModules_ScoresTable +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/TimeGap_Server.Script.txt" as UIModules_TimeGap +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/Checkpoint_Server.Script.txt" as UIModules_Checkpoint +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/PauseMenuOnline_Server.Script.txt" as UIModules_PauseMenu_Online +#Include "ManiaApps/Nadeo/TMNext/TrackMania/LapsCommon/Libs/Constants.Script.txt" as LibLaps_Constants + +#Include "ManiaApps/Nadeo/TMxSM/Race/UIModules/BigMessage_Server.Script.txt" as UIModules_BigMessage + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Settings +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Setting S_DisableGiveUp True as _("Disable give up") +#Setting S_WarmUpNb 0 as _("Number of warm up") +#Setting S_WarmUpDuration 0 as _("Duration of one warm up") +#Setting S_WarmUpTimeout -1 as _("Warm up timeout") + +#Setting S_NbLapsWithoutKO 0 as "Number of laps without elimination" +#Setting S_EliminatedPlayersNbRanks "4,16,16" as _("Nb of players above which one extra elim. /lap. Same setting of Knock") + +/* About S_EliminatedPlayersNbRanks + * Example : "8,16" + * 1 to 8 players -> 1 elimination per lap + * 9 to 16 players -> 2 eliminations per lap + * 17 or more players -> 3 eliminations per lap + * + * Example : "8,16,16" + * 1 to 8 players -> 1 eliminations per lap + * 9 to 16 players -> 2 eliminations per lap + * 17 or more players -> 4 eliminations per lap + * + * Example : "0,8" + * 1 to 8 players -> 2 eliminations per lap + * 9 or more players -> 3 eliminations per lap + * + * Example : "" + * 1 or more players -> 1 elimination per lap + */ + + +#Struct K_LapState { + Integer NbAliveAfter; + Integer NbEliminations; + Integer NbLapFinishers; +} + + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Constants +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Const C_ModeName "Laps" +//L16N [Laps] Description of the mode rules +#Const Description _("$zIn $<$t$6F9Laps$> mode, the goal is to drive as far as possible by passing $<$t$6F9checkpoints$>.\n\nThe laps mode takes place on multilap (cyclical) maps, and is played in one go for every map.\n\nWhen the time is up, the $<$t$6F9winner$> is the player who passed the most $<$t$6F9checkpoints$>. In case of draws, the winner is the player who passed the last checkpoint first.") + +#Const C_HudModulePath "" //< Path to the hud module +#Const C_ManiaAppUrl "file://Media/ManiaApps/Nadeo/TMNext/TrackMania/Laps/Laps.Script.txt" //< Url of the mania app +#Const C_FakeUsersNb 0 + +#Const C_UploadRecord True +#Const C_DisplayRecordGhost False +#Const C_DisplayRecordMedal False +#Const C_CelebrateRecordGhost True +#Const C_CelebrateRecordMedal True + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Extends +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +***Match_LogVersions*** +*** +Log::RegisterScript(ScriptName, Version); +Log::RegisterScript(StateMgr::ScriptName, StateMgr::Version); +*** + +***Match_LoadLibraries*** +*** +StateMgr::Load(); +*** + +***Match_UnloadLibraries*** +*** +StateMgr::Unload(); +*** + +***Match_Settings*** +*** +MB_Settings_UseDefaultHud = (C_HudModulePath == ""); +MB_Settings_UseDefaultTimer = False; +*** + +***Match_Rules*** +*** +ModeInfo::SetName(C_ModeName); +ModeInfo::SetType(ModeInfo::C_Type_FreeForAll); +ModeInfo::SetRules(Description); +ModeInfo::SetStatusMessage(_("TYPE: Free for all\nOBJECTIVE: Set the best time on the track.")); +*** + +***Match_LoadHud*** +*** +if (C_HudModulePath != "") Hud_Load(C_HudModulePath); +*** + +***Match_AfterLoadHud*** +*** +ClientManiaAppUrl = C_ManiaAppUrl; +Race::SortScores(Race::C_Sort_BestRaceCheckpointsProgress); +UIModules_TimeGap::SetTimeGapMode(UIModules_TimeGap::C_TimeGapMode_BestRace); +UIModules_Checkpoint::SetRankMode(UIModules_Checkpoint::C_RankMode_BestRace); +UIModules_Checkpoint::SetVisibilityTimeDiff(False, True); +UIModules_PauseMenu_Online::SetHelp(Description); +UIModules_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_Laps); +UIModules_ScoresTable::SetHideSpectators(True); +*** + +***Match_Yield*** +*** +foreach (Event in PendingEvents) { + switch (Event.Type) { + // Initialize players when they join the server + case CSmModeEvent::EType::OnPlayerAdded: { + StateMgr::InitializePlayer(Event.Player); + CarRank::InitializePlayer(Event.Player); + } + } +} + +StateMgr::Yield(); +*** + +***Match_StartServer*** +*** +// Initialize mode +Clans::SetClansNb(0); +StateMgr::ForcePlayersStates([LibLaps_Constants::C_State_Waiting]); +WarmUp::SetAvailability(True); +Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_Normal); +Race::SetupRecord( + MenuConsts::C_ScopeType_Season, + MenuConsts::C_ScopeType_PersonalBest, + MenuConsts::C_GameMode_Laps, + "", + C_UploadRecord, + C_DisplayRecordGhost, + C_DisplayRecordMedal, + C_CelebrateRecordGhost, + C_CelebrateRecordMedal +); + +Race::UseAutomaticDossardColor(False); +*** + +***Match_InitMap*** +*** +declare Integer Last_NbLapsWithoutKO; +declare Integer Last_NumberOfPlayers; +declare Text Last_EliminatedPlayersNbRanks; +declare K_LapState[Integer] MatchState; + +Race::SetLapsSettings(True, -1); // force infinite lap +*** + +***Match_StartMap*** +*** +CarRank::Reset(); + +// Warm up +---Laps_Warmup--- + +StartTime = Now + Race::C_SpawnDuration; +if (S_DisableGiveUp) { + Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_NeverGiveUp); +} else { + Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_Normal); +} + +// Spawn players for the race +foreach (Score in Scores) { + declare Boolean Laps_CanSpawn for Score = True; + Laps_CanSpawn = True; +} +foreach (Player in Players) { + if (Player.Score != Null && Race::IsReadyToStart(Player)) { + declare Boolean Laps_CanSpawn for Player.Score = True; + Race::Start(Player, StartTime); + Laps_CanSpawn = False; + } +} + +CarRank::Update(CarRank::C_SortCriteria_BestRace); +StateMgr::ForcePlayersStates([LibLaps_Constants::C_State_Playing]); +*** + +***Laps_Warmup*** +*** +Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_Normal); +MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); +*** + +***Match_StartRound*** +*** +// Once the round started, no one can join to play +Last_NumberOfPlayers = Players.count; + +Last_NbLapsWithoutKO = S_NbLapsWithoutKO; +Last_EliminatedPlayersNbRanks = S_EliminatedPlayersNbRanks; + +MatchState = ComputeMatchState(Last_NumberOfPlayers); + +// Update UI lap number +foreach (LapNb => State in MatchState.sortkeyreverse()) { + Race::SetLapsSettings(False, LapNb); // Set number of laps with number of + break; +} + +// Reset Dossard Color +foreach (Player in Players) { + Player.Dossard_Color = <1., 1., 1.>; +} +*** + +***Match_PlayLoop*** +*** +// Manage race events +declare RacePendingEvents = Race::GetPendingEvents(); +foreach (Event in RacePendingEvents) { + Race::ValidEvent(Event); + + // Waypoint + if (Event.Type == Events::C_Type_Waypoint) { + if (Event.Player != Null) { + if (Event.IsEndLap) { + Scores::UpdatePlayerBestLapIfBetter(Event.Player); + + if (MatchState.existskey(Event.Player.CurrentLapNumber)) { + MatchState[Event.Player.CurrentLapNumber].NbLapFinishers += 1; + + // Proceed kick + if (MatchState[Event.Player.CurrentLapNumber].NbLapFinishers == MatchState[Event.Player.CurrentLapNumber].NbAliveAfter) { + foreach (Player in Players) { + if (Player.SpawnStatus != CSmPlayer::ESpawnStatus::Spawned) continue; + + if (Player.CurrentLapNumber < Event.Player.CurrentLapNumber) { + EliminatePlayer(Player); + } + } + + foreach (LapNb => State in MatchState) { + if (LapNb > Event.Player.CurrentLapNumber) { + break; + } + MatchState.removekey(LapNb); + } + } + } + } + + if (Event.IsEndRace) { + MB_StopMatch(); + } + + // Update best race at each checkpoint to sort scores with C_Sort_BestRaceCheckpointsProgress + Scores::UpdatePlayerBestRace(Event.Player); + + CarRank::ThrottleUpdate(CarRank::C_SortCriteria_BestRace); + } + UpdateDossardColors(MatchState); + } +} + +// Manage mode events +foreach (Event in PendingEvents) { + if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue; + Events::Invalid(Event); +} + +if (Last_NbLapsWithoutKO != S_NbLapsWithoutKO || Last_EliminatedPlayersNbRanks != S_EliminatedPlayersNbRanks) { + Last_NbLapsWithoutKO = S_NbLapsWithoutKO; + Last_EliminatedPlayersNbRanks = S_EliminatedPlayersNbRanks; + + MatchState = ComputeMatchState(Last_NumberOfPlayers); + + foreach (LapNb => State in MatchState.sortkeyreverse()) { + Race::SetLapsSettings(False, LapNb); + break; + } +} + +if (Players.count > 0 && PlayersNbAlive <= 0) { + MB_StopMatch(); +} +*** + +***Match_EndMap*** +*** +// Ensure that we stop the match (after a vote for the next map, ...) +MB_StopMatch(); + +EndTime = -1; +StateMgr::ForcePlayersStates([LibLaps_Constants::C_State_Waiting]); + +CarRank::Update(CarRank::C_SortCriteria_BestRace); +Race::SortScores(Race::C_Sort_BestRaceCheckpointsProgress); +Scores::SetPlayerWinner(Scores::GetBestPlayer(Scores::C_Sort_BestRaceCheckpointsProgress)); +Race::StopSkipOutroAll(); +*** + +/* + * + * Functions + * +*/ + +/** Compute Match State based on S_EliminatedPlayersNbRanks and number of Players + * + * @return K_LapState[Integer] + */ +K_LapState[Integer] ComputeMatchState(Integer _NumberOfPlayers) { + declare K_LapState[Integer] MatchState; + + declare Integer[] Eliminations = [0]; + foreach (EliminationText in TL::Split(",", S_EliminatedPlayersNbRanks)) { + declare Integer Elimination = TL::ToInteger(EliminationText); + if (Elimination > 0) { // -1 if error + Eliminations.add(Elimination); + } + } + + declare Integer KickedPlayers = 0; + declare Integer LapNumber = S_NbLapsWithoutKO + 1; + + while (KickedPlayers <= _NumberOfPlayers) { + while (_NumberOfPlayers - KickedPlayers < Eliminations[Eliminations.count - 1]) { + Eliminations.removekey(Eliminations.count - 1); + } + + if (_NumberOfPlayers - KickedPlayers - Eliminations.count < Eliminations[Eliminations.count - 1]) { + Eliminations.removekey(Eliminations.count - 1); + } + + declare Integer Alive = _NumberOfPlayers - KickedPlayers - Eliminations.count; + + if (Alive <= 0) break; + + MatchState[LapNumber] = K_LapState { + NbAliveAfter = Alive, + NbEliminations = Eliminations.count + }; + + KickedPlayers += Eliminations.count; + LapNumber += 1; + if (LapNumber > 255) break; // Anti crash + } + + return MatchState; +} + +/** Update Dossard Color of Players depending of the Lap and the Rank + * + * @return Void + */ +Void UpdateDossardColors(K_LapState[Integer] _MatchState) { + declare Integer Rank = 1; + + foreach (Score in Scores) { + if (Score == Null) continue; + if (Score.User == Null) continue; + + declare CSmPlayer Player = GetPlayer(Score.User.Login); + if (Player == Null) continue; + + declare Integer NbAlive; + + if (_MatchState.existskey(Player.CurrentLapNumber + 1)) { + NbAlive = _MatchState[Player.CurrentLapNumber + 1].NbAliveAfter; + } else { + // get first lap + foreach (Value in _MatchState) { + NbAlive = Value.NbAliveAfter; + break; + } + } + + if (Rank > NbAlive) { + Player.Dossard_Color = <1., 0., 0.>; + } else { + Player.Dossard_Color = <1., 1., 1.>; + } + + Rank += 1; + } +} + +/** Eliminate Player and send a message in a Chat + * + * @return Void + */ +Void EliminatePlayer(CSmPlayer _Player) { + if (_Player == Null) return; + if (_Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned) return; + Race::StopSkipOutro(_Player); + UIManager.UIAll.SendChat("Player $<$ff6" ^ _Player.User.Name ^ "$> is $<$f00eliminated$>"); +} \ No newline at end of file