diff --git a/src/Interface/ModalDialogs/Modes/RMSTHelpModalDialog.as b/src/Interface/ModalDialogs/Modes/RMSTHelpModalDialog.as new file mode 100644 index 00000000..90aa4751 --- /dev/null +++ b/src/Interface/ModalDialogs/Modes/RMSTHelpModalDialog.as @@ -0,0 +1,91 @@ +class RMSTHelpModalDialog : ModalDialog +{ + UI::Texture@ clubIdTex; + UI::Texture@ roomIdTex; + + RMSTHelpModalDialog() + { + super(Icons::InfoCircle + " \\$zRandom Map Survival Together###RMSTHelp"); + m_size = vec2(Draw::GetWidth(), Draw::GetHeight()) * 0.6f; + @clubIdTex = UI::LoadTexture("src/Assets/Images/help_clubId.jpg"); + @roomIdTex = UI::LoadTexture("src/Assets/Images/help_roomId.jpg"); + } + + void RenderDialog() override + { + float scale = UI::GetScale(); + UI::BeginChild("Content", vec2(0, -32) * scale); + UI::PushFont(Fonts::Header); + UI::Text("Random Map Survival Together"); + UI::PopFont(); + UI::Markdown( + "Random Map Survival Together is a multiplayer survival mode where players work together to survive as long as possible.\n\n" + + "The game mode requires all players to join the server you have set up. Players have no plugins to install and is compatible with console players.\n\n" + ); + UI::NewLine(); + UI::Separator(); + UI::PushFont(Fonts::Header); + UI::Text("Prerequisites for a good functioning of the RMST mode"); + UI::PopFont(); + UI::Markdown( + "- You need to have a **Club** with at least **Admin** permissions\n" + + "- You need to have a **Room** in this club\n" + + "- You need to **join the room** before starting the mode\n" + + "- All players must **join the same room**\n\n" + ); + UI::Separator(); + UI::PushFont(Fonts::Header); + UI::Text("How RMST works"); + UI::PopFont(); + UI::Markdown( + "**Collaborative Survival:**\n" + + "- All players share the same timer and work as a team\n" + + "- When **any player** gets a goal medal, the **entire team** gets +3 minutes\n" + + "- When **any player** skips a map, the **entire team** loses -1 minute\n" + + "- The session ends when the shared timer reaches 0\n\n" + + "**Team Progress Tracking:**\n" + + "- Team achievements (total medals and skips) are shared\n" + + "- Individual contributions are tracked for recognition\n" + + "- MVP is determined by individual goals vs skips ratio\n" + + "- Real-time team progress updates\n\n" + ); + UI::Separator(); + UI::PushFont(Fonts::Header); + UI::Text("How to get Club ID and Room ID"); + UI::PopFont(); + UI::Markdown( + "**Club ID:**\n" + + "Go to your club page on the Trackmania website and look at the URL. The Club ID is the number at the end.\n\n" + ); + if (clubIdTex !is null) { + UI::Image(clubIdTex, vec2(clubIdTex.GetSize().x, clubIdTex.GetSize().y) * 0.5f * scale); + } + UI::NewLine(); + UI::Markdown( + "**Room ID:**\n" + + "Go to your room page in the club and look at the URL. The Room ID is the number at the end.\n\n" + ); + if (roomIdTex !is null) { + UI::Image(roomIdTex, vec2(roomIdTex.GetSize().x, roomIdTex.GetSize().y) * 0.5f * scale); + } + UI::NewLine(); + UI::Separator(); + UI::PushFont(Fonts::Header); + UI::Text("Tips for a successful RMST session"); + UI::PopFont(); + UI::Markdown( + "- **True teamwork:** Any team member getting a goal medal helps everyone!\n" + + "- **Coordinate skips:** Discuss as a team before anyone skips a difficult map\n" + + "- **Divide and conquer:** Players can focus on different types of maps they're good at\n" + + "- **Watch the shared timer:** Keep track of team time and plan accordingly\n" + + "- **Use voice chat:** Communication is crucial for team coordination\n" + + "- **Celebrate together:** Every goal medal is a team achievement!\n" + + "- **Support each other:** Help teammates learn difficult sections\n\n" + ); + UI::EndChild(); + + if (UI::Button("Close")) { + m_visible = false; + } + } +} \ No newline at end of file diff --git a/src/Interface/Views/RMCMenu.as b/src/Interface/Views/RMCMenu.as index 0572548f..b737e630 100644 --- a/src/Interface/Views/RMCMenu.as +++ b/src/Interface/Views/RMCMenu.as @@ -166,6 +166,16 @@ namespace RMC selectedGameMode = GameMode::Together; startnew(CoroutineFunc(Together.StartRMT)); } + if (RMT_isServerOK && !TM::IsInServer()) { + UI::BeginDisabled(); + UI::GreyButton(Icons::Heart + " Start Random Map Survival Together"); + UI::Text("\\$a50" + Icons::ExclamationTriangle + " \\$zPlease join the room before continuing"); + UI::EndDisabled(); + } + if (RMT_isServerOK && TM::IsInServer() && UI::GreenButton(Icons::Heart + " Start Random Map Survival Together")){ + selectedGameMode = GameMode::SurvivalTogether; + startnew(CoroutineFunc(SurvivalTogether.StartRMST)); + } #endif UI::TreePop(); } @@ -227,6 +237,12 @@ namespace RMC Together.RenderBelowGoalMedal(); Together.RenderScores(); } + else if (selectedGameMode == GameMode::SurvivalTogether) { + SurvivalTogether.RenderGoalMedal(); + UI::HPadding(25); + SurvivalTogether.RenderBelowGoalMedal(); + SurvivalTogether.RenderScores(); + } #endif } } @@ -237,6 +253,7 @@ namespace RMC else if (selectedGameMode == GameMode::Survival || selectedGameMode == GameMode::SurvivalChaos) Survival.Render(); else if (selectedGameMode == GameMode::Objective) Objective.Render(); else if (selectedGameMode == GameMode::Together) Together.Render(); + else if (selectedGameMode == GameMode::SurvivalTogether) SurvivalTogether.Render(); } void RenderBaseInfos() @@ -305,4 +322,4 @@ namespace RMC MXNadeoServicesGlobal::isCheckingRoom = false; } #endif -} \ No newline at end of file +} diff --git a/src/Utils/DataManager.as b/src/Utils/DataManager.as index 77f7a6d1..d516de29 100644 --- a/src/Utils/DataManager.as +++ b/src/Utils/DataManager.as @@ -56,6 +56,12 @@ namespace DataManager void CreateSaveFile() { string lastLetter = tostring(RMC::selectedGameMode).SubStr(0,1); string gameMode = "RM" + lastLetter; + + // Handle SurvivalTogether mode separately to avoid conflict with Survival + if (RMC::selectedGameMode == RMC::GameMode::SurvivalTogether) { + gameMode = "RMST"; + } + Json::Value SaveFileData = Json::Object(); SaveFileData["PBOnMap"] = -1; SaveFileData["TimerRemaining"] = 0; @@ -74,6 +80,12 @@ namespace DataManager void RemoveCurrentSaveFile() { string lastLetter = tostring(RMC::selectedGameMode).SubStr(0,1); string gameMode = "RM" + lastLetter; + + // Handle SurvivalTogether mode separately to avoid conflict with Survival + if (RMC::selectedGameMode == RMC::GameMode::SurvivalTogether) { + gameMode = "RMST"; + } + IO::Delete(SAVE_DATA_LOCATION + gameMode + ".json"); RMC::CurrentRunData = Json::Object(); } @@ -81,6 +93,12 @@ namespace DataManager void SaveCurrentRunData() { string lastLetter = tostring(RMC::selectedGameMode).SubStr(0,1); string gameMode = "RM" + lastLetter; + + // Handle SurvivalTogether mode separately to avoid conflict with Survival + if (RMC::selectedGameMode == RMC::GameMode::SurvivalTogether) { + gameMode = "RMST"; + } + Json::ToFile(SAVE_DATA_LOCATION + gameMode + ".json", RMC::CurrentRunData); } @@ -126,8 +144,15 @@ namespace DataManager bool LoadRunData() { string lastLetter = tostring(RMC::selectedGameMode).SubStr(0,1); string gameMode = "RM" + lastLetter; - if (IO::FileExists(SAVE_DATA_LOCATION + gameMode + ".json")) { - RMC::CurrentRunData = Json::FromFile(SAVE_DATA_LOCATION + gameMode + ".json"); + + // Handle SurvivalTogether mode separately to avoid conflict with Survival + if (RMC::selectedGameMode == RMC::GameMode::SurvivalTogether) { + gameMode = "RMST"; + } + + string saveFilePath = SAVE_DATA_LOCATION + gameMode + ".json"; + if (IO::FileExists(saveFilePath)) { + RMC::CurrentRunData = Json::FromFile(saveFilePath); if (!EnsureSaveDataIsLoadable(gameMode, RMC::CurrentRunData)) { Log::Error("Deleting the current" + gameMode + " save file, as it is corrupted!"); Log::Error("Please create an issue on github if this repeatedly happens"); diff --git a/src/Utils/MX/Methods/LoadRandomMap.as b/src/Utils/MX/Methods/LoadRandomMap.as index ac4cbbba..9e1e6a5e 100644 --- a/src/Utils/MX/Methods/LoadRandomMap.as +++ b/src/Utils/MX/Methods/LoadRandomMap.as @@ -149,6 +149,13 @@ namespace MX return; } + if (RMC::selectedGameMode == RMC::GameMode::SurvivalTogether && map.ServerSizeExceeded) { + Log::Warn("Map is too big to play in servers, retrying..."); + sleep(1000); + PreloadRandomMap(); + return; + } + // Check if map is uploaded to Nadeo Services (if goal == WorldRecord) if (PluginSettings::RMC_GoalMedal == RMC::Medals[4]) { if (map.OnlineMapId == "" && !MXNadeoServicesGlobal::CheckIfMapExistsAsync(map.MapUid)) { diff --git a/src/Utils/RMC/GlobalProperties.as b/src/Utils/RMC/GlobalProperties.as index 11abcd23..ed0ad868 100644 --- a/src/Utils/RMC/GlobalProperties.as +++ b/src/Utils/RMC/GlobalProperties.as @@ -43,6 +43,7 @@ namespace RMC RMS@ Survival; RMObjective@ Objective; RMT@ Together; + RMST@ SurvivalTogether; enum GameMode { @@ -51,7 +52,8 @@ namespace RMC ChallengeChaos, SurvivalChaos, Objective, - Together + Together, + SurvivalTogether } GameMode selectedGameMode; @@ -72,6 +74,7 @@ namespace RMC @Survival = RMS(); @Objective = RMObjective(); @Together = RMT(); + @SurvivalTogether = RMST(); } string FormatTimer(int time) { diff --git a/src/Utils/RMC/RMST.as b/src/Utils/RMC/RMST.as new file mode 100644 index 00000000..bd73ff9b --- /dev/null +++ b/src/Utils/RMC/RMST.as @@ -0,0 +1,585 @@ +class RMST : RMS +{ +#if TMNEXT + string LobbyMapUID = ""; + NadeoServices::ClubRoom@ RMSTRoom; + MX::MapInfo@ currentMap; + MX::MapInfo@ nextMap; + array m_mapPersonalBests; + array m_playerScores; + bool m_CurrentlyLoadingRecords = false; + PBTime@ playerGotGoalActualMap; + uint RMSTTimerMapChange = 0; + bool isSwitchingMap = false; + bool pressedStopButton = false; + bool isFetchingNextMap = false; + array seenMaps; + + string GetModeName() override { return "Random Map Survival Together";} + + void DevButtons() override {} + + void Render() override + { + if (UI::IsOverlayShown() || (!UI::IsOverlayShown() && PluginSettings::RMC_AlwaysShowBtns)) { + if (UI::RedButton(Icons::Times + " Stop RMST")) + { + pressedStopButton = true; + RMC::IsRunning = false; + RMC::ShowTimer = false; + RMC::StartTime = -1; + RMC::EndTime = -1; + @nextMap = null; + @MX::preloadedMap = null; +#if DEPENDENCY_BETTERCHAT + BetterChat::SendChatMessage(Icons::Users + " Random Map Survival Together stopped"); + startnew(CoroutineFunc(BetterChatSendLeaderboard)); +#endif + startnew(CoroutineFunc(ResetToLobbyMap)); + } + + RenderCustomSearchWarning(); + UI::Separator(); + } + + RenderTimer(); + if (IS_DEV_MODE) UI::Text(RMC::FormatTimer(RMC::StartTime - SurvivedTimeStart)); + UI::Separator(); + RenderGoalMedal(); + UI::HPadding(25); + RenderBelowGoalMedal(); + RenderMVPPlayer(); + + if (PluginSettings::RMC_DisplayCurrentMap) + { + RenderCurrentMap(); + } + + if (RMC::IsRunning && (UI::IsOverlayShown() || (!UI::IsOverlayShown() && PluginSettings::RMC_AlwaysShowBtns))) { + UI::Separator(); + RenderPlayingButtons(); + UI::Separator(); + DrawPlayerProgress(); + } + } + + void RenderTimer() override + { + UI::PushFont(Fonts::TimerFont); + if (RMC::IsRunning || RMC::EndTime > 0 || RMC::StartTime > 0) { + if (RMC::IsPaused) UI::TextDisabled(RMC::FormatTimer(RMC::EndTime - RMC::StartTime)); + else UI::Text(RMC::FormatTimer(RMC::EndTime - RMC::StartTime)); + + SurvivedTime = RMC::StartTime - SurvivedTimeStart; + if (SurvivedTime > 0 && PluginSettings::RMC_SurvivalShowSurvivedTime) { + UI::PopFont(); + UI::Dummy(vec2(0, 8)); + UI::PushFont(Fonts::HeaderSub); + UI::Text(RMC::FormatTimer(SurvivedTime)); + UI::SetPreviousTooltip("Total time survived"); + } else { + UI::Dummy(vec2(0, 8)); + } + if (PluginSettings::RMC_DisplayMapTimeSpent) { + if(SurvivedTime > 0 && PluginSettings::RMC_SurvivalShowSurvivedTime) { + UI::SameLine(); + } + UI::PushFont(Fonts::HeaderSub); + UI::Text(Icons::Map + " " + RMC::FormatTimer(RMC::TimeSpentMap)); + UI::SetPreviousTooltip("Time spent on this map"); + UI::PopFont(); + } + } else { + UI::TextDisabled(RMC::FormatTimer(TimeLimit())); + UI::Dummy(vec2(0, 8)); + } + + UI::PopFont(); + } + + void StartRMST() + { + m_mapPersonalBests = {}; + m_playerScores = {}; + if (!seenMaps.IsEmpty()) seenMaps.RemoveRange(0, seenMaps.Length); + RMC::GoalMedalCount = 0; + Skips = 0; + RMC::ShowTimer = true; + RMC::ClickedOnSkip = false; + pressedStopButton = false; + Log::Trace("RMST: Getting lobby map UID from the room..."); + MXNadeoServicesGlobal::CheckNadeoRoomAsync(); + yield(); + @RMSTRoom = MXNadeoServicesGlobal::foundRoom; + LobbyMapUID = RMSTRoom.room.currentMapUid; + Log::Trace("RMST: Lobby map UID: " + LobbyMapUID); +#if DEPENDENCY_BETTERCHAT + BetterChat::SendChatMessage(Icons::Users + " Starting Random Map Survival Together. Have Fun!"); + sleep(200); + BetterChat::SendChatMessage(Icons::Users + " Goal medal: " + tostring(PluginSettings::RMC_GoalMedal)); +#endif + SetupMapStart(); + } + + void SetupMapStart() { + RMC::IsStarting = true; + isSwitchingMap = true; + // Fetch a map + Log::Trace("RMST: Fetching a random map..."); + Json::Value res; + try { + res = API::GetAsync(MX::CreateQueryURL())["Results"][0]; + } catch { + Log::Error("ManiaExchange API returned an error, retrying...", true); + SetupMapStart(); + return; + } + @currentMap = MX::MapInfo(res); + Log::Trace("RMST: Random map: " + currentMap.Name + " (" + currentMap.MapId + ")"); + seenMaps.InsertLast(currentMap.MapUid); + UI::ShowNotification(Icons::InfoCircle + " RMST - Information on map switching", "Nadeo prevent sometimes when switching map too often and will not change map.\nIf after 10 seconds the podium screen is not shown, you can start a vote to change to next map in the game pause menu.", Text::ParseHexColor("#420399")); + + if (!MXNadeoServicesGlobal::CheckIfMapExistsAsync(currentMap.MapUid)) { + Log::Trace("RMST: Map is not on NadeoServices, retrying..."); + SetupMapStart(); + return; + } + + DataManager::SaveMapToRecentlyPlayed(currentMap); + MXNadeoServicesGlobal::ClubRoomSetMapAndSwitchAsync(RMSTRoom, currentMap.MapUid); + while (!TM::IsMapCorrect(currentMap.MapUid)) sleep(1000); + MXNadeoServicesGlobal::ClubRoomSetCountdownTimer(RMSTRoom, TimeLimit() / 1000); + while (GetApp().CurrentPlayground is null) yield(); + CGamePlayground@ GamePlayground = cast(GetApp().CurrentPlayground); + while (GamePlayground.GameTerminals.Length < 0) yield(); + while (GamePlayground.GameTerminals[0] is null) yield(); + while (GamePlayground.GameTerminals[0].ControlledPlayer is null) yield(); + CSmPlayer@ player = cast(GamePlayground.GameTerminals[0].ControlledPlayer); + while (player.ScriptAPI is null) yield(); + CSmScriptPlayer@ playerScriptAPI = cast(player.ScriptAPI); + while (playerScriptAPI.Post == 0) yield(); + RMC::StartTime = Time::Now; + RMC::EndTime = RMC::StartTime + TimeLimit(); + SurvivedTimeStart = Time::Now; + RMC::IsPaused = false; + RMC::GotGoalMedalOnCurrentMap = false; + RMC::IsRunning = true; + startnew(CoroutineFunc(TimerYield)); + startnew(CoroutineFunc(UpdateRecordsLoop)); + RMC::TimeSpawnedMap = Time::Now; + isSwitchingMap = false; + RMC::IsStarting = false; + startnew(CoroutineFunc(RMSTFetchNextMap)); + } + + void RMSTFetchNextMap() { + isFetchingNextMap = true; + // Fetch a map + Log::Trace("RMST: Fetching a random map..."); + Json::Value res; + try { + res = API::GetAsync(MX::CreateQueryURL())["Results"][0]; + } catch { + Log::Error("ManiaExchange API returned an error, retrying..."); + RMSTFetchNextMap(); + return; + } + @nextMap = MX::MapInfo(res); + Log::Trace("RMST: Next Random map: " + nextMap.Name + " (" + nextMap.MapId + ")"); + + if (PluginSettings::SkipSeenMaps) { + if (seenMaps.Find(nextMap.MapUid) != -1) { + Log::Trace("Map has been played already, retrying..."); + RMSTFetchNextMap(); + return; + } + + seenMaps.InsertLast(nextMap.MapUid); + } + + if (!MXNadeoServicesGlobal::CheckIfMapExistsAsync(nextMap.MapUid)) { + Log::Trace("RMST: Next map is not on NadeoServices, retrying..."); + @nextMap = null; + RMSTFetchNextMap(); + return; + } + + isFetchingNextMap = false; + } + + void RMSTSwitchMap() { + m_playerScores.SortDesc(); + isSwitchingMap = true; + m_mapPersonalBests = {}; + RMSTTimerMapChange = RMC::EndTime - RMC::StartTime; + RMC::IsPaused = true; + RMC::GotGoalMedalOnCurrentMap = false; + if (nextMap is null && !isFetchingNextMap) RMSTFetchNextMap(); + while (isFetchingNextMap) yield(); + @currentMap = nextMap; + @nextMap = null; + Log::Trace("RMST: Random map: " + currentMap.Name + " (" + currentMap.MapId + ")"); + UI::ShowNotification(Icons::InfoCircle + " RMST - Information on map switching", "Nadeo prevent sometimes when switching map too often and will not change map.\nIf after 10 seconds the podium screen is not shown, you can start a vote to change to next map in the game pause menu.", Text::ParseHexColor("#420399")); + + DataManager::SaveMapToRecentlyPlayed(currentMap); + MXNadeoServicesGlobal::ClubRoomSetMapAndSwitchAsync(RMSTRoom, currentMap.MapUid); + while (!TM::IsMapCorrect(currentMap.MapUid)) sleep(1000); + MXNadeoServicesGlobal::ClubRoomSetCountdownTimer(RMSTRoom, RMSTTimerMapChange / 1000); + while (GetApp().CurrentPlayground is null) yield(); + CGamePlayground@ GamePlayground = cast(GetApp().CurrentPlayground); + while (GamePlayground.GameTerminals.Length < 0) yield(); + while (GamePlayground.GameTerminals[0] is null) yield(); + while (GamePlayground.GameTerminals[0].ControlledPlayer is null) yield(); + CSmPlayer@ player = cast(GamePlayground.GameTerminals[0].ControlledPlayer); + while (player.ScriptAPI is null) yield(); + m_playerScores.SortDesc(); +#if DEPENDENCY_BETTERCHAT + if (m_playerScores.Length > 0) { + RMSTPlayerScore@ p = m_playerScores[0]; + string currentStatChat = Icons::Users + " RMST Team Progress: " + tostring(RMC::GoalMedalCount) + " " + tostring(PluginSettings::RMC_GoalMedal) + " medals - " + Skips + " skips\n"; + currentStatChat += "Current MVP: " + p.name + ": " + p.goals + " " + tostring(PluginSettings::RMC_GoalMedal) + " - " + p.skips + " skips"; + BetterChat::SendChatMessage(currentStatChat); + } +#endif + CSmScriptPlayer@ playerScriptAPI = cast(player.ScriptAPI); + while (playerScriptAPI.Post == 0) yield(); + RMC::EndTime = RMC::EndTime + (Time::Now - RMC::StartTime); + RMC::TimeSpawnedMap = Time::Now; + RMC::IsPaused = false; + isSwitchingMap = false; + RMC::ClickedOnSkip = false; + startnew(CoroutineFunc(RMSTFetchNextMap)); + } + + void ResetToLobbyMap() { + if (LobbyMapUID != "") { + UI::ShowNotification("Returning to lobby map", "Please wait...", Text::ParseHexColor("#993f03")); +#if DEPENDENCY_BETTERCHAT + sleep(200); + BetterChat::SendChatMessage(Icons::Users + " Returning to lobby map..."); +#endif + MXNadeoServicesGlobal::SetMapToClubRoomAsync(RMSTRoom, LobbyMapUID); + if (pressedStopButton) MXNadeoServicesGlobal::ClubRoomSwitchMapAsync(RMSTRoom); + while (!TM::IsMapCorrect(LobbyMapUID)) sleep(1000); + pressedStopButton = false; + } + MXNadeoServicesGlobal::ClubRoomSetCountdownTimer(RMSTRoom, 0); + } + + void TimerYield() override + { + while (RMC::IsRunning){ + yield(); + if (RMC::IsPaused) { + RMC::StartTime = Time::Now - (Time::Now - RMC::StartTime); + RMC::EndTime = Time::Now - (Time::Now - RMC::EndTime); + } else { + CGameCtnChallenge@ currentMapChallenge = cast(GetApp().RootMap); + if (currentMapChallenge !is null) { + CGameCtnChallengeInfo@ currentMapInfo = currentMapChallenge.MapInfo; + if (currentMapInfo !is null) { + RMC::StartTime = Time::Now; + RMC::TimeSpentMap = Time::Now - RMC::TimeSpawnedMap; + SurvivedTime = RMC::StartTime - SurvivedTimeStart; + PendingTimerLoop(); + + if (!pressedStopButton && (RMC::StartTime > RMC::EndTime || !RMC::IsRunning || RMC::EndTime <= 0)) { + RMC::StartTime = -1; + RMC::EndTime = -1; + RMC::IsRunning = false; + RMC::ShowTimer = false; + GameEndNotification(); + @nextMap = null; + @MX::preloadedMap = null; + m_playerScores.SortDesc(); +#if DEPENDENCY_BETTERCHAT + BetterChat::SendChatMessage(Icons::Users + " Random Map Survival Together ended, thanks for playing!"); + sleep(200); + BetterChatSendLeaderboard(); +#endif + ResetToLobbyMap(); + } + } + } + } + } + } + + void PendingTimerLoop() override + { + // Cap timer max + if ((RMC::EndTime - RMC::StartTime) > (PluginSettings::RMC_SurvivalMaxTime-Skips)*60*1000) { + RMC::EndTime = RMC::StartTime + (PluginSettings::RMC_SurvivalMaxTime-Skips)*60*1000; + } + } + + void GotGoalMedalNotification() override + { + Log::Trace("RMST: Got the "+ tostring(PluginSettings::RMC_GoalMedal) + " medal!"); + // In survival mode, add 3 minutes when getting a goal medal + RMC::EndTime += (3*60*1000); + + // Find the player who got the medal for the notification + string playerName = "Someone"; + if (playerGotGoalActualMap !is null) { + playerName = playerGotGoalActualMap.name; + } + + UI::ShowNotification("\\$071" + Icons::Trophy + " Team Goal Medal!", playerName + " got the " + tostring(PluginSettings::RMC_GoalMedal) + " medal! +3 minutes added to team timer!"); + + if (PluginSettings::RMC_AutoSwitch) { + startnew(CoroutineFunc(RMSTSwitchMap)); + } + } + + void SkipButtons() override + { + UI::BeginDisabled(TM::IsPauseMenuDisplayed() || RMC::ClickedOnSkip); + if (UI::Button(Icons::PlayCircleO + " Skip")) { + if (RMC::IsPaused) RMC::IsPaused = false; + Skips += 1; + // In survival mode, lose 1 minute when skipping + RMC::EndTime -= (1*60*1000); + Log::Trace("RMST: Skipping map"); + UI::ShowNotification("Please wait...", "-1 minute for skip"); + startnew(CoroutineFunc(RMSTSwitchMap)); + } + if (UI::OrangeButton(Icons::PlayCircleO + " Skip Broken Map")) { + if (!UI::IsOverlayShown()) UI::ShowOverlay(); + RMC::IsPaused = true; + Renderables::Add(BrokenMapSkipWarnModalDialog()); + } + + if (TM::IsPauseMenuDisplayed()) UI::SetPreviousTooltip("To skip the map, please exit the pause menu."); + UI::EndDisabled(); + } + + void NextMapButton() override + { + if(UI::GreenButton(Icons::Play + " Next map")) { + if (RMC::IsPaused) RMC::IsPaused = false; + Log::Trace("RMST: Next map"); + UI::ShowNotification("Please wait..."); + RMC::EndTime += (3*60*1000); + startnew(CoroutineFunc(RMSTSwitchMap)); + } + } + + void GameEndNotification() override + { + if (RMC::selectedGameMode == RMC::GameMode::SurvivalTogether) { +#if TMNEXT + RMCLeaderAPI::postRMS(RMC::GoalMedalCount, Skips, SurvivedTime, PluginSettings::RMC_GoalMedal); +#endif + UI::ShowNotification( + "\\$0f0Random Map Survival Together ended!", + "You survived with a time of " + RMC::FormatTimer(SurvivedTime) + + ".\nYou got "+ RMC::GoalMedalCount + " " + tostring(PluginSettings::RMC_GoalMedal) + + " medals and " + Skips + " skips." + ); + } + } + + void UpdateRecordsLoop() { + while (RMC::IsRunning) { + if (!m_CurrentlyLoadingRecords && !isSwitchingMap) { + m_CurrentlyLoadingRecords = true; + startnew(CoroutineFunc(UpdateRecords)); + } + sleep(1000); + } + } + + void UpdateRecords() { + auto newPBs = GetPlayersPBsMLFeed(); + if (newPBs.Length > 0) // empty arrays are returned on e.g., http error + m_mapPersonalBests = newPBs; + + // Check for goal medals + if (GetApp().RootMap !is null) { + uint objectiveTime = uint(-1); + if (PluginSettings::RMC_GoalMedal == RMC::Medals[3]) objectiveTime = GetApp().RootMap.MapInfo.TMObjective_GoldTime; + if (PluginSettings::RMC_GoalMedal == RMC::Medals[2]) objectiveTime = GetApp().RootMap.MapInfo.TMObjective_SilverTime; + if (PluginSettings::RMC_GoalMedal == RMC::Medals[1]) objectiveTime = GetApp().RootMap.MapInfo.TMObjective_BronzeTime; +#if TMNEXT + if (PluginSettings::RMC_GoalMedal == RMC::Medals[4]) objectiveTime = TM::GetWorldRecordFromCache(GetApp().RootMap.MapInfo.MapUid); +#endif + + if (m_mapPersonalBests.Length > 0 && !RMC::GotGoalMedalOnCurrentMap) { + for (uint r = 0; r < m_mapPersonalBests.Length; r++) { + PBTime@ record = m_mapPersonalBests[r]; + if (record is null || record.time <= 0) continue; + + // Collaborative mode: Any player getting the goal medal benefits the entire team + if (record.time <= objectiveTime) { + @playerGotGoalActualMap = record; + RMC::GotGoalMedalOnCurrentMap = true; + RMC::GoalMedalCount++; + + // Update the specific player's goal count for leaderboard tracking + for (uint j = 0; j < m_playerScores.Length; j++) { + if (m_playerScores[j].wsid == record.wsid) { + m_playerScores[j].AddGoal(); + break; + } + } + + GotGoalMedalNotification(); + break; // Only count one goal medal per update + } + } + } + } + + m_CurrentlyLoadingRecords = false; + } + + array GetPlayersPBsMLFeed() { + array ret; +#if DEPENDENCY_MLFEEDRACEDATA + try { + auto app = cast(GetApp()); + if (app.Network is null || app.Network.ClientManiaAppPlayground is null) return {}; + auto raceData = MLFeed::GetRaceData_V4(); + if (raceData is null) return {}; + auto @players = raceData.SortedPlayers_TimeAttack; + if (players.Length == 0) return {}; + + // Update player scores list + for (uint i = 0; i < players.Length; i++) { + auto player = cast(players[i]); + if (player is null) continue; + if (player.bestTime < 1) continue; + if (player.BestRaceTimes is null || player.BestRaceTimes.Length != raceData.CPsToFinish) continue; + auto pbTime = PBTime(player); + ret.InsertLast(pbTime); + + // Ensure player is in our scores list + bool foundPlayer = false; + for (uint j = 0; j < m_playerScores.Length; j++) { + if (m_playerScores[j].wsid == pbTime.wsid) { + foundPlayer = true; + break; + } + } + if (!foundPlayer) { + m_playerScores.InsertLast(RMSTPlayerScore(pbTime)); + } + } + ret.SortAsc(); + } catch { + warn("Error while getting player PBs: " + getExceptionInfo()); + } +#endif + return ret; + } + + void RenderMVPPlayer() { + if (m_playerScores.Length > 0) { + m_playerScores.SortDesc(); + RMSTPlayerScore@ mvp = m_playerScores[0]; + UI::Text("MVP: " + mvp.name + " (" + mvp.goals + " goals, " + mvp.skips + " skips)"); + } + } + + void DrawPlayerProgress() { + if (m_playerScores.Length == 0) return; + + UI::Text("Team Member Contributions:"); + m_playerScores.SortDesc(); + + for (uint i = 0; i < Math::Min(m_playerScores.Length, 10); i++) { + RMSTPlayerScore@ player = m_playerScores[i]; + UI::Text((i+1) + ". " + player.name + ": " + player.goals + " goals, " + player.skips + " skips"); + } + } + + void RenderScores() { + UI::Text("Team Progress: " + tostring(RMC::GoalMedalCount) + " " + tostring(PluginSettings::RMC_GoalMedal) + " medals, " + Skips + " skips"); + + if (m_playerScores.Length == 0) return; + + UI::Text("Individual Contributions:"); + m_playerScores.SortDesc(); + + for (uint i = 0; i < Math::Min(m_playerScores.Length, 5); i++) { + RMSTPlayerScore@ player = m_playerScores[i]; + UI::Text((i+1) + ". " + player.name + ": " + player.goals + " goals, " + player.skips + " skips"); + } + } + + void BetterChatSendLeaderboard() { +#if DEPENDENCY_BETTERCHAT + if (m_playerScores.Length == 0) return; + + m_playerScores.SortDesc(); + string leaderboard = Icons::Users + " RMST Team Results:\n"; + leaderboard += "Team Achievement: " + tostring(RMC::GoalMedalCount) + " " + tostring(PluginSettings::RMC_GoalMedal) + " medals, " + Skips + " skips\n"; + leaderboard += "Individual Contributions:\n"; + + for (uint i = 0; i < Math::Min(m_playerScores.Length, 5); i++) { + RMSTPlayerScore@ player = m_playerScores[i]; + leaderboard += (i+1) + ". " + player.name + ": " + player.goals + " goals, " + player.skips + " skips\n"; + } + + BetterChat::SendChatMessage(leaderboard); +#endif + } + + void RenderCustomSearchWarning() { + // Implementation for custom search warning if needed + // For now, empty implementation + } + + void RenderCurrentMap() { + if (currentMap !is null) { + UI::Text("Current Map: " + currentMap.Name); + UI::Text("Author: " + currentMap.Username); + } + } + + void RenderPlayingButtons() { + SkipButtons(); + if (!PluginSettings::RMC_AutoSwitch) { + NextMapButton(); + } + } + +#else + string GetModeName() override { return "Random Map Survival Together (NOT SUPPORTED ON THIS GAME)";} +#endif +} + +class RMSTPlayerScore { + string name; + string wsid; + int goals; + int skips; + + RMSTPlayerScore(PBTime@ _player) { + wsid = _player.wsid; + name = _player.name; + goals = 0; + skips = 0; + } + + int AddGoal() { + goals = goals + 1; + return goals; + } + + int AddSkip() { + skips = skips + 1; + return skips; + } + + int opCmp(RMSTPlayerScore@ other) const { + if (goals < other.goals) return -1; + if (goals == other.goals) { + if (skips > other.skips) return -1; + if (skips == other.skips) return 0; + return 1; + } + return 1; + } +} \ No newline at end of file