diff --git a/TM_CPKnockout_V2.Script.txt b/TM_CPKnockout_V2.Script.txt new file mode 100644 index 0000000..a7ce4c2 --- /dev/null +++ b/TM_CPKnockout_V2.Script.txt @@ -0,0 +1,476 @@ +/** + * CP Knockout V2 mode + * Similar to TM_CPKnockout.Script.txt but allow multiple rounds, and can finish before the finish if not enough players + */ + +// #RequireContext CSmMode +#Extends "Modes/Nadeo/Trackmania/Base/TrackmaniaRoundsBase.Script.txt" + +#Const CompatibleMapTypes "TrackMania\\TM_Race,TM_Race" +#Const Version "2023-11-27" +#Const ScriptName "Modes/TM2020-Gamemodes/TM_CPKnockout.Script.txt" + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Libraries +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Include "TextLib" as TL +#Include "MathLib" as ML +#Include "Libs/Nadeo/Trackmania/MainMenu/Constants.Script.txt" as MenuConsts +#Include "Libs/Nadeo/Trackmania/Modes/Rounds/StateManager.Script.txt" as StateMgr +#Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/ScoresTable_Server.Script.txt" as UIModules_ScoresTable +#Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/TimeGap_Server.Script.txt" as UIModules_TimeGap +#Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/Checkpoint_Server.Script.txt" as UIModules_Checkpoint +#Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/PauseMenuOnline_Server.Script.txt" as UIModules_PauseMenu_Online + +#Include "Libs/Nadeo/TMGame/Modes/Base/UIModules/BigMessage_Server.Script.txt" as UIModules_BigMessage + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Settings +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Setting S_DisableGiveUp True as _("Disable give up") +#Setting S_NumberOfFinishers 8 +#Setting S_FinishTimeout -1 as _("Finish timeout") +#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_EliminatedPlayersNbRanks "2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,2" as _("Nb of players above which one extra elim. /CP. Same setting of Knock") + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// Constants +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +#Const C_ModeName "CP Knockout" +//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/Trackmania/Modes/Laps.Script.txt" //< Url of the mania app + +#Const C_UploadRecord True +#Const C_DisplayRecordGhost False +#Const C_DisplayRecordMedal False +#Const C_CelebrateRecordGhost True +#Const C_CelebrateRecordMedal True + +#Struct K_State { + Integer NbAliveAfter; + Integer NbFinishers; +} + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // +// 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_TotalPoints); +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); +Scores::SaveInScore(Scores::C_Points_Match); +UIModules_ScoresTable::SetScoreMode(UIModules_ScoresTable::C_Mode_PrevTime); +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([StateMgr::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_InitMatch*** +*** +foreach (Score in Scores) { + declare Boolean IsAlive for Score = False; + IsAlive = False; +} +UIModules_ScoresTable::SetCustomPoints([]); +UIModules_ScoresTable::SetCustomTimes([]); + +declare Boolean Match_InitPlayers = True; +declare Integer Match_PlayersEliminated; +declare Integer Match_Players; +*** + +***Match_StartMap*** +*** +CarRank::Reset(); + +if (S_WarmUpNb > 0) { + foreach (Score in Scores) { + WarmUp::CanPlay(Score, True); + } + + MB_WarmUp(S_WarmUpNb, S_WarmUpDuration * 1000, S_WarmUpTimeout * 1000); +} + +if (Match_InitPlayers) { + Match_InitPlayers = False; + + foreach (Score in Scores) { + declare Boolean IsAlive for Score = False; + IsAlive = True; + Match_Players += 1; + + if (Score.User == Null) continue; + } +} + +StartTime = Now + Race::C_SpawnDuration; +if (S_DisableGiveUp) { + Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_NeverGiveUp); +} else { + Race::SetRespawnBehaviour(Race::C_RespawnBehaviour_Normal); +} +*** + +***Match_InitRound*** +*** +declare Integer Round_NumberOfPlayers = 0; +declare Integer Round_DossardsUpdateCooldown = 0; + +declare Text Round_EliminatedPlayersNbRanks = ""; +declare K_State[Integer] Round_State; + +declare Ident[] Round_EliminatedScores; +*** + +***Match_StartRound*** +*** +Round_EliminatedPlayersNbRanks = S_EliminatedPlayersNbRanks; + +declare Integer[Text] CustomTimes = []; + +foreach (Player in Players) { + Player.Dossard_Color = <1., 1., 1.>; + if (Player.Score == Null || Player.User == Null) continue; + declare Boolean IsAlive for Player.Score = False; + if (IsAlive) { + Scores::SetPlayerMatchPoints(Player.Score, Match_Players); + CustomTimes[Player.User.WebServicesUserId] = 0; + Round_NumberOfPlayers += 1; + } +} +UIModules_ScoresTable::SetCustomTimes(CustomTimes); + +Round_State = ComputeState(Round_NumberOfPlayers); +log("Round_State: " ^ Round_State); + +EndTime = -1; +*** + +***Match_PlayLoop*** +*** +// Manage race events +foreach (Event in Race::GetPendingEvents()) { + Race::ValidEvent(Event); + + // Waypoint + if (Event.Type == Events::C_Type_Waypoint) { + if (Event.Player != Null) { + declare Integer NBOfCP = Event.Player.RaceWaypointTimes.count; + + if (Round_State.existskey(NBOfCP)) { + Round_State[NBOfCP].NbFinishers += 1; + + // Proceed kick + if (Round_State[NBOfCP].NbFinishers >= Round_State[NBOfCP].NbAliveAfter) { + foreach (Player in Players) { + if (Player.SpawnStatus != CSmPlayer::ESpawnStatus::Spawned) continue; + + if (Player.RaceWaypointTimes.count < NBOfCP) { + EliminatePlayer(Player, Match_PlayersEliminated); + Match_PlayersEliminated += 1; + Round_EliminatedScores.add(Player.Score.Id); + } + } + + foreach (CPNb => State in Round_State) { + if (CPNb > Event.Player.RaceWaypointTimes.count) { + break; + } + } + } + } + + if (Event.IsEndRace) { + Scores::UpdatePlayerPrevRace(Event.Player); + + declare Integer[Text] CustomTimes = UIModules_ScoresTable::GetCustomTimes(); + CustomTimes.removekey(Event.Player.User.WebServicesUserId); + UIModules_ScoresTable::SetCustomTimes(CustomTimes); + + if (EndTime <= 0) { + EndTime = Race::GetFinishTimeout(S_FinishTimeout, Race::GetLapsNb(), Map); + } + } + + // Update best race at each checkpoint to sort scores with C_Sort_BestRaceCheckpointsProgress + Scores::UpdatePlayerBestRace(Event.Player); + + CarRank::ThrottleUpdate(CarRank::C_SortCriteria_CurrentRace); + + if (Round_DossardsUpdateCooldown == 0) { + UpdateDossardColors(Round_State); + Round_DossardsUpdateCooldown = Now + 1000; + } + } + } else if (Event.Type == Events::C_Type_SkipOutro) { + if (Event.Player != Null) { + Race::StopSkipOutro(Event.Player); + } + } +} + +// Manage mode events +foreach (Event in PendingEvents) { + if (Event.HasBeenPassed || Event.HasBeenDiscarded) continue; + Events::Invalid(Event); +} + +if (Round_DossardsUpdateCooldown > 0 && Round_DossardsUpdateCooldown < Now) { + Round_DossardsUpdateCooldown = 0; + UpdateDossardColors(Round_State); +} + +if (Round_EliminatedPlayersNbRanks != S_EliminatedPlayersNbRanks) { + Round_EliminatedPlayersNbRanks = S_EliminatedPlayersNbRanks; + + Round_State = ComputeState(Round_NumberOfPlayers); +} + +if (Players.count > 0 && Round_NumberOfPlayers > 0 && PlayersNbAlive <= S_NumberOfFinishers) { + MB_StopMatch(); +} +*** + +***Match_EndRound*** +*** +Race::StopSkipOutroAll(); +EndTime = -1; +StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); +CarRank::Update(CarRank::C_SortCriteria_CurrentRace); + +if (Round_ForceEndRound || Round_SkipPauseRound) { + declare Text[][Text] CustomPoints = UIModules_ScoresTable::GetCustomPoints(); + foreach (Score in Scores) { + if (!Round_EliminatedScores.exists(Score.Id)) continue; + + declare Boolean IsAlive for Score = False; + IsAlive = True; + + if (Score.User == Null || !CustomPoints.existskey(Score.User.WebServicesUserId)) continue; + CustomPoints.removekey(Score.User.WebServicesUserId); + } + UIModules_ScoresTable::SetCustomPoints(CustomPoints); + + // Cancel points + foreach (Score in Scores) { + Scores::SetPlayerRoundPoints(Score, 0); + } + // Do not launch the forced end round sequence after a pause + if (!Round_SkipPauseRound) { + ForcedEndRoundSequence(); + } + MB_SetValidRound(False); +} else { + UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; + UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound; + MB_Sleep(S_ChatTime / 2); + UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal; + UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing; + MB_Sleep(S_ChatTime / 2); +} +*** + +***Match_EndMap*** +*** +// Ensure that we stop the match (after a vote for the next map, ...) +MB_StopMatch(); + +EndTime = -1; +StateMgr::ForcePlayersStates([StateMgr::C_State_Waiting]); + +CarRank::Update(CarRank::C_SortCriteria_CurrentRace); +Race::SortScores(Race::C_Sort_TotalPoints); +Scores::SetPlayerWinner(Scores::GetBestPlayer(Scores::C_Sort_MatchPoints)); +Race::StopSkipOutroAll(); +*** + +***Rounds_CanSpawn*** +*** +foreach (Score in Scores) { + declare Boolean ModeRounds_CanSpawn for Score = True; + ModeRounds_CanSpawn = CanSpawn(Score); +} +*** + +***Rounds_CheckCanSpawn*** +*** +// During PlayLoop Check +declare Boolean IsAlive for _Player.Score = False; +return IsAlive; +---PouleParty_Rounds_CanSpawn--- +*** + +Boolean CanSpawn(CSmScore _Score) { + // Before PlayLoop Check + declare Boolean IsAlive for _Score = False; + return IsAlive; +} + +/* + * + * Functions + * +*/ + +/** Compute Match State based on S_EliminatedPlayersNbRanks and number of Players + * + * @return K_State[Integer] + */ +K_State[Integer] ComputeState(Integer _NbOfPlayer) { + declare K_State[Integer] State; + + declare Text[] KORepartition = TL::Split(",", S_EliminatedPlayersNbRanks); + + declare Integer NBOfCP = Map::GetCheckpointsCount() + 1; + declare Integer NbAliveAfter = _NbOfPlayer; + + for (I, 0 , NBOfCP) { + declare Integer CPIndex = NBOfCP - I; + if (KORepartition.existskey(CPIndex) && KORepartition[CPIndex] != "0") { + NbAliveAfter -= TL::ToInteger(KORepartition[CPIndex]); + State[I] = K_State{ + NbAliveAfter = NbAliveAfter + }; + } + } + + return State; +} + +/** Update Dossard Color of Players depending of the CP and the Rank + * + * @return Void + */ +Void UpdateDossardColors(K_State[Integer] _Round_State) { + Log::Log("UpdateDossardColors"); + declare Integer Rank = 1; + + foreach (Score in Scores) { + if (Score.User == Null) continue; + + declare CSmPlayer Player = GetPlayer(Score.User.Login); + if (Player == Null) continue; + if (Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned) continue; + + declare Integer NbAlive; + + if (_Round_State.existskey(Player.CurrentLapNumber + 1)) { + NbAlive = _Round_State[Player.CurrentLapNumber + 1].NbAliveAfter; + } else { + // get first CP + foreach (Value in _Round_State) { + 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, Integer _Eliminated) { + if (_Player == Null || _Player.Score == Null) return; + if (_Player.SpawnStatus == CSmPlayer::ESpawnStatus::NotSpawned) return; + log("EliminatePlayer: " ^ _Player.User.Name ^ " (" ^ _Player.User.Login ^ ")"); + Race::StopSkipOutro(_Player); + UIManager.UIAll.SendChat("Player $<$ff6" ^ _Player.User.Name ^ "$> is $<$f00eliminated$>"); + + declare Text[][Text] CustomPoints = UIModules_ScoresTable::GetCustomPoints(); + CustomPoints[_Player.User.WebServicesUserId] = [_("|Status|K.O."), "f00"]; + UIModules_ScoresTable::SetCustomPoints(CustomPoints); + + Scores::SetPlayerMatchPoints(_Player.Score, _Eliminated); + + declare Boolean IsAlive for _Player.Score = False; + IsAlive = False; +} \ No newline at end of file