/******************************************** * Mode: InstaDM * * Author: steeffeen * * Contact: steeffeen@team-devota.com * * * * With this file you can easily * * run a DeathMatch/Melee server * * based on any weapon you want. * ********************************************/ #Extends "Modes/ShootMania/ModeBase.Script.txt" #Const CompatibleMapTypes "MeleeArena,RoyalArena" #Const Version "1.1 (2014-04-04)" #Const ScriptName "InstaDM.Script.txt" #Include "MathLib" as MathLib #Include "TextLib" as TextLib #Include "Libs/Nadeo/Interface.Script.txt" as Interface #Include "Libs/Nadeo/Message.Script.txt" as Message #Include "Libs/Nadeo/ShootMania/Score.Script.txt" as Score #Include "Libs/Nadeo/ShootMania/SM.Script.txt" as SM #Include "Libs/Nadeo/ShootMania/SpawnScreen.Script.txt" as SpawnScreen // SETTINGS #Setting S_TimeLimit 600 as _("Time Limit") // Time limit on a map #Setting S_PointsLimit 50 as _("Points Limit") // Points limit on a map #Setting S_WeaponNumber 2 as _("Used Weapon (1: Rocket, 2: Laser, 3: Nucleus, 4: Arrow, else: Random)") #Setting S_ArmorMax 1 as _("Max Armor of Players") // Max armor of players #Setting S_AmmoMax 1. as _("Max Ammunition Factor") // Max ammunition factor #Setting S_AmmoGain 1. as _("Ammunition Gain Factor") // Speed of ammunition reload #Setting S_Beta2Gameplay False as _("Use Beta2 Gameplay") // Use beta 2 gameplay #Setting S_FillUpBots 0 as _("Number of Players to fill up with Bots") // Number of players up to which bots will be added // CONSTANTS #Const C_KillStreakTime 3100 // Time between hits to peform a consecutive kill streak #Const C_AutoSwitchWeapon False // Automatically switch weapons (tunnels & laser pads) - With rockets => AutoTrue #Const C_StaminaFactor 1. // Stamina max & reload factor #Const C_DisplayHitDist True // Enable to display the distance when you hit someone #Const C_NearMissDist 0.7 // Max distance to show NearMiss message (-1 to disable the messages) #Const C_AnnounceHeadshots True // Announce headshots with a headshot sound #Const C_NeutralEmblemUrl "http://team-devota.com/steeffeen/images/emblem_instadm.dds" // Url for neutral emblem #Const C_ST_ColId_Accuracy "Accuracy" #Const C_ST_ColId_KillDeath "KillDeath" #Const Description _("$<$oObjective:$> Free For All! Hit as many Opponents as possible to win.") // GLOBALES declare Ident[] G_SpawnsList; // Id of all the spawn landmarks of the map declare Real G_LongestHitDist; // Distance of the longest landed hit declare Text G_LongestHitName; // Name of the player with the longest hit declare Ident G_LeaderId; // Id of currently loading player // EXTENDS ***LogVersion*** *** MB_LogVersion(ScriptName, Version); MB_LogVersion(Interface::GetScriptName(), Interface::GetScriptVersion()); MB_LogVersion(Message::GetScriptName(), Message::GetScriptVersion()); MB_LogVersion(Score::GetScriptName(), Score::GetScriptVersion()); MB_LogVersion(SM::GetScriptName(), SM::GetScriptVersion()); MB_LogVersion(SpawnScreen::GetScriptName(), SpawnScreen::GetScriptVersion()); *** ***ModeStatusMessage*** *** declare Message = Description; if (Scores.existskey(0)) { Message ^= "\n$<$oLeading:$> $<"^Scores[0].User.Name^"$>"; } ModeStatusMessage = Message; *** ***StartServer*** *** // Set mode options UseClans = False; ForceNavMapsComputation = True; UsePlayerTagging = True; if (MB_NeutralEmblemUrl == "") MB_NeutralEmblemUrl = C_NeutralEmblemUrl; // UI UIManager.UIAll.UISequence_CanSkipIntroMT = True; SpawnScreen::SetModeName("InstaDM"); SpawnScreen::ResetRulesSection(); SpawnScreen::AddSubsection(_("Rules"), Description^"\n\nYou can see your Shot $<$08fAccuracy$> and $<$08fKill/Death Ratio$> and the Points You did since Spawning ($<$08fStreak$>) on the Scoreboard!\nVery fast consecutive Eliminations will grant you $<$08fKillstreaks$>!", 5.); SpawnScreen::CreatePrettyRules("InstaDM"); SpawnScreen::CreateScores(); SpawnScreen::CreateMapInfo(); Interface::CreateRank(); // ScoresTable ST2::SetStyle("LibST_SMBaseSolo"); ST2::SetStyle("LibST_SMBasePoints"); ST2::SetStyle("LibST_SMWithLegends"); ST2::SetFormat(2, 6); ST2::SetSizeRatio(<1.1, 1.>); ST2::CreateCol(C_ST_ColId_Accuracy, _("|Accuracy|Acc."), "-", 2.8, 80.); ST2::SetColScript(C_ST_ColId_Accuracy, """ declare netread Net_Hitrate for _Score = -1; if (Net_Hitrate > 0) { declare netread Net_HitrateHighlight for _Score = False; if (Net_HitrateHighlight) { Label_Col.Value = "$s$o$0f0"^TL::ToText(Net_Hitrate)^"%"; } else { Label_Col.Value = TL::ToText(Net_Hitrate)^"%"; } } else { Label_Col.Value = ""; } """); ST2::CreateCol(C_ST_ColId_KillDeath, _("|Kills/Deaths|K/D"), "-", 2.8, 90.); ST2::SetColScript(C_ST_ColId_KillDeath, """ declare netread Net_Winrate for _Score = -1.; if (Net_Winrate > 0.) { Label_Col.Value = TL::ToText(Net_Winrate); } else { Label_Col.Value = ""; } """); ST2::SetColLegend("LibST_SMRoundPoints", _("|Consecutive hits|Streak")); ST2::SetColScript("LibST_SMRoundPoints",""" declare Score <=> (_Score as CSmScore); declare RoundPoints = ""; if (Score.RoundPoints > 0) { RoundPoints = TL::ToText(Score.RoundPoints); } Label_Col.Value = RoundPoints; """); ST2::SetColLegend("LibST_SMPoints", _("|Total points|Points")); ST2::Build("SM"); *** ***StartMap*** *** G_SpawnsList.clear(); foreach (MapBase in MapBases) { MapBase.IsActive = True; } foreach (GaugeLandmark in MapLandmarks_Gauge) { GaugeLandmark.Gauge.ValueReal = 1.; GaugeLandmark.Gauge.Captured = True; } SM::SetupDefaultVisibility(); // Init scores Score::MatchBegin(); Score::RoundBegin(); foreach (Score in Scores) { declare Integer LastPoint for Score; LastPoint = 0; InitScore(Score); } G_LeaderId = NullId; G_LongestHitDist = 0.; // New map sound UIManager.UIAll.SendNotice(_("New Match!"), CUIConfig::ENoticeLevel::MatchInfo, Null, CUIConfig::EAvatarVariant::Default, CUIConfig::EUISound::StartRound, 0); StartTime = Now; EndTime = StartTime + S_TimeLimit * 1000; UIManager.UIAll.UISequence = CUIConfig::EUISequence::Playing; *** ***OnNewPlayer*** *** InitPlayer(Player, False); SetFooterStats(Player); *** ***OnNewSpectator*** *** InitPlayer(Spectator, False); SetFooterStats(Spectator); *** ***PlayLoop*** *** // Check for setting update CheckSettings(); // Handle events foreach (Event in PendingEvents) { switch (Event.Type) { case CSmModeEvent::EType::OnArmorEmpty: { // On armor empty if (Event.Shooter == Event.Victim || Event.Shooter == Null) { AddPoints(Event.Victim, -1); } else { if (Event.Shooter.Score != Null) { declare CurrentKillNb for Event.Shooter.Score = 0; CurrentKillNb += 1; UpdateWinrate(Event.Shooter); } if (!Event.Shooter.IsFakePlayer) { XmlRpc::OnArmorEmpty(Event); } } PassOn(Event); } case CSmModeEvent::EType::OnHit: { // On hit if (Event.Shooter == Null || Event.Victim == Null || Event.Shooter == Event.Victim) { Discard(Event); continue; } // React on hit Event.Damage = 100; SendHitDistanceMessage(Event); DoHeadShot(Event); // Grant points declare Points = 1; if (Event.Shooter.Score != Null) { if (Event.Victim.Armor <= Event.Damage) { // Victim will be eliminated - Handle killstreak declare LastKillTimes for Event.Shooter.Score = Integer[Integer]; if (LastKillTimes.count > 0) { if (LastKillTimes[LastKillTimes.count-1] + C_KillStreakTime > Now) { // Consecutive kill streak LastKillTimes[LastKillTimes.count] = Now; Points *= LastKillTimes.count; Message::SendStatusMessage(Event.Shooter, TextLib::Compose(_("KillStreak #%1!"), TextLib::ToText(LastKillTimes.count)), 2300, 0); } else { // Too slow! LastKillTimes.clear(); } } else { // Begin kill streak LastKillTimes[0] = Now; } } declare CurrentHitNb for Event.Shooter.Score = 0; CurrentHitNb += 1; UpdateHitrate(Event.Shooter); } AddPoints(Event.Shooter, Points); Event.ShooterPoints = Points; // Play sound and notice if someone is close to win if (Event.Shooter != Null && Event.Shooter.Score != Null) { declare LastPoint for Event.Shooter.Score = 0; declare Gap = S_PointsLimit - Event.Shooter.Score.Points; if (Gap > 0 && Gap <= 3) { declare Variant = 3 - Gap; declare Msg = ""; if (Gap > 1) Msg = TextLib::Compose(_("$<%1$> is %2 Points from Victory!"), Event.Shooter.Name, TextLib::ToText(Gap)); else Msg = TextLib::Compose(_("$<%1$> is 1 point from victory!"), Event.Shooter.Name); Message::SendBigMessage(Msg, 3000, 2, CUIConfig::EUISound::TieBreakPoint, Variant); } else if (Gap <= 0) { Message::SendBigMessage(TextLib::Compose(_("$<%1$> gets the Final Hit!"), Event.Shooter.Name), 3000, 3, CUIConfig::EUISound::VictoryPoint, 0 ); } else { declare SoundGap = S_PointsLimit / 5; if (SoundGap < 5) SoundGap = 5; if (Event.Shooter.Score.Points / SoundGap > LastPoint) { LastPoint = Event.Shooter.Score.Points / SoundGap; declare Msg = TextLib::Compose(_("$666Score : $fff%1 / %2"), TextLib::ToText(Event.Shooter.Score.Points), TextLib::ToText(S_PointsLimit) ); declare Variant = ((Event.Shooter.Score.Points / SoundGap) - 1); Message::SendBigMessage(Event.Shooter, Msg, 3000, 0, CUIConfig::EUISound::ScoreProgress, Variant); } } } if (!Event.Shooter.IsFakePlayer) { XmlRpc::OnHit(Event); } PassOn(Event); } case CSmModeEvent::EType::OnPlayerRequestRespawn: { // On player request respawn if (Event.Player == Null) { Discard(Event); continue; } AddPoints(Event.Player, -1); if (!Event.Player.IsFakePlayer) { XmlRpc::OnPlayerRequestRespawn(Event); } PassOn(Event); } case CSmModeEvent::EType::OnShoot: { // On shots if (Event.Shooter == Null) { Discard(Event); continue; } if (Event.Shooter.Score != Null) { declare CurrentShotNb for Event.Shooter.Score = 0; CurrentShotNb += 1; UpdateHitrate(Event.Shooter); } if (!Event.Shooter.IsFakePlayer) { XmlRpc::OnShoot(Event); } PassOn(Event); } case CSmModeEvent::EType::OnNearMiss: { // On near miss if (Event.Shooter == Null || Event.Victim == Null || Event.Shooter == Event.Victim) { Discard(Event); continue; } SendNearMissMessage(Event); if (!Event.Shooter.IsFakePlayer) { XmlRpc::OnNearMiss(Event); } PassOn(Event); } default: { // Others PassOn(Event); } } } // Manage players foreach (Player in Players) { switch (Player.SpawnStatus) { case CSmPlayer::ESpawnStatus::NotSpawned: { MeleeSpawnPlayer(Player); } } } // Manage bots ManageBots(); // Play sound and notice if someone is taking the lead if (Scores.existskey(0) && Scores[0].Points > 0 && Scores[0].Id != G_LeaderId) { G_LeaderId = Scores[0].Id; Message::SendStatusMessage(TextLib::Compose(_("$<%1$> takes the Lead!"), Scores[G_LeaderId].User.Name), 2500, 1, CUIConfig::EUISound::PhaseChange, 1); ---ModeStatusMessage--- } Message::Loop(); // Victory conditions if (Now > EndTime) { MB_StopMap = True; } else { foreach (Score in Scores) { if (Score.Points >= S_PointsLimit) { MB_StopMap = True; } } } *** ***EndMap*** *** StartTime = -1; EndTime = -1; Score::MatchEnd(); XmlRpc::SendRankings(); // End match sequence declare CSmScore Winner <=> Null; declare WinnerPoints = 0; declare MarkStat = 0; foreach (Score in Scores) { if (Score.Points > WinnerPoints) { WinnerPoints = Score.Points; Winner <=> Score; } else { if (Score.Points == WinnerPoints) { if (GetHitrate(Score) > GetHitrate(Winner)) { WinnerPoints = Score.Points; Winner <=> Score; MarkStat = 1; } else { if (GetHitrate(Score) == GetHitrate(Winner)) { Winner <=> Null; } } } } } foreach (Player in Players) { if (Player.Score != Winner) { UnspawnPlayer(Player); } else { switch (MarkStat) { case 1: { UpdateHitrate(Player, True); } } } Interface::UpdatePosition(Player); } MB_Sleep(1000); Message::CleanBigMessages(); Message::CleanStatusMessages(); UIManager.UIAll.BigMessageSound = CUIConfig::EUISound::EndRound; UIManager.UIAll.BigMessageSoundVariant = 0; if (Winner != Null && Winner.User != Null) { UIManager.UIAll.BigMessage = TextLib::Compose(_("$<%1$> wins the Match!"), Winner.User.Name); } else { UIManager.UIAll.BigMessage = _("|Match|Draw"); } MB_Sleep(2000); UIManager.UIAll.UISequence = CUIConfig::EUISequence::EndRound; UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::ForcedVisible; MB_Sleep(5000); UIManager.UIAll.UISequence = CUIConfig::EUISequence::Podium; wait(UIManager.UIAll.UISequenceIsCompleted); DestroyAllBotPlayers(); UIManager.UIAll.ScoreTableVisibility = CUIConfig::EVisibility::Normal; UIManager.UIAll.BigMessage = ""; *** ***EndServer*** *** Interface::DestroyRank(); SpawnScreen::DestroyRules(); SpawnScreen::DestroyScores(); SpawnScreen::DestroyMapInfo(); *** // ---------------------------------- // // Functions // ---------------------------------- // // Set footer stats on the scoreboard for the given player Void SetFooterStats(CSmPlayer _Player) { if (_Player == Null) { foreach (Player in AllPlayers) { SetFooterStats(Player); } } else { declare Message = ""; if (G_LongestHitDist > 0) { Message = TextLib::Compose(_("Points Limit: %1\nLongest Hit: %2m by $<%3$>$z"), TextLib::ToText(S_PointsLimit), TextLib::ToText(G_LongestHitDist), G_LongestHitName); } else { Message = TextLib::Compose(_("Points Limit: %1"), TextLib::ToText(S_PointsLimit)); } ST2::SetFooterText(Message); } } // Add points to current and total points Void AddPoints(CSmPlayer _Player, Integer _Points) { if (_Player == Null || _Player.Score == Null) return; _Player.Score.RoundPoints += _Points; _Player.Score.Points += _Points; if (_Player.Score.RoundPoints < 0) { _Player.Score.RoundPoints = 0; } if (_Player.Score.Points < 0) { _Player.Score.Points = 0; } } // Get hitrate of a player Integer GetHitrate(CSmScore _Score) { if (_Score == Null) return -1; declare CurrentShotNb for _Score = 0; declare CurrentHitNb for _Score = 0; if (CurrentShotNb > 0) { declare Hitrate = (CurrentHitNb * 1.) / (CurrentShotNb * 1.); if (Hitrate > 0.) { return MathLib::NearestInteger(Hitrate * 100.); } } return -1; } // Update hitrate on the scorestable Void UpdateHitrate(CSmPlayer _Player, Boolean _Highlight) { if (_Player == Null || _Player.Score == Null) return; declare Hitrate = GetHitrate(_Player.Score); declare netwrite Integer Net_Hitrate for _Player.Score; declare netwrite Boolean Net_HitrateHighlight for _Player.Score; Net_Hitrate = Hitrate; Net_HitrateHighlight = _Highlight; } Void UpdateHitrate(CSmPlayer _Player) { UpdateHitrate(_Player, False); } // Get winrate of a player Real GetWinrate(CSmPlayer _Player) { if (_Player == Null || _Player.Score == Null) return -1.; declare CurrentSpawnNb for _Player.Score = 0; declare CurrentKillNb for _Player.Score = 0; if (CurrentSpawnNb > 0) { declare Winrate = (CurrentKillNb * 1.) / (CurrentSpawnNb * 1.); if (Winrate > 0.) { return MathLib::NearestInteger(Winrate * 100.) / 100.; } } return -1.; } // Update winrate on the scorestable Void UpdateWinrate(CSmPlayer _Player) { if (_Player == Null || _Player.Score == Null) return; declare Winrate = GetWinrate(_Player); declare netwrite Real Net_Winrate for _Player.Score; Net_Winrate = Winrate; } // Get the currently used weapon CSmMode::EWeapon GetWeapon() { declare WeaponNumber = S_WeaponNumber; if (WeaponNumber < 1 || WeaponNumber > 4) { // Random weapon WeaponNumber = MathLib::Rand(1, 4); } // Return weapon switch (WeaponNumber) { case 2: { return CSmMode::EWeapon::Laser; } case 3: { return CSmMode::EWeapon::Nucleus; } case 4: { return CSmMode::EWeapon::Arrow; } } return CSmMode::EWeapon::Rocket; } // Get whether weapons should be switched automatically Boolean GetAutoSwitchWeapon(CSmMode::EWeapon _Weapon) { return (C_AutoSwitchWeapon || _Weapon == CSmMode::EWeapon::Rocket); } // Initialize the given score Void InitScore(CSmScore _Score) { if (_Score == Null) return; declare Integer CurrentShotNb for _Score; declare Integer CurrentHitNb for _Score; declare Integer CurrentSpawnNb for _Score; declare Integer CurrentKillNb for _Score; CurrentShotNb = 0; CurrentHitNb = 0; CurrentSpawnNb = 0; CurrentKillNb = 0; } // Initialize values for a given player Void InitPlayer(CSmPlayer _Player, Boolean _ForceResetScore) { if (_Player == Null) return; // Set player values _Player.AmmoGain = S_AmmoGain; _Player.ArmorMax = S_ArmorMax * 100; _Player.StaminaGain = C_StaminaFactor; _Player.StaminaMax = C_StaminaFactor; // Apply bot hehavior if (_Player.Driver != Null) { _Player.Driver.AggroRadius = 70.; _Player.Driver.Agressivity = 10.; _Player.Driver.AttackFilter = CSmPlayerDriver::ESmAttackFilter::AllOpposite; _Player.Driver.Behaviour = CSmPlayerDriver::ESmDriverBehaviour::Saunter; _Player.Driver.DisengageDistance = 100.; _Player.Driver.ReactionTime = 200; _Player.Driver.RocketAnticipation = True; _Player.Driver.Saunter_BaseChillingTime = 200; _Player.Driver.Saunter_ChillingTimeDelta = 100; _Player.Driver.Saunter_Radius = 70.; } // Reset statistics declare MyNewPlayer for _Player = True; if (MyNewPlayer || _ForceResetScore) { MyNewPlayer = False; InitScore(_Player.Score); } UpdateHitrate(_Player); UpdateWinrate(_Player); } // Sends a message for a near miss Void SendNearMissMessage(CSmModeEvent _Event) { // Catch invalid events if (_Event.Type != CSmModeEvent::EType::OnNearMiss) return; // Wrong event type if (C_NearMissDist <= 0.) return; // Near miss message disabled if (_Event.Shooter == Null || _Event.Victim == Null || _Event.Shooter == _Event.Victim) return; // Invalid players if (_Event.MissDist <= 0. || _Event.MissDist > C_NearMissDist) return; // Invalid miss distance // Get distance declare Message = ""; declare Dist = MathLib::NearestInteger(_Event.MissDist * 1000.); if (Dist >= 10) { Message = (Dist / 10.)^"cm!"; } else { Dist = MathLib::NearestInteger(_Event.MissDist * 10000.); if (Dist >= 10) { Message = (Dist / 10.)^"mm!"; } else { Dist = MathLib::NearestInteger(_Event.MissDist * 10000000.); Message = (Dist / 10.)^"µm!"; } } // Send message Message::SendStatusMessage(_Event.Shooter, TextLib::Compose(_("You missed $<%1$> by %2"), _Event.Victim.Name, Message), 3000, 0); } // Sends a message for hit distances Void SendHitDistanceMessage(CSmModeEvent _Event) { // Catch invalid events if (!C_DisplayHitDist) return; // Hit distance message disabled if (_Event.Type != CSmModeEvent::EType::OnHit) return; // Wrong event type if (_Event.Shooter == Null || _Event.Victim == Null || _Event.Shooter == _Event.Victim) return; // Invalid players // Get distance declare Distance = MathLib::Distance(_Event.Shooter.Position, _Event.Victim.Position); Distance = MathLib::NearestInteger(Distance * 10) / 10.; // Save longest hit if (Distance > G_LongestHitDist) { G_LongestHitDist = Distance; G_LongestHitName = _Event.Shooter.Name; SetFooterStats(Null); } declare UI <=> UIManager.GetUI(_Event.Shooter); if (UI == Null) return; // Send message UI.SendNotice(TextLib::Compose(_("%1m Hit!"), TextLib::ToText(Distance)), CUIConfig::ENoticeLevel::Default, Null, CUIConfig::EAvatarVariant::Default, CUIConfig::EUISound::Silence, 0); } // Calculates skalar of two vectors Real Skalar(Vec3 _Vec1, Vec3 _Vec2) { return _Vec1[0]*_Vec2[0] + _Vec1[1]*_Vec2[1] + _Vec1[2]*_Vec2[2]; } // Plays headshot sound on headshot Void DoHeadShot(CSmModeEvent _Event) { if (!C_AnnounceHeadshots || _Event == Null || _Event.Shooter == Null || _Event.Victim == Null) return; if (_Event.WeaponNum != GetWeaponNum(CSmMode::EWeapon::Laser)) return; declare UI <=> UIManager.GetUI(_Event.Shooter); if (UI == Null) return; declare HeadPosi = <_Event.Victim.Position[0] + _Event.Victim.AimDirection[0] * 0.2, _Event.Victim.Position[1] + 1.5, _Event.Victim.Position[2] + _Event.Victim.AimDirection[2] * 0.2>; declare ShootPosi = _Event.Shooter.Position + <0., 1.35, 0.>; declare ShootDir = _Event.Shooter.AimDirection; declare a1 = Skalar(ShootDir, ShootPosi); declare a2 = Skalar(ShootDir, ShootDir); declare a3 = Skalar(ShootDir, -HeadPosi); declare gamma = (a1+a3)/(-1*a2); declare stutz = ShootPosi + (gamma*ShootDir); declare distance = MathLib::Distance(HeadPosi, stutz); if (distance > 0.2) return; Message::SendStatusMessage(_Event.Shooter, _("Headshot!"), 2000, 0); UI.ManialinkPage = """//{{{Now}}}